package cwdecode

import (
	"math"
	"math/rand"
	"strings"
	"testing"
)

// ---- Synthesizer -----------------------------------------------------------

func charToMorse() map[byte]string {
	m := map[byte]string{}
	for code, ch := range morse {
		m[ch] = code
	}
	return m
}

// keyMessage synthesizes keyed CW for msg with raised-cosine edges (5 ms), so
// the signal has realistic click-free envelopes rather than hard steps.
func keyMessage(msg string, fs, wpm int, pitch, amp float64) []int16 {
	dot := fs * 1200 / (wpm * 1000) // samples per dit
	edge := fs * 5 / 1000           // 5 ms shaping
	c2m := charToMorse()
	var out []float64
	phase := 0.0
	dphi := 2 * math.Pi * pitch / float64(fs)

	tone := func(n int) {
		for i := 0; i < n; i++ {
			g := 1.0
			if i < edge {
				g = 0.5 - 0.5*math.Cos(math.Pi*float64(i)/float64(edge))
			} else if n-1-i < edge {
				g = 0.5 - 0.5*math.Cos(math.Pi*float64(n-1-i)/float64(edge))
			}
			out = append(out, amp*g*math.Sin(phase))
			phase += dphi
		}
	}
	silence := func(n int) {
		for i := 0; i < n; i++ {
			out = append(out, 0)
		}
	}

	silence(fs / 4) // lead-in for envelope warm-up
	for i := 0; i < len(msg); i++ {
		ch := msg[i]
		if ch == ' ' {
			silence(4 * dot) // + trailing 3 from the previous char = 7 total
			continue
		}
		code := c2m[ch]
		for j := 0; j < len(code); j++ {
			if code[j] == '.' {
				tone(dot)
			} else {
				tone(3 * dot)
			}
			silence(dot)
		}
		silence(2 * dot) // + trailing element gap = 3 total
	}
	silence(fs / 2)
	return toInt16(out)
}

func toInt16(x []float64) []int16 {
	out := make([]int16, len(x))
	for i, v := range x {
		if v > 32767 {
			v = 32767
		} else if v < -32768 {
			v = -32768
		}
		out[i] = int16(v)
	}
	return out
}

func addNoise(s []int16, sigma float64, seed int64) []int16 {
	r := rand.New(rand.NewSource(seed))
	out := make([]int16, len(s))
	for i, v := range s {
		out[i] = int16(math.Max(-32768, math.Min(32767, float64(v)+r.NormFloat64()*sigma)))
	}
	return out
}

// applyQSB modulates the amplitude between lo..1.0 at rate Hz (slow fading).
func applyQSB(s []int16, fs int, rate, lo float64) []int16 {
	out := make([]int16, len(s))
	for i, v := range s {
		g := lo + (1-lo)*(0.5+0.5*math.Sin(2*math.Pi*rate*float64(i)/float64(fs)))
		out[i] = int16(float64(v) * g)
	}
	return out
}

// applyDropouts blanks brief windows (ms long) every period ms — static-crash
// style holes that land inside dahs and gaps alike.
func applyDropouts(s []int16, fs int, everyMs, holeMs int) []int16 {
	out := make([]int16, len(s))
	copy(out, s)
	every := fs * everyMs / 1000
	hole := fs * holeMs / 1000
	for start := every; start+hole < len(out); start += every {
		for i := start; i < start+hole; i++ {
			out[i] = 0
		}
	}
	return out
}

func mix(a, b []int16) []int16 {
	n := len(a)
	if len(b) > n {
		n = len(b)
	}
	out := make([]int16, n)
	for i := 0; i < n; i++ {
		var v int
		if i < len(a) {
			v += int(a[i])
		}
		if i < len(b) {
			v += int(b[i])
		}
		if v > 32767 {
			v = 32767
		} else if v < -32768 {
			v = -32768
		}
		out[i] = int16(v)
	}
	return out
}

// decode runs samples through a fresh decoder in live-sized chunks.
func decode(t *testing.T, samples []int16, targetHz int) string {
	t.Helper()
	var sb strings.Builder
	d := New(16000, func(s string) { sb.WriteString(s) }, nil)
	if targetHz > 0 {
		d.SetTarget(targetHz)
	}
	for i := 0; i < len(samples); i += 256 {
		end := i + 256
		if end > len(samples) {
			end = len(samples)
		}
		d.Process(samples[i:end])
	}
	return strings.ToUpper(sb.String())
}

func wantContains(t *testing.T, got, want, label string) {
	t.Helper()
	if !strings.Contains(got, want) {
		t.Fatalf("%s: decoded %q, want it to contain %q", label, got, want)
	}
}

// ---- Tests -----------------------------------------------------------------

func TestCleanSignalSpeeds(t *testing.T) {
	const fs = 16000
	for _, wpm := range []int{12, 18, 25, 32, 40} {
		got := decode(t, keyMessage("CQ TEST DE F4BPO K", fs, wpm, 700, 9000), 0)
		wantContains(t, got, "CQ TEST DE F4BPO K", "clean @"+itoa(wpm)+"wpm")
	}
}

func TestOtherPitches(t *testing.T) {
	const fs = 16000
	for _, pitch := range []float64{450, 600, 850} {
		got := decode(t, keyMessage("PARIS PARIS", fs, 22, pitch, 9000), 0)
		wantContains(t, got, "PARIS PARIS", "pitch")
	}
}

func TestWithNoise(t *testing.T) {
	const fs = 16000
	clean := keyMessage("CQ CQ DE HB9HBY", fs, 22, 700, 9000)
	noisy := addNoise(clean, 2000, 1) // ≈13 dB tone/noise in the audio band
	got := decode(t, noisy, 0)
	wantContains(t, got, "CQ CQ DE HB9HBY", "noise")
}

