package offline

import (
	"math"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	cfgpkg "github.com/jan/fm-rds-tx/internal/config"
	"github.com/jan/fm-rds-tx/internal/license"
)

func TestGenerateFrame(t *testing.T) {
	g := NewGenerator(cfgpkg.Default())
	frame := g.GenerateFrame(50 * time.Millisecond)
	if frame == nil || len(frame.Samples) == 0 {
		t.Fatal("expected samples")
	}
	m := g.LatestMeasurement()
	if m == nil {
		t.Fatal("expected latest measurement")
	}
	if m.ChunkSamples == 0 || m.SampleRateHz <= 0 {
		t.Fatal("expected measurement metadata")
	}
	if m.Flags.StereoMode == "" {
		t.Fatal("expected stereo mode flag")
	}
}

func TestGenerateFrameFMIQ(t *testing.T) {
	cfg := cfgpkg.Default()
	cfg.FM.FMModulationEnabled = true
	frame := NewGenerator(cfg).GenerateFrame(10 * time.Millisecond)
	for i := 100; i < len(frame.Samples) && i < 200; i++ {
		s := frame.Samples[i]
		mag := math.Sqrt(float64(s.I)*float64(s.I) + float64(s.Q)*float64(s.Q))
		if math.Abs(mag-1.0) > 0.01 {
			t.Fatalf("sample %d: mag=%.4f", i, mag)
		}
	}
}

func TestGenerateFrameCompositeOnly(t *testing.T) {
	cfg := cfgpkg.Default()
	cfg.FM.FMModulationEnabled = false
	frame := NewGenerator(cfg).GenerateFrame(10 * time.Millisecond)
	for i := 0; i < len(frame.Samples) && i < 100; i++ {
		if frame.Samples[i].Q != 0 {
			t.Fatalf("sample %d: Q=%.6f", i, frame.Samples[i].Q)
		}
	}
}

func TestStereoDisabled(t *testing.T) {
	cfgS := cfgpkg.Default()
	cfgS.FM.FMModulationEnabled = false
	cfgS.FM.StereoEnabled = true
	cfgM := cfgS
	cfgM.FM.StereoEnabled = false
	sf := NewGenerator(cfgS).GenerateFrame(20 * time.Millisecond)
	mf := NewGenerator(cfgM).GenerateFrame(20 * time.Millisecond)
	var diffEnergy float64
	for i := range sf.Samples {
		d := float64(sf.Samples[i].I - mf.Samples[i].I)
		diffEnergy += d * d
	}
	if diffEnergy == 0 {
		t.Fatal("expected difference")
	}
}

func TestWriteFile(t *testing.T) {
	cfg := cfgpkg.Default()
	out := filepath.Join(t.TempDir(), "test.iqf32")
	if err := NewGenerator(cfg).WriteFile(out, 20*time.Millisecond); err != nil {
		t.Fatal(err)
	}
	info, _ := os.Stat(out)
	if info.Size() == 0 {
		t.Fatal("empty file")
	}
}

func TestSummaryTones(t *testing.T) {
	cfg := cfgpkg.Default()
	cfg.Audio.InputPath = ""
	s := NewGenerator(cfg).Summary(10 * time.Millisecond)
	if !strings.Contains(s, "source=tones") {
		t.Fatalf("unexpected: %s", s)
	}
}

func TestSummaryToneFallback(t *testing.T) {
	cfg := cfgpkg.Default()
	cfg.Audio.InputPath = "missing.wav"
	s := NewGenerator(cfg).Summary(10 * time.Millisecond)
	if !strings.Contains(s, "source=tone-fallback") {
		t.Fatalf("unexpected: %s", s)
	}
}

func TestSummaryPreemph(t *testing.T) {
	cfg := cfgpkg.Default()
	cfg.FM.PreEmphasisTauUS = 50
	if !strings.Contains(NewGenerator(cfg).Summary(10*time.Millisecond), "preemph=50µs") {
		t.Fatal("missing preemph")
	}
}

func TestSummaryFMIQ(t *testing.T) {
	cfg := cfgpkg.Default()
	cfg.FM.FMModulationEnabled = true
	if !strings.Contains(NewGenerator(cfg).Summary(10*time.Millisecond), "FM-IQ") {
		t.Fatal("missing FM-IQ")
	}
}

func TestLimiterPreventsClipping(t *testing.T) {
	cfg := cfgpkg.Default()
	cfg.FM.LimiterEnabled = true
	cfg.FM.LimiterCeiling = 1.0
	cfg.FM.FMModulationEnabled = false
	cfg.Audio.ToneAmplitude = 0.9
	cfg.Audio.Gain = 2.0
	cfg.FM.OutputDrive = 1.0
	frame := NewGenerator(cfg).GenerateFrame(50 * time.Millisecond)
	// Audio clipped to ceiling, pilot+RDS added on top (standard broadcast).
	// Total = ceiling + pilotLevel*drive + rdsInjection*drive
	maxAllowed := cfg.FM.LimiterCeiling +
		cfg.FM.PilotLevel*cfg.FM.OutputDrive +
		cfg.FM.RDSInjection*cfg.FM.OutputDrive + 0.02
	for i, s := range frame.Samples {
		if math.Abs(float64(s.I)) > maxAllowed {
			t.Fatalf("sample %d: %.4f exceeds max %.4f", i, s.I, maxAllowed)
		}
	}
}

