package ui

import (
	"math"
	"reflect"
	"testing"
	"time"
)

func TestAnalyzeSupportsArbitraryBandCounts(t *testing.T) {
	v := NewVisualizer(44100)
	samples := make([]float64, classicPeakFFTSize)
	for i := range samples {
		samples[i] = math.Sin(2 * math.Pi * 440 * float64(i) / v.sr)
	}

	for _, spec := range []VisAnalysisSpec{
		spectrumAnalysisSpec(DefaultSpectrumBands),
		{BandCount: 17, FFTSize: defaultFFTSize},
		{BandCount: classicPeakSpectrumBands, FFTSize: classicPeakFFTSize},
	} {
		bands := v.Analyze(samples, spec)
		if len(bands) != spec.BandCount {
			t.Fatalf("Analyze(..., %+v) len = %d, want %d", spec, len(bands), spec.BandCount)
		}
	}
}

func TestBuildSpectrumEdgesPreservesLegacyDefaultLayout(t *testing.T) {
	got := buildSpectrumEdges(DefaultSpectrumBands)
	want := legacySpectrumEdges[:]
	if !reflect.DeepEqual(got, want) {
		t.Fatalf("buildSpectrumEdges(%d) = %v, want %v", DefaultSpectrumBands, got, want)
	}
}

func TestAnalyzeDecayStateIsIndependentPerAnalysisSpec(t *testing.T) {
	v := NewVisualizer(44100)
	specA := spectrumAnalysisSpec(DefaultSpectrumBands)
	specB := VisAnalysisSpec{BandCount: DefaultSpectrumBands, FFTSize: classicPeakFFTSize}
	v.prevBySpec[specA] = uniformBandsN(specA.BandCount, 0.5)
	v.prevBySpec[specB] = uniformBandsN(specB.BandCount, 0.8)

	bandsA := v.Analyze(nil, specA)
	bandsB := v.Analyze(nil, specB)

	if got := bandsA[0]; math.Abs(got-0.4) > classicPeakTestEpsilon {
		t.Fatalf("default-spec decay = %v, want 0.4", got)
	}
	if got := bandsB[0]; math.Abs(got-0.64) > classicPeakTestEpsilon {
		t.Fatalf("classic-peak-spec decay = %v, want 0.64", got)
	}
}

func TestAverageSpectrumRangeLinearDistinguishesSubBinLowBands(t *testing.T) {
	magnitudes := make([]float64, classicPeakFFTSize/2)
	for i := range magnitudes {
		magnitudes[i] = float64(i)
	}

	spec := VisAnalysisSpec{BandCount: classicPeakSpectrumBands, FFTSize: classicPeakFFTSize}
	edges := buildSpectrumEdges(spec.BandCount)
	binHz := 44100.0 / float64(spec.FFTSize)
	low := make([]float64, 3)
	for i := range low {
		low[i] = averageSpectrumRangeLinear(magnitudes, edges[i]/binHz, edges[i+1]/binHz)
	}

	if !(low[0] < low[1] && low[1] < low[2]) {
		t.Fatalf("low sub-bin bands collapsed unexpectedly: got %v", low)
	}
}

func TestRenderOnlyDriverUsesDefaultTickInterval(t *testing.T) {
	v := NewVisualizer(44100)
	// Pulse is a per-frame-animated spectrum mode and stays on the default
	// (TickFast) cadence — VisBars/Bricks/Columns/etc. opt into TickAnim.
	activateMode(t, v, VisPulse)

	if got := v.TickInterval(VisTickContext{Playing: true}); got != TickFast {
		t.Fatalf("TickInterval(playing) = %v, want %v", got, TickFast)
	}
	if got := v.TickInterval(VisTickContext{OverlayActive: true}); got != TickSlow {
		t.Fatalf("TickInterval(overlay) = %v, want %v", got, TickSlow)
	}
	if got := v.TickInterval(VisTickContext{}); got != TickSlow {
		t.Fatalf("TickInterval(idle) = %v, want %v", got, TickSlow)
	}
}

func TestSmoothBarsDriverUsesAnimTick(t *testing.T) {
	v := NewVisualizer(44100)
	activateMode(t, v, VisBars)

	if got := v.TickInterval(VisTickContext{Playing: true}); got != TickAnim {
		t.Fatalf("Bars TickInterval(playing) = %v, want %v", got, TickAnim)
	}
}

