package dsp

import (
	"math"
	"testing"
)

// TestExprFunctions exercises every built-in function branch of parseCall plus
// the two-argument forms and the right-associative power operator.
func TestExprFunctions(t *testing.T) {
	st := map[string]any{}
	cases := []struct {
		expr   string
		inputs []float64
		want   float64
	}{
		{"exp(a)", []float64{1}, math.E},
		{"log(a)", []float64{math.E}, 1},
		{"ln(a)", []float64{math.E}, 1},
		{"log2(a)", []float64{8}, 3},
		{"log10(a)", []float64{1000}, 3},
		{"sqrt(a)", []float64{9}, 3},
		{"abs(a)", []float64{-4}, 4},
		{"sin(a)", []float64{0}, 0},
		{"cos(a)", []float64{0}, 1},
		{"tan(a)", []float64{0}, 0},
		{"asin(a)", []float64{1}, math.Pi / 2},
		{"acos(a)", []float64{1}, 0},
		{"atan(a)", []float64{1}, math.Pi / 4},
		{"atan2(a, b)", []float64{1, 1}, math.Pi / 4},
		{"pow(a, b)", []float64{2, 10}, 1024},
		{"floor(a)", []float64{2.9}, 2},
		{"ceil(a)", []float64{2.1}, 3},
		{"round(a)", []float64{2.5}, 3},
		{"min(a, b)", []float64{3, 7}, 3},
		{"max(a, b)", []float64{3, 7}, 7},
		{"a ^ b", []float64{2, 3}, 8},
		{"2 ^ 3 ^ 2", []float64{}, 512}, // right-associative: 2^(3^2)
		{"-a ^ 2", []float64{3}, -9},    // unary minus binds outside power
	}
	for _, tc := range cases {
		t.Run(tc.expr, func(t *testing.T) {
			n := &ExprNode{Expr: tc.expr}
			got, err := n.Process(tc.inputs, st)
			if err != nil {
				t.Fatalf("Process(%q): %v", tc.expr, err)
			}
			if math.Abs(got-tc.want) > 1e-9 {
				t.Errorf("Process(%q) = %v, want %v", tc.expr, got, tc.want)
			}
		})
	}
}

// TestExprFunctionErrors covers the error branches of parseCall/parseFactor.
func TestExprFunctionErrors(t *testing.T) {
	st := map[string]any{}
	cases := []string{
		"bogus(a)", // unknown function
		"sqrt(a",   // missing ')'
		"sqrt(",    // empty / unexpected end inside call
		"@",        // unexpected character
		"(a + 1",   // missing closing parenthesis
		"1.2.3",    // invalid number
	}
	for _, expr := range cases {
		t.Run(expr, func(t *testing.T) {
			n := &ExprNode{Expr: expr}
			if _, err := n.Process([]float64{1}, st); err == nil {
				t.Errorf("Process(%q): want error", expr)
			}
		})
	}
}

// TestArrayNodeScalarAdapters covers the legacy scalar Node interface
// (Type + Process) on the array nodes, which the array-path tests skip.
func TestArrayNodeScalarAdapters(t *testing.T) {
	st := map[string]any{}

	// Reductions: Process treats its float64 inputs as a single-element array,
	// so each reduction over one value returns that value.
	reductions := []struct {
		node ArrayNode
		typ  string
	}{
		{&SumNode{}, "sum"},
		{&MeanNode{}, "mean"},
		{&MinNode{}, "min"},
		{&MaxNode{}, "max"},
		{&IndexNode{I: 0}, "index"},
	}
	for _, r := range reductions {
		t.Run(r.typ, func(t *testing.T) {
			if r.node.Type() != r.typ {
				t.Errorf("Type() = %q, want %q", r.node.Type(), r.typ)
			}
			got, err := r.node.Process([]float64{42}, st)
			if err != nil {
				t.Fatalf("Process: %v", err)
			}
			if got != 42 {
				t.Errorf("Process = %v, want 42", got)
			}
		})
	}

	// LengthNode over a single scalar input → length 1.
	ln := &LengthNode{}
	if ln.Type() != "length" {
		t.Errorf("LengthNode.Type() = %q", ln.Type())
	}
	if got, err := ln.Process([]float64{7}, st); err != nil || got != 1 {
		t.Errorf("LengthNode.Process = %v, %v; want 1", got, err)
	}

	// SliceNode.Process returns the first element of the resulting slice.
	sn := &SliceNode{Start: 0, End: 0}
	if sn.Type() != "slice" {
		t.Errorf("SliceNode.Type() = %q", sn.Type())
	}
	if got, err := sn.Process([]float64{5}, st); err != nil || got != 5 {
		t.Errorf("SliceNode.Process = %v, %v; want 5", got, err)
	}

	// FFTNode.Process returns the first magnitude bin (DC term = the value).
	fn := &FFTNode{}
	if fn.Type() != "fft" {
		t.Errorf("FFTNode.Type() = %q", fn.Type())
	}
	if got, err := fn.Process([]float64{3}, st); err != nil || math.Abs(got-3) > 1e-9 {
		t.Errorf("FFTNode.Process = %v, %v; want 3", got, err)
	}
}

// TestArrayNodeProcessErrors covers the error propagation through the scalar
// Process adapters.
func TestArrayNodeProcessErrors(t *testing.T) {
	st := map[string]any{}
	// IndexNode with an out-of-range index propagates the reduction error.
	n := &IndexNode{I: 5}
	if _, err := n.Process([]float64{1}, st); err == nil {
		t.Error("IndexNode.Process out of range: want error")
	}
}