// --- Operator truth tests ---

func TestRDSDisabledSuppressesRDSEnergy(t *testing.T) {
	cfgOn := cfgpkg.Default()
	cfgOn.FM.FMModulationEnabled = false
	cfgOn.RDS.Enabled = true
	cfgOff := cfgOn
	cfgOff.RDS.Enabled = false
	fOn := NewGenerator(cfgOn).GenerateFrame(20 * time.Millisecond)
	fOff := NewGenerator(cfgOff).GenerateFrame(20 * time.Millisecond)
	var diff float64
	for i := range fOn.Samples {
		d := float64(fOn.Samples[i].I - fOff.Samples[i].I)
		diff += d * d
	}
	if diff == 0 {
		t.Fatal("rds.enabled=false should produce different output")
	}
}

func TestFMModDisabledMeansComposite(t *testing.T) {
	cfg := cfgpkg.Default()
	cfg.FM.FMModulationEnabled = false
	frame := NewGenerator(cfg).GenerateFrame(10 * time.Millisecond)
	for i := 0; i < 100; i++ {
		if frame.Samples[i].Q != 0 {
			t.Fatal("Q should be 0 when FM mod is off")
		}
	}
}

func TestLimiterEnabledChangesWaveform(t *testing.T) {
	base := cfgpkg.Default()
	base.FM.FMModulationEnabled = false
	base.Audio.ToneAmplitude = 0.95
	base.Audio.Gain = 3.0
	base.FM.OutputDrive = 2.5
	base.FM.LimiterCeiling = 0.8

	cfgOn := base
	cfgOn.FM.LimiterEnabled = true
	cfgOff := base
	cfgOff.FM.LimiterEnabled = false

	fOn := NewGenerator(cfgOn).GenerateFrame(50 * time.Millisecond)
	fOff := NewGenerator(cfgOff).GenerateFrame(50 * time.Millisecond)

	if len(fOn.Samples) != len(fOff.Samples) {
		t.Fatalf("sample length mismatch: %d vs %d", len(fOn.Samples), len(fOff.Samples))
	}

	var diffEnergy float64
	for i := range fOn.Samples {
		d := float64(fOn.Samples[i].I - fOff.Samples[i].I)
		diffEnergy += d * d
	}
	if diffEnergy == 0 {
		t.Fatal("expected limiterEnabled to change waveform")
	}
}

func TestSetLicenseDoesNotImplicitlyEnableWatermark(t *testing.T) {
	cfg := cfgpkg.Default()
	g := NewGenerator(cfg)
	g.SetLicense(license.NewState(""))
	g.init()
	if g.stftEmbedder != nil {
		t.Fatal("watermark should remain disabled unless explicitly configured")
	}
}

func TestConfigureWatermarkExplicitOptIn(t *testing.T) {
	cfg := cfgpkg.Default()
	cfg.FM.WatermarkEnabled = true
	g := NewGenerator(cfg)
	g.SetLicense(license.NewState("test-key"))
	g.ConfigureWatermark(true, "test-key")
	g.init()
	if g.stftEmbedder == nil {
		t.Fatal("expected watermark embedder after explicit opt-in")
	}
	_ = g.GenerateFrame(10 * time.Millisecond)
	m := g.LatestMeasurement()
	if m == nil || !m.Flags.WatermarkEnabled {
		t.Fatal("expected watermark flag in measurement")
	}
}

func TestGeneratorResetRestoresDeterministicFirstFrame(t *testing.T) {
	cfg := cfgpkg.Default()
	cfg.RDS.Enabled = false
	cfg.FM.FMModulationEnabled = true
	g := NewGenerator(cfg)

	first := g.GenerateFrame(10 * time.Millisecond)
	_ = g.GenerateFrame(10 * time.Millisecond)
	g.Reset()
	afterReset := g.GenerateFrame(10 * time.Millisecond)

	if len(first.Samples) != len(afterReset.Samples) {
		t.Fatalf("sample length mismatch: %d vs %d", len(first.Samples), len(afterReset.Samples))
	}
	for i := range first.Samples {
		a := first.Samples[i]
		b := afterReset.Samples[i]
		if a != b {
			t.Fatalf("sample %d differs after reset: first=(%v,%v) reset=(%v,%v)", i, a.I, a.Q, b.I, b.Q)
		}
	}
}