func TestAdvanceSmoothingEasesTowardBands(t *testing.T) {
	v := NewVisualizer(44100)
	v.bands = []float64{1.0, 0.0}
	v.smoothedBands = []float64{0.0, 1.0}

	t0 := time.Unix(0, 0)
	v.lastSmoothTick = t0
	v.advanceSmoothing(t0.Add(TickAnim))

	// Expectations derive from the same easing call the implementation uses,
	// so changes to the rate constants don't silently invalidate the test.
	dt := TickAnim.Seconds()
	wantRise := classicPeakStep(0, 1, dt)
	wantFall := classicPeakStep(1, 0, dt)
	if got := v.smoothedBands[0]; math.Abs(got-wantRise) > 1e-9 {
		t.Fatalf("rise step = %v, want %v", got, wantRise)
	}
	if got := v.smoothedBands[1]; math.Abs(got-wantFall) > 1e-9 {
		t.Fatalf("fall step = %v, want %v", got, wantFall)
	}
	if (1 - wantFall) >= wantRise {
		t.Fatalf("expected decay slower than attack: rise %v, fall %v", wantRise, wantFall)
	}
}

func TestAdvanceSmoothingResizesOnBandCountChange(t *testing.T) {
	v := NewVisualizer(44100)
	v.smoothedBands = []float64{0.5, 0.5}
	v.bands = []float64{0.1, 0.2, 0.3}
	v.advanceSmoothing(time.Time{})

	if len(v.smoothedBands) != len(v.bands) {
		t.Fatalf("smoothedBands len = %d, want %d", len(v.smoothedBands), len(v.bands))
	}
	for i, got := range v.smoothedBands {
		if got != v.bands[i] {
			t.Fatalf("smoothedBands[%d] = %v, want %v after resync", i, got, v.bands[i])
		}
	}
}

func TestDefaultDriverTickGatesAnalyzeAtAnalyzeCadence(t *testing.T) {
	v := NewVisualizer(44100)
	activateMode(t, v, VisBars)

	calls := 0
	analyze := func(spec VisAnalysisSpec) []float64 {
		calls++
		return uniformBandsN(spec.BandCount, 0.4)
	}

	t0 := time.Unix(0, 0)
	// First tick analyzes (no prior timestamp).
	v.Tick(VisTickContext{Now: t0, Playing: true, Analyze: analyze})
	// A tick well within TickAnalyze should NOT analyze again.
	v.Tick(VisTickContext{Now: t0.Add(TickAnim), Playing: true, Analyze: analyze})
	if calls != 1 {
		t.Fatalf("Analyze() calls within analyze window = %d, want 1", calls)
	}
	// Past TickAnalyze, it should analyze again.
	v.Tick(VisTickContext{Now: t0.Add(TickAnalyze + time.Millisecond), Playing: true, Analyze: analyze})
	if calls != 2 {
		t.Fatalf("Analyze() calls after analyze window = %d, want 2", calls)
	}
}

func TestRenderOnlyDriverSkipsAnalyzeUnderOverlay(t *testing.T) {
	v := NewVisualizer(44100)
	activateMode(t, v, VisBars)

	calls := 0
	v.Tick(VisTickContext{
		OverlayActive: true,
		Analyze: func(VisAnalysisSpec) []float64 {
			calls++
			return uniformBands(0.6)
		},
	})

	if calls != 0 {
		t.Fatalf("Analyze() calls = %d, want 0 while overlay is active", calls)
	}
}

func TestRenderOnlyDriverRequestsConfiguredBandCount(t *testing.T) {
	v := NewVisualizer(44100)
	activateMode(t, v, VisBars)

	var requested VisAnalysisSpec
	v.Tick(VisTickContext{
		Analyze: func(spec VisAnalysisSpec) []float64 {
			requested = spec
			return uniformBandsN(spec.BandCount, 0.6)
		},
	})

	want := spectrumAnalysisSpec(DefaultSpectrumBands)
	if requested != want {
		t.Fatalf("Analyze() requested %+v, want %+v", requested, want)
	}
	if len(v.bands) != DefaultSpectrumBands {
		t.Fatalf("stored bands len = %d, want %d", len(v.bands), DefaultSpectrumBands)
	}
}

func TestClassicPeakRequestsHighResBands(t *testing.T) {
	v := NewVisualizer(44100)
	activateMode(t, v, VisClassicPeak)

	var requested VisAnalysisSpec
	v.Tick(VisTickContext{
		Now:     time.Now(),
		Playing: true,
		Analyze: func(spec VisAnalysisSpec) []float64 {
			requested = spec
			return uniformBandsN(spec.BandCount, 0.6)
		},
	})

	want := VisAnalysisSpec{BandCount: classicPeakSpectrumBands, FFTSize: classicPeakFFTSize}
	if requested != want {
		t.Fatalf("Analyze() requested %+v, want %+v", requested, want)
	}
	if len(v.bands) != classicPeakSpectrumBands {
		t.Fatalf("stored bands len = %d, want %d", len(v.bands), classicPeakSpectrumBands)
	}
}

