package dsp

import (
	"math"
	"testing"
)

func TestGainNode(t *testing.T) {
	n := &GainNode{Gain: 3.0}
	state := map[string]any{}
	got, err := n.Process([]float64{2.0}, state)
	if err != nil {
		t.Fatal(err)
	}
	if got != 6.0 {
		t.Errorf("GainNode: want 6.0, got %v", got)
	}
}

func TestGainNodeNoInputs(t *testing.T) {
	n := &GainNode{Gain: 1.0}
	_, err := n.Process(nil, map[string]any{})
	if err == nil {
		t.Error("GainNode: expected error with no inputs")
	}
}

func TestOffsetNode(t *testing.T) {
	n := &OffsetNode{Offset: 5.0}
	state := map[string]any{}
	got, err := n.Process([]float64{3.0}, state)
	if err != nil {
		t.Fatal(err)
	}
	if got != 8.0 {
		t.Errorf("OffsetNode: want 8.0, got %v", got)
	}
}

func TestAddNode(t *testing.T) {
	n := &AddNode{}
	state := map[string]any{}
	got, err := n.Process([]float64{1.0, 2.0, 3.0}, state)
	if err != nil {
		t.Fatal(err)
	}
	if got != 6.0 {
		t.Errorf("AddNode: want 6.0, got %v", got)
	}
}

func TestSubtractNode(t *testing.T) {
	tests := []struct {
		name   string
		inputs []float64
		want   float64
		errOk  bool
	}{
		{"basic", []float64{10.0, 3.0}, 7.0, false},
		{"negative result", []float64{3.0, 10.0}, -7.0, false},
		{"too few inputs", []float64{1.0}, 0, true},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			n := &SubtractNode{}
			got, err := n.Process(tc.inputs, map[string]any{})
			if tc.errOk {
				if err == nil {
					t.Error("expected error")
				}
				return
			}
			if err != nil {
				t.Fatal(err)
			}
			if got != tc.want {
				t.Errorf("SubtractNode: want %v, got %v", tc.want, got)
			}
		})
	}
}

func TestMultiplyNode(t *testing.T) {
	n := &MultiplyNode{}
	got, err := n.Process([]float64{2.0, 3.0, 4.0}, map[string]any{})
	if err != nil {
		t.Fatal(err)
	}
	if got != 24.0 {
		t.Errorf("MultiplyNode: want 24.0, got %v", got)
	}
}

func TestDivideNode(t *testing.T) {
	tests := []struct {
		name   string
		inputs []float64
		want   float64
		errOk  bool
	}{
		{"basic", []float64{10.0, 2.0}, 5.0, false},
		{"zero denominator", []float64{10.0, 0.0}, 0.0, false},
		{"too few inputs", []float64{1.0}, 0, true},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			n := &DivideNode{}
			got, err := n.Process(tc.inputs, map[string]any{})
			if tc.errOk {
				if err == nil {
					t.Error("expected error")
				}
				return
			}
			if err != nil {
				t.Fatal(err)
			}
			if got != tc.want {
				t.Errorf("DivideNode: want %v, got %v", tc.want, got)
			}
		})
	}
}

func TestMovingAverageNode(t *testing.T) {
	n := &MovingAverageNode{Window: 3}
	state := map[string]any{}

	// Feed 1, 2, 3; window fills up
	values := []float64{1, 2, 3}
	wants := []float64{1, 1.5, 2.0}
	for i, v := range values {
		got, err := n.Process([]float64{v}, state)
		if err != nil {
			t.Fatalf("step %d: %v", i, err)
		}
		if math.Abs(got-wants[i]) > 1e-9 {
			t.Errorf("step %d: want %v, got %v", i, wants[i], got)
		}
	}
	// Feed 4; window slides: [2, 3, 4] → avg 3.0
	got, err := n.Process([]float64{4}, state)
	if err != nil {
		t.Fatal(err)
	}
	if math.Abs(got-3.0) > 1e-9 {
		t.Errorf("sliding window: want 3.0, got %v", got)
	}
}

func TestRMSNode(t *testing.T) {
	n := &RMSNode{Window: 2}
	state := map[string]any{}
	// Feed 3.0 and 4.0; RMS over [3,4] = sqrt((9+16)/2) = sqrt(12.5)
	n.Process([]float64{3.0}, state)
	got, err := n.Process([]float64{4.0}, state)
	if err != nil {
		t.Fatal(err)
	}
	want := math.Sqrt(12.5)
	if math.Abs(got-want) > 1e-9 {
		t.Errorf("RMSNode: want %v, got %v", want, got)
	}
}

func TestDerivativeNode(t *testing.T) {
	n := &DerivativeNode{}
	state := map[string]any{}

	// First call should return 0
	got, err := n.Process([]float64{1.0}, state)
	if err != nil {
		t.Fatal(err)
	}
	if got != 0.0 {
		t.Errorf("DerivativeNode first call: want 0, got %v", got)
	}

	// Second call should return a non-zero derivative
	got, err = n.Process([]float64{2.0}, state)
	if err != nil {
		t.Fatal(err)
	}
	// dt is very small (nanoseconds), so derivative should be large and positive
	if got <= 0 {
		t.Errorf("DerivativeNode second call: expected positive derivative, got %v", got)
	}
}