// QSB fading between 35% and 100% amplitude — the adaptive dB envelope must
// ride it. The old linear envelope lost the faded halves entirely.
func TestQSBFading(t *testing.T) {
	const fs = 16000
	clean := keyMessage("CQ CQ CQ DE F4BPO F4BPO", fs, 20, 700, 12000)
	faded := applyQSB(clean, fs, 0.4, 0.35)
	got := decode(t, faded, 0)
	wantContains(t, got, "DE F4BPO", "qsb")
}

// Brief 10 ms holes punched every 150 ms — they land inside dahs. Without the
// two-sided debounce every hit dah shatters into dits (the old decoder's
// single worst failure on real signals).
func TestDropoutsInsideDahs(t *testing.T) {
	const fs = 16000
	clean := keyMessage("TEST TEST TEST", fs, 18, 700, 9000)
	holed := applyDropouts(clean, fs, 150, 10)
	got := decode(t, holed, 0)
	wantContains(t, got, "TEST TEST", "dropouts")
}

// QRM: a second, slightly weaker keyed signal at 950 Hz. The pitch lock must
// hold the 700 Hz target and ignore the interferer.
func TestQRMAutoLock(t *testing.T) {
	const fs = 16000
	target := keyMessage("PARIS PARIS PARIS", fs, 20, 700, 9000)
	qrm := keyMessage("QRZ QRZ QRZ QRZ QRZ", fs, 26, 950, 5000)
	got := decode(t, mix(target, qrm), 0)
	wantContains(t, got, "PARIS", "qrm-auto")
}

// Targeted mode: with two comparable signals, SetTarget must decode the chosen
// one even though the other is as strong.
func TestQRMTargeted(t *testing.T) {
	const fs = 16000
	want := keyMessage("SOS SOS SOS", fs, 20, 600, 8000)
	other := keyMessage("QRL QRL QRL QRL", fs, 24, 900, 8000)
	got := decode(t, mix(want, other), 600)
	wantContains(t, got, "SOS SOS", "qrm-target")
}

// Pure noise must stay silent: the squelch keys nothing, so no text at all.
func TestNoiseOnlySquelch(t *testing.T) {
	const fs = 16000
	noise := addNoise(make([]int16, fs*6), 3000, 7)
	got := strings.TrimSpace(decode(t, noise, 0))
	if len(got) > 2 { // tolerate at most a stray flagged char
		t.Fatalf("squelch: decoded %q from pure noise, want (almost) nothing", got)
	}
}

// keyMessageJitter synthesizes hand-sent CW: every element and gap duration
// is jittered (elements ±je, gaps ±jg, uniform), like a human fist.
func keyMessageJitter(msg string, fs, wpm int, pitch, amp, je, jg float64, seed int64) []int16 {
	r := rand.New(rand.NewSource(seed))
	dot := float64(fs) * 1200 / (float64(wpm) * 1000)
	edge := fs * 5 / 1000
	c2m := charToMorse()
	var out []float64
	phase := 0.0
	dphi := 2 * math.Pi * pitch / float64(fs)
	jit := func(n float64, j float64) int { return int(n * (1 + (r.Float64()*2-1)*j)) }
	tone := func(n int) {
		for i := 0; i < n; i++ {
			g := 1.0
			if i < edge {
				g = 0.5 - 0.5*math.Cos(math.Pi*float64(i)/float64(edge))
			} else if n-1-i < edge {
				g = 0.5 - 0.5*math.Cos(math.Pi*float64(n-1-i)/float64(edge))
			}
			out = append(out, amp*g*math.Sin(phase))
			phase += dphi
		}
	}
	silence := func(n int) {
		for i := 0; i < n; i++ {
			out = append(out, 0)
		}
	}
	silence(fs / 4)
	for i := 0; i < len(msg); i++ {
		ch := msg[i]
		if ch == ' ' {
			silence(jit(4*dot, jg))
			continue
		}
		code := c2m[ch]
		for j := 0; j < len(code); j++ {
			if code[j] == '.' {
				tone(jit(dot, je))
			} else {
				tone(jit(3*dot, je))
			}
			silence(jit(dot, jg))
		}
		silence(jit(2*dot, jg))
	}
	silence(fs / 2)
	return toInt16(out)
}

// Hand keying: ±20% element jitter, ±25% gap jitter — a sloppy but readable
// human fist. The batch classifier and the gap-fed dit tracking must ride it.
func TestHandKeying(t *testing.T) {
	const fs = 16000
	for seed := int64(1); seed <= 3; seed++ {
		s := keyMessageJitter("CQ CQ DE HB9HBY HB9HBY K", fs, 22, 700, 9000, 0.20, 0.25, seed)
		got := decode(t, s, 0)
		wantContains(t, got, "HB9HBY", "hand-keying")
	}
}

// Speed change mid-over: the cluster tracker must follow 25 → 15 WPM.
func TestSpeedChange(t *testing.T) {
	const fs = 16000
	fast := keyMessage("CQ CQ CQ DE F4BPO", fs, 25, 700, 9000)
	slow := keyMessage("UR RST 599 599", fs, 15, 700, 9000)
	got := decode(t, append(fast, slow...), 0)
	wantContains(t, got, "F4BPO", "speed-fast-part")
	wantContains(t, got, "599", "speed-slow-part")
}

func itoa(n int) string {
	if n == 0 {
		return "0"
	}
	var b [8]byte
	i := len(b)
	for n > 0 {
		i--
		b[i] = byte('0' + n%10)
		n /= 10
	}
	return string(b[i:])
}
