package civ

import (
	"bytes"
	"testing"
)

func TestFreqBCDRoundTrip(t *testing.T) {
	cases := []int64{0, 1, 7074000, 14250000, 28074000, 50313000, 144174000, 1296000000}
	for _, hz := range cases {
		b := FreqToBCD(hz)
		if len(b) != 5 {
			t.Fatalf("FreqToBCD(%d) len=%d, want 5", hz, len(b))
		}
		got, ok := BCDToFreq(b)
		if !ok || got != hz {
			t.Errorf("round trip %d → % X → %d (ok=%v)", hz, b, got, ok)
		}
	}
}

func TestFreqBCDKnownEncoding(t *testing.T) {
	// 14.250.000 Hz → little-endian BCD 00 00 25 14 00.
	want := []byte{0x00, 0x00, 0x25, 0x14, 0x00}
	if got := FreqToBCD(14250000); !bytes.Equal(got, want) {
		t.Errorf("FreqToBCD(14250000) = % X, want % X", got, want)
	}
}

func TestFrame(t *testing.T) {
	// Read-frequency request to a 7610 (0x98) from the controller (0xE0).
	got := Frame(0x98, AddrController, CmdReadFreq)
	want := []byte{0xFE, 0xFE, 0x98, 0xE0, 0x03, 0xFD}
	if !bytes.Equal(got, want) {
		t.Errorf("Frame = % X, want % X", got, want)
	}
}

func TestScanSingleFreqResponse(t *testing.T) {
	// Rig (0x98) → controller (0xE0): freq read response for 14.250 MHz.
	in := Frame(AddrController, 0x98, CmdReadFreq, 0x00, 0x00, 0x25, 0x14, 0x00)
	frames, consumed := Scan(in)
	if consumed != len(in) {
		t.Fatalf("consumed=%d, want %d", consumed, len(in))
	}
	if len(frames) != 1 {
		t.Fatalf("got %d frames, want 1", len(frames))
	}
	f := frames[0]
	if f.From != 0x98 || f.To != AddrController || f.Cmd != CmdReadFreq {
		t.Errorf("addrs/cmd wrong: %+v", f)
	}
	if hz, ok := BCDToFreq(f.Data); !ok || hz != 14250000 {
		t.Errorf("decoded freq %d (ok=%v), want 14250000", hz, ok)
	}
}

func TestScanSkipsEchoAndKeepsPartial(t *testing.T) {
	echo := Frame(0x98, AddrController, CmdReadFreq)               // our outgoing (echoed back)
	resp := Frame(AddrController, 0x98, CmdReadMode, ModeCW, 0x01) // a real response
	buf := append(append([]byte{}, echo...), resp...)
	buf = append(buf, 0xFE, 0xFE, 0x98) // a partial third frame (no FD yet)

	frames, consumed := Scan(buf)
	if len(frames) != 2 {
		t.Fatalf("got %d frames, want 2", len(frames))
	}
	// The partial frame must be left unconsumed so the next read can finish it.
	if consumed != len(echo)+len(resp) {
		t.Errorf("consumed=%d, want %d (partial frame retained)", consumed, len(echo)+len(resp))
	}
	if frames[1].Cmd != CmdReadMode || len(frames[1].Data) < 1 || frames[1].Data[0] != ModeCW {
		t.Errorf("second frame wrong: %+v", frames[1])
	}
}

func TestModeToADIF(t *testing.T) {
	cases := []struct {
		m    byte
		data bool
		want string
	}{
		{ModeUSB, false, "SSB"},
		{ModeLSB, false, "SSB"},
		{ModeUSB, true, "DATA"},
		{ModeCW, false, "CW"},
		{ModeCWR, false, "CW"},
		{ModeRTTY, false, "RTTY"},
		{ModeAM, false, "AM"},
		{ModeFM, false, "FM"},
	}
	for _, c := range cases {
		if got := ModeToADIF(c.m, c.data); got != c.want {
			t.Errorf("ModeToADIF(0x%02X, %v) = %q, want %q", c.m, c.data, got, c.want)
		}
	}
}

func TestLevelBCDRoundTrip(t *testing.T) {
	for _, v := range []int{0, 1, 50, 99, 100, 128, 200, 255} {
		b := LevelToBCD(v)
		if len(b) != 2 {
			t.Fatalf("LevelToBCD(%d) len=%d", v, len(b))
		}
		if got := BCDToLevel(b); got != v {
			t.Errorf("level round trip %d → % X → %d", v, b, got)
		}
	}
	// Known encodings from the Icom CI-V reference.
	if got := LevelToBCD(128); !bytes.Equal(got, []byte{0x01, 0x28}) {
		t.Errorf("LevelToBCD(128) = % X, want 01 28", got)
	}
	if got := LevelToBCD(255); !bytes.Equal(got, []byte{0x02, 0x55}) {
		t.Errorf("LevelToBCD(255) = % X, want 02 55", got)
	}
}

func TestByteBCDRoundTrip(t *testing.T) {
	for _, v := range []int{0, 6, 12, 18, 21} {
		if got := BCDToByte(ByteToBCD(v)); got != v {
			t.Errorf("byte BCD round trip %d → %d", v, got)
		}
	}
}

func TestModelName(t *testing.T) {
	if got := ModelName(0x98); got != "IC-7610" {
		t.Errorf("ModelName(0x98) = %q, want IC-7610", got)
	}
	if got := ModelName(0x12); got != "Icom (0x12)" {
		t.Errorf("ModelName(0x12) = %q, want fallback", got)
	}
}