func TestClampNode(t *testing.T) {
	tests := []struct {
		input float64
		min   float64
		max   float64
		want  float64
	}{
		{5.0, 0.0, 10.0, 5.0},
		{-5.0, 0.0, 10.0, 0.0},
		{15.0, 0.0, 10.0, 10.0},
	}
	for _, tc := range tests {
		n := &ClampNode{Min: tc.min, Max: tc.max}
		got, err := n.Process([]float64{tc.input}, map[string]any{})
		if err != nil {
			t.Fatal(err)
		}
		if got != tc.want {
			t.Errorf("ClampNode(%v): want %v, got %v", tc.input, tc.want, got)
		}
	}
}

func TestThresholdNode(t *testing.T) {
	n := &ThresholdNode{Threshold: 5.0, High: 1.0, Low: 0.0}
	tests := []struct {
		input float64
		want  float64
	}{
		{3.0, 0.0},  // below threshold
		{5.0, 1.0},  // at threshold (outputs High)
		{10.0, 1.0}, // above threshold
	}
	for _, tc := range tests {
		got, err := n.Process([]float64{tc.input}, map[string]any{})
		if err != nil {
			t.Fatal(err)
		}
		if got != tc.want {
			t.Errorf("ThresholdNode(%v): want %v, got %v", tc.input, tc.want, got)
		}
	}
}

func TestExprNode(t *testing.T) {
	tests := []struct {
		name   string
		expr   string
		inputs []float64
		want   float64
		errOk  bool
	}{
		{"addition", "a + b", []float64{3, 4}, 7.0, false},
		{"multiplication", "a * b", []float64{3, 4}, 12.0, false},
		{"complex", "(a + b) * c", []float64{1, 2, 3}, 9.0, false},
		{"division", "a / b", []float64{10, 2}, 5.0, false},
		{"subtraction", "a - b", []float64{10, 3}, 7.0, false},
		{"literal", "2 + 3", []float64{}, 5.0, false},
		{"negative factor", "-a + b", []float64{3, 5}, 2.0, false},
		{"nested parens", "(a + (b * c))", []float64{1, 2, 3}, 7.0, false},
		{"invalid var", "x + 1", []float64{1}, 0, true},
		{"invalid syntax", "a ++ b", []float64{1, 2}, 0, true},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			n := &ExprNode{Expr: tc.expr}
			got, err := n.Process(tc.inputs, map[string]any{})
			if tc.errOk {
				if err == nil {
					t.Errorf("ExprNode(%q): expected error", tc.expr)
				}
				return
			}
			if err != nil {
				t.Fatalf("ExprNode(%q): %v", tc.expr, err)
			}
			if math.Abs(got-tc.want) > 1e-9 {
				t.Errorf("ExprNode(%q): want %v, got %v", tc.expr, tc.want, got)
			}
		})
	}
}

func TestLuaNode(t *testing.T) {
	tests := []struct {
		name   string
		script string
		inputs []float64
		want   float64
		errOk  bool
	}{
		{"multiply input", "return a * 2", []float64{3.0}, 6.0, false},
		{"add two inputs", "return a + b", []float64{3.0, 4.0}, 7.0, false},
		{"constant", "return 42", []float64{}, 42.0, false},
		{"math lib", "return math.sqrt(a)", []float64{4.0}, 2.0, false},
		{"no return", "local x = a + 1", []float64{1.0}, 0, true},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			n := &LuaNode{Script: tc.script}
			state := map[string]any{}
			got, err := n.Process(tc.inputs, state)
			if tc.errOk {
				if err == nil {
					t.Errorf("LuaNode(%q): expected error", tc.script)
				}
				return
			}
			if err != nil {
				t.Fatalf("LuaNode(%q): %v", tc.script, err)
			}
			if math.Abs(got-tc.want) > 1e-9 {
				t.Errorf("LuaNode(%q): want %v, got %v", tc.script, tc.want, got)
			}
		})
	}
}

func TestLuaNodeStateReuse(t *testing.T) {
	// Verify the Lua VM is reused across calls (stateful counter example)
	n := &LuaNode{Script: `
count = (count or 0) + 1
return count
`}
	state := map[string]any{}
	for i := 1; i <= 3; i++ {
		got, err := n.Process(nil, state)
		if err != nil {
			t.Fatalf("call %d: %v", i, err)
		}
		if int(got) != i {
			t.Errorf("call %d: want %d, got %v", i, i, got)
		}
	}
}

func TestNodeTypes(t *testing.T) {
	nodes := []Node{
		&GainNode{},
		&OffsetNode{},
		&AddNode{},
		&SubtractNode{},
		&MultiplyNode{},
		&DivideNode{},
		&MovingAverageNode{},
		&RMSNode{},
		&DerivativeNode{},
		&ClampNode{},
		&ThresholdNode{},
		&ExprNode{},
		&LuaNode{},
	}
	types := []string{
		"gain", "offset", "add", "subtract", "multiply", "divide",
		"moving_average", "rms", "derivative", "clamp", "threshold", "expr", "lua",
	}
	for i, n := range nodes {
		if n.Type() != types[i] {
			t.Errorf("node %T: want type %q, got %q", n, types[i], n.Type())
		}
	}
}
