package synthetic

import (
	"context"
	"log/slog"
	"os"
	"testing"
	"time"

	"github.com/uopi/uopi/internal/broker"
	"github.com/uopi/uopi/internal/datasource"
)

// seqSource is a test DataSource that emits a fixed sequence of values, each
// carrying its own timestamp, so tests can control upstream sample times.
type seqSource struct {
	name string
	seq  []datasource.Value
}

func (s *seqSource) Name() string                                               { return s.name }
func (s *seqSource) Connect(context.Context) error                              { return nil }
func (s *seqSource) ListSignals(context.Context) ([]datasource.Metadata, error) { return nil, nil }
func (s *seqSource) GetMetadata(context.Context, string) (datasource.Metadata, error) {
	return datasource.Metadata{Name: "x", Type: datasource.TypeFloat64}, nil
}
func (s *seqSource) Write(context.Context, string, any) error { return datasource.ErrNotWritable }
func (s *seqSource) History(context.Context, string, time.Time, time.Time, int) ([]datasource.Value, error) {
	return nil, datasource.ErrHistoryUnavailable
}
func (s *seqSource) Subscribe(ctx context.Context, _ string, ch chan<- datasource.Value) (datasource.CancelFunc, error) {
	go func() {
		for _, v := range s.seq {
			select {
			case ch <- v:
			case <-ctx.Done():
				return
			}
			time.Sleep(8 * time.Millisecond)
		}
	}()
	return func() {}, nil
}

// TestSubscribePreservesUpstreamTimestamp verifies a single-source synthetic
// emits each computed value with the upstream sample's timestamp.
func TestSubscribePreservesUpstreamTimestamp(t *testing.T) {
	log := slog.New(slog.NewTextHandler(os.Stderr, nil))
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	base := time.Date(2026, 6, 19, 10, 0, 0, 0, time.UTC)
	src := &seqSource{name: "src", seq: []datasource.Value{
		{Timestamp: base.Add(1 * time.Second), Data: 1.0, Quality: datasource.QualityGood},
		{Timestamp: base.Add(2 * time.Second), Data: 2.0, Quality: datasource.QualityGood},
		{Timestamp: base.Add(3 * time.Second), Data: 3.0, Quality: datasource.QualityGood},
	}}

	brk := broker.New(ctx, log)
	brk.Register(src)
	syn := New(t.TempDir(), brk, log)
	if err := syn.Connect(ctx); err != nil {
		t.Fatal(err)
	}
	if err := syn.AddSignal(SignalDef{
		Name: "g", DS: "src", Signal: "x",
		Pipeline: []NodeDef{{Type: "gain", Params: map[string]any{"gain": 10.0}}},
	}); err != nil {
		t.Fatal(err)
	}

	ch := make(chan datasource.Value, 8)
	if _, err := syn.Subscribe(ctx, "g", ch); err != nil {
		t.Fatal(err)
	}

	want := []time.Time{base.Add(1 * time.Second), base.Add(2 * time.Second), base.Add(3 * time.Second)}
	for i, w := range want {
		select {
		case v := <-ch:
			if !v.Timestamp.Equal(w) {
				t.Errorf("emit #%d timestamp: want %s, got %s", i, w, v.Timestamp)
			}
		case <-time.After(2 * time.Second):
			t.Fatalf("timeout waiting for emit #%d", i)
		}
	}
}

// TestSubscribeMultiSourceUsesLatestTimestamp verifies that a synthetic combining
// two independent sources stamps each output with the MOST RECENT contributing
// sample time — not the timestamp of whichever source happened to trigger the
// computation. A slow source carrying a stale timestamp must not drag the output
// backwards in time (which previously produced wrong/non-monotonic plot points).
func TestSubscribeMultiSourceUsesLatestTimestamp(t *testing.T) {
	log := slog.New(slog.NewTextHandler(os.Stderr, nil))
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	now := time.Date(2026, 6, 19, 12, 0, 0, 0, time.UTC)
	// Fast source A with current timestamps.
	a := &seqSource{name: "A", seq: []datasource.Value{
		{Timestamp: now.Add(10 * time.Second), Data: 1.0, Quality: datasource.QualityGood},
		{Timestamp: now.Add(11 * time.Second), Data: 2.0, Quality: datasource.QualityGood},
		{Timestamp: now.Add(12 * time.Second), Data: 3.0, Quality: datasource.QualityGood},
		{Timestamp: now.Add(13 * time.Second), Data: 4.0, Quality: datasource.QualityGood},
	}}
	// Slow source B: a single sample with a much older timestamp.
	b := &seqSource{name: "B", seq: []datasource.Value{
		{Timestamp: now.Add(1 * time.Second), Data: 100.0, Quality: datasource.QualityGood},
	}}

	brk := broker.New(ctx, log)
	brk.Register(a)
	brk.Register(b)
	syn := New(t.TempDir(), brk, log)
	if err := syn.Connect(ctx); err != nil {
		t.Fatal(err)
	}
	if err := syn.AddSignal(SignalDef{
		Name: "diff",
		Graph: &Graph{Output: "out", Nodes: []GraphNode{
			{ID: "sa", Kind: "source", DS: "A", Signal: "x"},
			{ID: "sb", Kind: "source", DS: "B", Signal: "x"},
			{ID: "sub", Kind: "op", Op: "subtract", Inputs: []string{"sa", "sb"}},
			{ID: "out", Kind: "output", Inputs: []string{"sub"}},
		}},
	}); err != nil {
		t.Fatal(err)
	}

	ch := make(chan datasource.Value, 16)
	if _, err := syn.Subscribe(ctx, "diff", ch); err != nil {
		t.Fatal(err)
	}

	var last time.Time
	for i := 0; i < 4; i++ {
		select {
		case v := <-ch:
			// The stale source-B timestamp (t=1s) must never be used: every output
			// is stamped with the newest input time, so emits stay monotonic.
			if v.Timestamp.Equal(now.Add(1 * time.Second)) {
				t.Errorf("emit #%d used the stale source-B timestamp %s", i, v.Timestamp)
			}
			if !last.IsZero() && v.Timestamp.Before(last) {
				t.Errorf("emit #%d went backwards: %s before previous %s", i, v.Timestamp, last)
			}
			last = v.Timestamp
		case <-time.After(2 * time.Second):
			t.Fatalf("timeout waiting for emit #%d", i)
		}
	}
}
