package synthetic

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

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

// evalSampleDef compiles a SignalDef and evaluates it against per-source
// Samples keyed by source node id, returning the output Sample.
func evalSampleDef(t *testing.T, def SignalDef, srcVals map[string]dsp.Sample) dsp.Sample {
	t.Helper()
	rg, err := compileGraph(def)
	if err != nil {
		t.Fatalf("compileGraph: %v", err)
	}
	out, err := rg.evalSample(srcVals)
	if err != nil {
		t.Fatalf("evalSample: %v", err)
	}
	return out
}

// TestArrayElementwiseChain runs an array source through an elementwise op
// (gain) and asserts the output stays an array, broadcast per element.
func TestArrayElementwiseChain(t *testing.T) {
	def := SignalDef{
		Name: "scaled",
		Graph: &Graph{
			Output: "out",
			Nodes: []GraphNode{
				{ID: "a", Kind: "source", DS: "x", Signal: "wave"},
				{ID: "g", Kind: "op", Op: "gain", Inputs: []string{"a"}, Params: map[string]any{"gain": 2.0}},
				{ID: "out", Kind: "output", Inputs: []string{"g"}},
			},
		},
	}
	out := evalSampleDef(t, def, map[string]dsp.Sample{"a": dsp.Array([]float64{1, 2, 3})})
	if !out.IsArray {
		t.Fatalf("want array output, got %v", out)
	}
	want := []float64{2, 4, 6}
	for i, v := range want {
		if out.Arr[i] != v {
			t.Errorf("scaled[%d]: want %v, got %v", i, v, out.Arr[i])
		}
	}
}

// TestArrayReductionToScalar runs an array source into mean (array→scalar) and
// asserts a scalar output and an array-output compile type for the producer.
func TestArrayReductionToScalar(t *testing.T) {
	def := SignalDef{
		Name: "avg",
		Graph: &Graph{
			Output: "out",
			Nodes: []GraphNode{
				{ID: "a", Kind: "source", DS: "x", Signal: "wave"},
				{ID: "m", Kind: "op", Op: "mean", Inputs: []string{"a"}},
				{ID: "out", Kind: "output", Inputs: []string{"m"}},
			},
		},
	}
	out := evalSampleDef(t, def, map[string]dsp.Sample{"a": dsp.Array([]float64{2, 4, 6, 8})})
	if out.IsArray {
		t.Fatalf("want scalar output, got array %v", out.Arr)
	}
	if math.Abs(out.F-5) > 1e-9 {
		t.Errorf("mean: want 5, got %v", out.F)
	}
}

// TestArrayOutTypeMetadata verifies compileGraph reports an array output type
// for a pure-elementwise array graph and scalar for a reduction graph.
func TestArrayOutTypeMetadata(t *testing.T) {
	arrayGraph := SignalDef{
		Name: "fftout",
		Graph: &Graph{
			Output: "out",
			Nodes: []GraphNode{
				{ID: "a", Kind: "source", DS: "x", Signal: "wave"},
				{ID: "f", Kind: "op", Op: "fft", Inputs: []string{"a"}},
				{ID: "out", Kind: "output", Inputs: []string{"f"}},
			},
		},
	}
	rg, err := compileGraph(arrayGraph)
	if err != nil {
		t.Fatalf("compileGraph: %v", err)
	}
	if rg.outType != dsp.ValArray {
		t.Errorf("fft graph outType: want ValArray, got %v", rg.outType)
	}

	reduction := SignalDef{
		Name: "sumout",
		Graph: &Graph{
			Output: "out",
			Nodes: []GraphNode{
				{ID: "a", Kind: "source", DS: "x", Signal: "wave"},
				{ID: "s", Kind: "op", Op: "sum", Inputs: []string{"a"}},
				{ID: "out", Kind: "output", Inputs: []string{"s"}},
			},
		},
	}
	rg2, err := compileGraph(reduction)
	if err != nil {
		t.Fatalf("compileGraph: %v", err)
	}
	if rg2.outType != dsp.ValScalar {
		t.Errorf("sum graph outType: want ValScalar, got %v", rg2.outType)
	}
}

// TestStatefulRejectsArray verifies a stateful op (moving_average) errors when
// fed an array input at runtime.
func TestStatefulRejectsArray(t *testing.T) {
	def := SignalDef{
		Name: "ma",
		Graph: &Graph{
			Output: "out",
			Nodes: []GraphNode{
				{ID: "a", Kind: "source", DS: "x", Signal: "wave"},
				{ID: "m", Kind: "op", Op: "moving_average", Inputs: []string{"a"}, Params: map[string]any{"window": 3.0}},
				{ID: "out", Kind: "output", Inputs: []string{"m"}},
			},
		},
	}
	rg, err := compileGraph(def)
	if err != nil {
		t.Fatalf("compileGraph: %v", err)
	}
	if _, err := rg.evalSample(map[string]dsp.Sample{"a": dsp.Array([]float64{1, 2, 3})}); err == nil {
		t.Error("expected moving_average to reject an array input")
	}
}

// TestSubscribeArrayPassthrough is an end-to-end check that a synthetic with an
// array-valued source and an elementwise op emits a []float64 over the broker,
// and that GetMetadata reports the waveform type.
func TestSubscribeArrayPassthrough(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: []float64{1, 2, 3}, Quality: datasource.QualityGood},
		{Timestamp: base.Add(2 * time.Second), Data: []float64{4, 5, 6}, 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)
	}
	// gain x2 elementwise keeps the value an array.
	if err := syn.AddSignal(SignalDef{
		Name: "scaled",
		Graph: &Graph{Output: "out", Nodes: []GraphNode{
			{ID: "a", Kind: "source", DS: "src", Signal: "x"},
			{ID: "g", Kind: "op", Op: "gain", Inputs: []string{"a"}, Params: map[string]any{"gain": 2.0}},
			{ID: "out", Kind: "output", Inputs: []string{"g"}},
		}},
	}); err != nil {
		t.Fatal(err)
	}
	// An fft-based signal has a statically-known array output type (the source's
	// runtime type need not be known), so its metadata reports the waveform type.
	if err := syn.AddSignal(SignalDef{
		Name: "spectrum",
		Graph: &Graph{Output: "out", Nodes: []GraphNode{
			{ID: "a", Kind: "source", DS: "src", Signal: "x"},
			{ID: "f", Kind: "op", Op: "fft", Inputs: []string{"a"}},
			{ID: "out", Kind: "output", Inputs: []string{"f"}},
		}},
	}); err != nil {
		t.Fatal(err)
	}

	meta, err := syn.GetMetadata(ctx, "spectrum")
	if err != nil {
		t.Fatal(err)
	}
	if meta.Type != datasource.TypeFloat64Array {
		t.Errorf("metadata type: want TypeFloat64Array, got %v", meta.Type)
	}

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

	want := [][]float64{{2, 4, 6}, {8, 10, 12}}
	for i, w := range want {
		select {
		case v := <-ch:
			arr, ok := v.Data.([]float64)
			if !ok {
				t.Fatalf("emit #%d: want []float64, got %T", i, v.Data)
			}
			if len(arr) != len(w) {
				t.Fatalf("emit #%d: want len %d, got %d", i, len(w), len(arr))
			}
			for k, val := range w {
				if arr[k] != val {
					t.Errorf("emit #%d [%d]: want %v, got %v", i, k, val, arr[k])
				}
			}
		case <-time.After(2 * time.Second):
			t.Fatalf("timeout waiting for emit #%d", i)
		}
	}
}
