package cat

import (
	"testing"
	"time"
)

// What SPLIT means when the operator presses it.
//
// Flipping the rig's split flag alone transmits wherever the OTHER VFO happens
// to sit — reported from a real FTDX10: listening on 14.244 with VFO B left on
// 18.115 from an earlier session, pressing SPLIT threw the transmitter onto
// another band. The other VFO is stale by nature, so the transmit frequency has
// to be derived from where the operator is listening now.
func TestYaesuDefaultSplitOffset(t *testing.T) {
	cases := []struct {
		raw  string
		want int64
	}{
		// CW and the data modes work 1 kHz up.
		{"CW-U", 1000},
		{"CW-L", 1000},
		{"RTTY-U", 1000},
		{"RTTY-L", 1000},
		{"DATA-U", 1000},
		{"DATA-L", 1000},
		// Phone works 5 kHz up.
		{"USB", 5000},
		{"LSB", 5000},
		{"AM", 5000},
		{"FM", 5000},
		// Unknown or not yet read: the phone offset is the safer default — too
		// wide is audible and obvious, too narrow lands on top of the DX.
		{"", 5000},
	}
	for _, c := range cases {
		y := &Yaesu{}
		y.panel.RawMode = c.raw
		if got := y.defaultSplitOffset(); got != c.want {
			t.Errorf("mode %q → split offset %d Hz, want %d", c.raw, got, c.want)
		}
	}
}

// The SWR scale, pinned to the two measurements it was derived from.
//
// Taken on an FTDX10 (2026-07-29) against an operator watching the rig's own
// meter: raw 0 at SWR 1.1, raw 52 at SWR 1.5. The second point is what proved
// the raw value is the reflection coefficient scaled to 255 — 52/255 = 0.204,
// rho for a 1.5 SWR — rather than a percentage of meter travel, which is how the
// bar came to read 81 on a perfect antenna.
func TestSWRFromReflection(t *testing.T) {
	cases := []struct {
		raw  int
		want float64
		tol  float64
	}{
		{0, 1.0, 0.01},   // no reflected power
		{52, 1.5, 0.02},  // the measured mismatch
		{85, 2.0, 0.05},  // rho = 1/3
		{128, 3.0, 0.1},  // rho = 0.5
		{-5, 1.0, 0.01},  // nonsense reading — never below 1.0, which is physical
		{255, 9.9, 0.01}, // full scale is capped rather than infinite
	}
	for _, c := range cases {
		got := swrFromReflection(c.raw)
		if got < c.want-c.tol || got > c.want+c.tol {
			t.Errorf("swrFromReflection(%d) = %.2f, want %.2f ±%.2f", c.raw, got, c.want, c.tol)
		}
	}
	// It must rise with the reflected power, or a worsening match would read
	// better on the panel than on the rig.
	prev := 0.0
	for raw := 0; raw <= 200; raw += 20 {
		v := swrFromReflection(raw)
		if v < prev {
			t.Fatalf("SWR fell from %.2f to %.2f at raw=%d", prev, v, raw)
		}
		prev = v
	}
}

// Needle inertia on the TX meters.
//
// The gaps are what this is for: milliseconds between CW elements, but most of a
// second between the words of a CQ — in CW as in SSB. A meter that decays
// immediately reads 0 in every one of those gaps, so the operator sees a bar
// flashing instead of the power they are running.
func TestMeterPeakHold(t *testing.T) {
	var m meterPeak
	t0 := time.Now()

	if got := m.update(80, t0); got != 80 {
		t.Fatalf("first sample = %d, want 80 — a meter must show a reading at once", got)
	}
	// A gap between two words: still inside the hold, so the reading stands.
	if got := m.update(0, t0.Add(400*time.Millisecond)); got != 80 {
		t.Errorf("during a word gap = %d, want 80 held", got)
	}
	if got := m.update(0, t0.Add(1400*time.Millisecond)); got != 80 {
		t.Errorf("just before the hold expires = %d, want 80 held", got)
	}
	// Past the hold it falls — but gradually, not to zero in one step.
	after := m.update(0, t0.Add(1600*time.Millisecond))
	if after >= 80 || after <= 0 {
		t.Errorf("after the hold = %d, want a value falling between 80 and 0", after)
	}
	// A HIGHER reading is taken immediately: a needle rises fast and falls slow.
	if got := m.update(95, t0.Add(1700*time.Millisecond)); got != 95 {
		t.Errorf("rising sample = %d, want 95 straight away", got)
	}
	// And it does reach the real value eventually, or a power drop would never show.
	v := 0
	for i := 0; i < 60; i++ {
		v = m.update(10, t0.Add(time.Duration(2000+i*250)*time.Millisecond))
	}
	if v != 10 {
		t.Errorf("settled at %d, want 10 — the meter must converge on the truth", v)
	}
}

// The power curve, pinned to the readings taken against the rig's own display.
//
// It is NOT linear, and one calibration point could not show that: scaling
// 207 = 100 W straight down read 30 W where the radio showed 10, and 75 where it
// showed 50. These are the three measured pairs, so a change to the curve that
// breaks them is a regression against the radio, not against a preference.
func TestYaesuPowerCurve(t *testing.T) {
	cases := []struct {
		raw   int
		watts float64
		tol   float64
	}{
		{0, 0, 0.1},
		{62, 10, 0.5},   // measured
		{155, 50, 0.5},  // measured
		{207, 100, 0.5}, // measured
		// Between the measured points it interpolates, so it must land inside the
		// bracket rather than shooting past it.
		{100, 30, 10},
		{180, 75, 10},
	}
	for _, c := range cases {
		got := yaesuWatts(c.raw)
		if got < c.watts-c.tol || got > c.watts+c.tol {
			t.Errorf("yaesuWatts(%d) = %.1f W, want %.1f ±%.1f", c.raw, got, c.watts, c.tol)
		}
	}
	// Monotonic: more meter must never mean less power.
	prev := -1.0
	for raw := 0; raw <= 255; raw++ {
		v := yaesuWatts(raw)
		if v < prev {
			t.Fatalf("power fell from %.1f to %.1f at raw=%d", prev, v, raw)
		}
		prev = v
	}
	// Above the top of the curve it keeps rising rather than flattening at 100 W —
	// a rig driving an amplifier can read past its own full scale.
	if v := yaesuWatts(230); v <= 100 {
		t.Errorf("yaesuWatts(230) = %.1f, want more than 100 — the curve should extend", v)
	}
}