func TestRawSampleModesRefreshWaveBufAtZeroBandCount(t *testing.T) {
	v := NewVisualizer(44100)
	activateMode(t, v, VisWave)

	samples := []float64{-0.5, -0.1, 0.25, 0.75}
	requested := VisAnalysisSpec{BandCount: -1, FFTSize: -1}
	v.Tick(VisTickContext{
		Playing: true,
		Analyze: func(spec VisAnalysisSpec) []float64 {
			requested = spec
			return v.Analyze(samples, spec)
		},
	})

	want := spectrumAnalysisSpec(0)
	if requested != want {
		t.Fatalf("Analyze() requested %+v, want %+v for raw-sample modes", requested, want)
	}
	if !reflect.DeepEqual(v.waveBuf, samples) {
		t.Fatalf("waveBuf = %v, want %v after zero-band tick refresh", v.waveBuf, samples)
	}
}

func TestRawSampleModesClearSpectrumHistoryOnModeSwitch(t *testing.T) {
	v := NewVisualizer(44100)
	barsSpec := spectrumAnalysisSpec(DefaultSpectrumBands)
	v.prevBySpec[barsSpec] = uniformBandsN(barsSpec.BandCount, 0.8)

	activateMode(t, v, VisBars)
	activateMode(t, v, VisWave)

	if len(v.prevBySpec) != 0 {
		t.Fatalf("prevBySpec len after switch to raw-sample mode = %d, want 0", len(v.prevBySpec))
	}

	activateMode(t, v, VisBars)
	bands := v.Analyze(nil, barsSpec)
	if got := bands[0]; math.Abs(got) > classicPeakTestEpsilon {
		t.Fatalf("first bar after raw-sample switch = %v, want 0 with cleared spectrum history", got)
	}
}

func TestTerrainPreservesStateAcrossModeSwitch(t *testing.T) {
	withPanelWidth(t, 8)

	v := NewVisualizer(44100)
	activateMode(t, v, VisTerrain)
	driver := terrainDriverFor(t, v)
	bands := uniformBands(0.6)
	v.bands = bands

	v.Tick(VisTickContext{})
	snapshot := append([]float64(nil), driver.buf...)
	if len(snapshot) != PanelWidth*2 {
		t.Fatalf("terrain buffer len = %d, want %d", len(snapshot), PanelWidth*2)
	}

	activateMode(t, v, VisBars)
	activateMode(t, v, VisTerrain)

	if len(driver.buf) != len(snapshot) {
		t.Fatalf("terrain buffer len after switch = %d, want %d", len(driver.buf), len(snapshot))
	}
	for i, got := range driver.buf {
		if got != snapshot[i] {
			t.Fatalf("terrain buffer[%d] = %v after switch, want %v", i, got, snapshot[i])
		}
	}
}

func TestTerrainRenderDoesNotAdvanceWithoutTick(t *testing.T) {
	withPanelWidth(t, 8)

	v := NewVisualizer(44100)
	activateMode(t, v, VisTerrain)
	driver := terrainDriverFor(t, v)
	v.bands = uniformBands(0.6)

	v.Tick(VisTickContext{})
	snapshot := append([]float64(nil), driver.buf...)

	v.Render()
	v.Render()

	if !reflect.DeepEqual(driver.buf, snapshot) {
		t.Fatalf("terrain buffer changed across redraws without tick: got %v want %v", driver.buf, snapshot)
	}
}

func TestTerrainTickSkipsAnalyzeUnderOverlay(t *testing.T) {
	withPanelWidth(t, 8)

	v := NewVisualizer(44100)
	activateMode(t, v, VisTerrain)
	driver := terrainDriverFor(t, v)
	driver.buf = append([]float64(nil), []float64{
		0.1, 0.2, 0.3, 0.4,
		0.5, 0.6, 0.7, 0.8,
		0.2, 0.3, 0.4, 0.5,
		0.6, 0.7, 0.8, 0.9,
	}...)
	snapshot := append([]float64(nil), driver.buf...)

	calls := 0
	v.Tick(VisTickContext{
		OverlayActive: true,
		Analyze: func(VisAnalysisSpec) []float64 {
			calls++
			return uniformBands(0.6)
		},
	})

	if calls != 0 {
		t.Fatalf("Analyze() calls = %d, want 0 while overlay is active", calls)
	}
	if !reflect.DeepEqual(driver.buf, snapshot) {
		t.Fatalf("terrain buffer changed under overlay: got %v want %v", driver.buf, snapshot)
	}
}