// CI-V addresses are hardware constants: a wrong one means the console shows the
// wrong model, and with it the wrong attenuator steps (6/12/18 dB on the big
// rigs, a single 20 dB on the small ones) — buttons the rig then NAKs.
//
// Two entries here were previously wrong in a way that pointed at each other:
// 0x80 was labelled IC-7800 (it is the IC-7410) and 0x88 IC-7700 (it is the
// IC-7100), while the real IC-7800 (0x6A) and IC-7700 (0x74) were missing — so an
// IC-7800 came up as "Icom (0x6A)" with a 20 dB attenuator it does not have.
func TestModelNameAddresses(t *testing.T) {
	for addr, want := range map[byte]string{
		0x6A: "IC-7800",
		0x74: "IC-7700",
		0x7A: "IC-7600",
		0x7C: "IC-9100",
		0x80: "IC-7410",
		0x88: "IC-7100",
		0x8E: "IC-7851",
		0x94: "IC-7300",
		0x98: "IC-7610",
		0xA2: "IC-9700",
		0xA4: "IC-705",
		0xB6: "IC-7300MKII",
	} {
		if got := ModelName(addr); got != want {
			t.Errorf("ModelName(0x%02X) = %q, want %q", addr, got, want)
		}
	}
	// An unknown address must stay identifiable rather than masquerade as a model.
	if got := ModelName(0x42); got != "Icom (0x42)" {
		t.Errorf("ModelName(0x42) = %q, want the hex fallback", got)
	}
}

// A frame that is not decimal BCD is refused rather than decoded.
//
// From an operator's log (F4JND, 2026-07-30): a rig sitting on 145.550 MHz FM
// produced status lines reading 16445550000, 8364550000, 5817408364 and 12640 Hz
// between good readings. CI-V had lost frame sync — a reply read at the wrong
// offset, alongside PTT timeouts and a scope stream the rig was rejecting — and
// the decoder treated the nibbles 0x0A..0x0F as the digits 10..15, turning noise
// into a plausible-looking number.
//
// Garbage that looks like a frequency is worse than a refused frame: it reaches
// the status bar, the band slots and the log, and it is what an operator then
// has to disprove.
func TestBCDToFreqRejectsNonDecimal(t *testing.T) {
	// Valid: 145.550 MHz, encoded by the same package rather than by hand — my
	// hand-written fixture was byte-reversed and this caught it.
	good := FreqToBCD(145550000)
	if hz, ok := BCDToFreq(good); !ok || hz != 145550000 {
		t.Errorf("valid BCD % X decoded as %d (ok=%v), want 145550000", good, hz, ok)
	}

	// Every nibble value above 9 must be refused, in either half of the byte.
	bad := [][]byte{
		{0x0A, 0x00, 0x55, 0x45, 0x01}, // low nibble
		{0x00, 0x00, 0xF5, 0x45, 0x01}, // high nibble
		{0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, // an idle line read as data
		{0x00, 0x00, 0x00, 0x0B, 0x00},
	}
	for _, b := range bad {
		if hz, ok := BCDToFreq(b); ok {
			t.Errorf("% X was accepted as %d Hz — it is not decimal BCD", b, hz)
		}
	}

	// A short frame still decodes what it holds: the caller decides whether a
	// partial read is usable, and refusing it here would break the 4-byte forms.
	if _, ok := BCDToFreq([]byte{0x00, 0x50}); !ok {
		t.Error("a short but valid BCD frame must still decode")
	}
}

// A band stacking register reply, byte for byte as the rigs send it:
// 1A 01 <band> <reg> <freq 5 LE-BCD> <mode> <filter> <data>. The payload here
// is everything after the command byte, which is what Decoded.Data holds.
func TestDecodeBandStack(t *testing.T) {
	// 14.074.000 MHz, USB, FIL1, data mode on — the FT8 stack on 20 m.
	frame := []byte{0x01, 0x05, 0x03, 0x00, 0x40, 0x07, 0x14, 0x00, ModeUSB, 0x01, 0x01}
	bs, ok := DecodeBandStack(frame, 0x05, 0x03)
	if !ok {
		t.Fatal("a well-formed register was rejected")
	}
	if bs.FreqHz != 14_074_000 {
		t.Errorf("freq = %d, want 14074000", bs.FreqHz)
	}
	if bs.Mode != ModeUSB || !bs.Data {
		t.Errorf("mode = 0x%02X data = %v, want USB + data", bs.Mode, bs.Data)
	}
	// The register ASKED FOR is part of the answer: a reply about another one is
	// a desynchronised read, not a frequency to send the radio to.
	if _, ok := DecodeBandStack(frame, 0x05, 0x01); ok {
		t.Error("a reply for register 3 was accepted as register 1")
	}
	if _, ok := DecodeBandStack(frame, 0x03, 0x03); ok {
		t.Error("a reply about 40 m was accepted as 20 m")
	}
	// A register without the data-mode byte is a shorter frame, not a bad one.
	if bs, ok := DecodeBandStack(frame[:10], 0x05, 0x03); !ok || bs.FreqHz != 14_074_000 || bs.Data {
		t.Errorf("short register = %+v ok=%v, want the frequency with no data flag", bs, ok)
	}
	if _, ok := DecodeBandStack([]byte{0x01, 0x05, 0x03}, 0x05, 0x03); ok {
		t.Error("a truncated frame was accepted")
	}
}
