package cat

import (
	"fmt"
	"strings"
	"sync"
	"testing"
	"time"

	"go.bug.st/serial"
)

// The Kenwood backend driven against a rig that answers, with no hardware.
//
// Nobody here owns a Kenwood, and a backend that has never completed a single
// exchange is a guess however carefully it was written. But this repository
// already contains the other half of the conversation: internal/catemu ANSWERS
// this dialect, pretending to be a TS-2000 so an ACOM amplifier follows OpsLog.
// The responder below replies exactly as catemu does — same FA/FB widths, same
// 38-character IF layout, same ID019 — so the two halves are checked against
// each other rather than against my reading of a manual.
//
// What this proves: the round trip completes, the model is identified, and
// frequency, mode, VFO and split come back correct, including the second read
// that split requires. What it cannot prove: that a real TS-590 answers on the
// same timings, or that its firmware fills every IF field the way the
// documentation says. Those still need a radio.

// fakeSerial is one end of an in-memory serial link. Only Read and Write carry
// meaning; the rest satisfy the interface.
type fakeSerial struct {
	mu      sync.Mutex
	toRig   *strings.Builder // what the backend has written
	fromRig []byte           // what the rig has queued for the backend
	answer  func(cmd string) string
	closed  bool
}

func (f *fakeSerial) Write(p []byte) (int, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	if f.closed {
		return 0, fmt.Errorf("closed")
	}
	f.toRig.Write(p)
	// A rig answers as each terminated command arrives.
	for {
		s := f.toRig.String()
		i := strings.IndexByte(s, ';')
		if i < 0 {
			break
		}
		cmd := s[:i+1]
		rest := s[i+1:]
		f.toRig.Reset()
		f.toRig.WriteString(rest)
		if r := f.answer(cmd); r != "" {
			f.fromRig = append(f.fromRig, r...)
		}
	}
	return len(p), nil
}

func (f *fakeSerial) Read(p []byte) (int, error) {
	// A real port blocks until data or timeout; the backend polls, so returning
	// (0, nil) on an empty buffer models a read timeout with no bytes.
	for i := 0; i < 50; i++ {
		f.mu.Lock()
		if f.closed {
			f.mu.Unlock()
			return 0, fmt.Errorf("closed")
		}
		if len(f.fromRig) > 0 {
			n := copy(p, f.fromRig)
			f.fromRig = f.fromRig[n:]
			f.mu.Unlock()
			return n, nil
		}
		f.mu.Unlock()
		time.Sleep(time.Millisecond)
	}
	return 0, nil
}

func (f *fakeSerial) Close() error {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.closed = true
	return nil
}

func (f *fakeSerial) SetMode(*serial.Mode) error { return nil }
func (f *fakeSerial) Drain() error               { return nil }
func (f *fakeSerial) ResetInputBuffer() error    { return nil }
func (f *fakeSerial) ResetOutputBuffer() error   { return nil }
func (f *fakeSerial) SetDTR(bool) error          { return nil }
func (f *fakeSerial) SetRTS(bool) error          { return nil }
func (f *fakeSerial) GetModemStatusBits() (*serial.ModemStatusBits, error) {
	return &serial.ModemStatusBits{}, nil
}
func (f *fakeSerial) SetReadTimeout(time.Duration) error { return nil }
func (f *fakeSerial) Break(time.Duration) error          { return nil }

// ts2000 answers as internal/catemu does. vfoA/vfoB are the two dials; mode is
// the Kenwood digit; split selects which VFO transmits.
type ts2000 struct {
	vfoA, vfoB int64
	mode       byte
	onB        bool
	split      bool
	// tx models the rig KEYED. On a real Kenwood in split, IF then reports the
	// TRANSMIT VFO as the one in use — which is the whole point of the test below.
	tx bool
	// lazyIF models a rig that answers FR/FT correctly but never fills IF's
	// split bit — the behaviour reported on a Flex through its Kenwood CAT
	// emulation, where the frequency reads perfectly and split never appears.
	lazyIF bool
	// noVFOCmds models a rig that rejects FR/FT outright ("?;").
	noVFOCmds bool
	seen      []string
}

func (r *ts2000) answer(cmd string) string {
	r.seen = append(r.seen, cmd)
	cur := r.vfoA
	vfoDigit := byte('0')
	if r.onB {
		cur, vfoDigit = r.vfoB, '1'
	}
	switch {
	case cmd == "ID;":
		return "ID019;" // TS-2000, exactly what catemu reports
	case cmd == "FA;":
		return fmt.Sprintf("FA%011d;", r.vfoA)
	case cmd == "FB;":
		return fmt.Sprintf("FB%011d;", r.vfoB)
	case cmd == "IF;":
		split := byte('0')
		if r.split && !r.lazyIF {
			split = '1'
		}
		// The catemu layout: IF | freq(11) | step(4) | RIT(±5) | 3 | mem(2) |
		// rx/tx | mode | VFO | scan | split | tone | tone#(2) | shift | ;
		txByte := 0
		if r.tx {
			txByte = 1
		}
		return fmt.Sprintf("IF%011d%04d%+06d%03d%02d%01d%c%c%01d%c%01d%02d%01d;",
			cur, 0, 0, 0, 0, txByte, r.mode, vfoDigit, 0, split, 0, 0, 0)
	case strings.HasPrefix(cmd, "FA") && len(cmd) > 3:
		fmt.Sscanf(cmd, "FA%d;", &r.vfoA)
		return ""
	case strings.HasPrefix(cmd, "FB") && len(cmd) > 3:
		fmt.Sscanf(cmd, "FB%d;", &r.vfoB)
		return ""
	case strings.HasPrefix(cmd, "MD") && len(cmd) == 4:
		r.mode = cmd[2]
		return ""
	case cmd == "FR;" || cmd == "FT;":
		if r.noVFOCmds {
			return "?;" // rejected, as a rig that does not know the command answers
		}
		// FR is the receive VFO, FT the transmit one. They differ exactly when
		// the rig is in split.
		v := vfoDigit
		if cmd == "FT;" && r.split {
			if vfoDigit == '0' {
				v = '1'
			} else {
				v = '0'
			}
		}
		return fmt.Sprintf("%s%c;", strings.TrimSuffix(cmd, ";"), v)
	}
	return "" // AI0;, TX;, RX; — set commands, no reply, as on a real rig
}

func dialTo(rig *ts2000) func() (serial.Port, error) {
	return func() (serial.Port, error) {
		return &fakeSerial{toRig: &strings.Builder{}, answer: rig.answer}, nil
	}
}

// dialToBusy is dialTo with a hook that makes the rig answer "?;" instead —
// what a real Kenwood does while it is busy, rather than staying silent.
func dialToBusy(rig *ts2000, busy func(cmd string) bool) func() (serial.Port, error) {
	return func() (serial.Port, error) {
		return &fakeSerial{toRig: &strings.Builder{}, answer: func(cmd string) string {
			if busy(cmd) {
				return "?;"
			}
			return rig.answer(cmd)
		}}, nil
	}
}

func TestKenwoodAgainstEmulatedRig(t *testing.T) {
	rig := &ts2000{vfoA: 14250000, vfoB: 14260000, mode: '2'} // 20 m USB
	k := NewKenwood("COM-TEST", 9600, "FT8")
	k.dialPort = dialTo(rig)

	if err := k.Connect(); err != nil {
		t.Fatalf("connect: %v", err)
	}
	defer k.Disconnect()
	if k.model != "TS-2000" {
		t.Errorf("model = %q, want TS-2000 (from ID019)", k.model)
	}

	// Simplex on A.
	s, err := k.ReadState()
	if err != nil {
		t.Fatalf("read: %v", err)
	}
	if !s.Connected || s.FreqHz != 14250000 || s.Mode != "USB" || s.Vfo != "A" || s.Split {
		t.Errorf("simplex A gave %+v — want 14250000 USB on A, no split", s)
	}

	// On B: everything must follow the VFO in use, the fault that took several
	// rounds to settle in the Yaesu backend.
	rig.onB = true
	if s, err = k.ReadState(); err != nil {
		t.Fatalf("read: %v", err)
	}
	if s.FreqHz != 14260000 || s.Vfo != "B" {
		t.Errorf("on B gave %d on VFO %s — want 14260000 on B", s.FreqHz, s.Vfo)
	}

	// Tuning must write to the VFO in use, not blindly to FA.
	if err := k.SetFrequency(14265000); err != nil {
		t.Fatalf("set freq: %v", err)
	}
	if rig.vfoB != 14265000 {
		t.Errorf("VFO B = %d, want 14265000 — the write went to the wrong VFO", rig.vfoB)
	}
	if rig.vfoA != 14250000 {
		t.Errorf("VFO A was disturbed: %d", rig.vfoA)
	}

	// Split: receive on B, transmit on A. ADIF says FREQ is the TRANSMIT
	// frequency, so the two must not be swapped — a mistake that writes the
	// wrong frequency into every logged QSO.
	rig.split = true
	if s, err = k.ReadState(); err != nil {
		t.Fatalf("read: %v", err)
	}
	if !s.Split || s.FreqHz != 14250000 || s.RxFreqHz != 14265000 {
		t.Errorf("split gave tx=%d rx=%d split=%v — want tx 14250000 (A), rx 14265000 (B)",
			s.FreqHz, s.RxFreqHz, s.Split)
	}

	// Mode: CW below 10 MHz stays CW; the sideband convention only governs SSB.
	rig.split = false
	if err := k.SetMode("CW"); err != nil {
		t.Fatalf("set mode: %v", err)
	}
	if rig.mode != '3' {
		t.Errorf("mode digit = %q, want '3' (CW)", rig.mode)
	}

	// PTT is a bare command, and the rig must have actually seen it.
	if err := k.SetPTT(true); err != nil {
		t.Fatalf("ptt: %v", err)
	}
	if err := k.SetPTT(false); err != nil {
		t.Fatalf("ptt off: %v", err)
	}
	seen := strings.Join(rig.seen, " ")
	for _, want := range []string{"AI0;", "TX;", "RX;"} {
		if !strings.Contains(seen, want) {
			t.Errorf("the rig never received %s — sent: %s", want, seen)
		}
	}
}

// A port that opens onto silence must be reported as such, not as a connected
// radio. A powered-off rig logged as "connected" wasted an evening of a user's
// time on the Yaesu backend.
func TestKenwoodSilentRigIsNotConnected(t *testing.T) {
	k := NewKenwood("COM-TEST", 9600, "FT8")
	k.dialPort = func() (serial.Port, error) {
		return &fakeSerial{toRig: &strings.Builder{}, answer: func(string) string { return "" }}, nil
	}
	err := k.Connect()
	if err == nil {
		t.Fatal("a silent port was reported as a connected rig")
	}
	if !strings.Contains(err.Error(), "sent nothing") {
		t.Errorf("error was %q — a silent port should be reported as silence", err)
	}
	if k.model != "" {
		t.Errorf("a stale model survived a failed connect: %q", k.model)
	}
}

// Split found through FR/FT when the rig never fills IF's split bit.
//
// Reported on a Flex through its Kenwood CAT emulation: frequency read
// perfectly, split never appeared. Rather than guess at which IF column that
// firmware populates, ask the question that DEFINES split — is the transmit VFO
// a different VFO from the receive one — which is what FR and FT answer, and
// the same rule the Yaesu backend settled on after several wrong turns.
func TestKenwoodSplitFromFRFT(t *testing.T) {
	rig := &ts2000{vfoA: 14250000, vfoB: 14260000, mode: '2', lazyIF: true}
	k := NewKenwood("COM-TEST", 9600, "FT8")
	k.dialPort = dialTo(rig)
	if err := k.Connect(); err != nil {
		t.Fatalf("connect: %v", err)
	}
	defer k.Disconnect()

	// Simplex: FR and FT agree, and nothing may be invented from that.
	s, err := k.ReadState()
	if err != nil {
		t.Fatalf("read: %v", err)
	}
	if s.Split {
		t.Errorf("split reported while FR and FT agree: tx=%d rx=%d", s.FreqHz, s.RxFreqHz)
	}

	// Split on, IF still silent about it: receive on A, transmit on B.
	rig.split = true
	// The FR/FT split re-check is throttled to once a second (it tripled the poll
	// time on a slow K3), so back-to-back reads reuse the last result. Simulate the
	// recheck window having elapsed — in the field the 250 ms poll covers it inside
	// a second.
	k.splitCheckAt = time.Time{}
	if s, err = k.ReadState(); err != nil {
		t.Fatalf("read: %v", err)
	}
	if !s.Split {
		t.Fatal("split not detected — FR/FT disagreed and IF's bit was empty, which is the reported case")
	}
	if s.FreqHz != 14260000 || s.RxFreqHz != 14250000 {
		t.Errorf("tx=%d rx=%d — want tx 14260000 (B), rx 14250000 (A)", s.FreqHz, s.RxFreqHz)
	}
}

// A rig that rejects FR/FT is asked once, then left alone — and its IF split
// bit still works. The fallback must not cost a timeout on every poll, nor
// break the rigs that were already fine.
func TestKenwoodSplitWhenFRFTRejected(t *testing.T) {
	rig := &ts2000{vfoA: 14250000, vfoB: 14260000, mode: '2', noVFOCmds: true}
	k := NewKenwood("COM-TEST", 9600, "FT8")
	k.dialPort = dialTo(rig)
	if err := k.Connect(); err != nil {
		t.Fatalf("connect: %v", err)
	}
	defer k.Disconnect()

	if _, err := k.ReadState(); err != nil {
		t.Fatalf("read: %v", err)
	}
	asked := 0
	for _, c := range rig.seen {
		if c == "FR;" || c == "FT;" {
			asked++
		}
	}
	if _, err := k.ReadState(); err != nil {
		t.Fatalf("read: %v", err)
	}
	after := 0
	for _, c := range rig.seen {
		if c == "FR;" || c == "FT;" {
			after++
		}
	}
	if after != asked {
		t.Errorf("a rejected command was asked again: %d then %d", asked, after)
	}

	// IF's own split bit still drives the result on such a rig.
	rig.split = true
	s, err := k.ReadState()
	if err != nil {
		t.Fatalf("read: %v", err)
	}
	if !s.Split || s.FreqHz != 14260000 || s.RxFreqHz != 14250000 {
		t.Errorf("IF-reported split broke: split=%v tx=%d rx=%d", s.Split, s.FreqHz, s.RxFreqHz)
	}
}

// A rig that talks but never answers what was asked must not be reported as a
// silent one.
//
// The two faults need opposite responses: silence means the radio is off, the
// wrong port, or a dead cable; noise means the baud rate is wrong or something
// is echoing the line. "The rig is not answering" sent an operator checking the
// power switch on a radio whose frames were visibly arriving.
func TestKenwoodNoisyRigIsReportedAsNoiseNotSilence(t *testing.T) {
	k := NewKenwood("COM-TEST", 9600, "FT8")
	k.dialPort = func() (serial.Port, error) {
		return &fakeSerial{toRig: &strings.Builder{}, answer: func(cmd string) string {
			return "XX9999;" // something, but never the reply to ID; or IF;
		}}, nil
	}
	err := k.Connect()
	if err == nil {
		t.Fatal("a rig answering gibberish was reported as connected")
	}
	if !strings.Contains(err.Error(), "sending data") || !strings.Contains(err.Error(), "XX9999;") {
		t.Errorf("error was %q — it should say data arrived, and quote it", err)
	}
}

// A "?;" to IF; must not tear the link down.
//
// The TS-590SG answers "?;" for a moment after coming out of transmit, which is
// exactly when the poll resumes. In the field that single rejected poll dropped
// the whole CAT link: WSJT-X, keying through shared CAT, lost the rig, and
// Hamlib then sent an uninitialised frequency (2^63) that OpsLog refused — an
// alarming error whose real cause was three lines earlier.
func TestKenwoodBusyRigKeepsTheLink(t *testing.T) {
	rig := &ts2000{vfoA: 14250000, vfoB: 14260000, mode: '2'}
	busy := 0
	k := NewKenwood("COM-TEST", 9600, "FT8")
	k.dialPort = dialToBusy(rig, func(cmd string) bool {
		if strings.HasPrefix(cmd, "IF") && busy > 0 {
			busy--
			return true
		}
		return false
	})
	if err := k.Connect(); err != nil {
		t.Fatalf("connect: %v", err)
	}
	defer k.Disconnect()

	// One good read establishes the state the busy window will keep serving.
	first, err := k.ReadState()
	if err != nil || !first.Connected {
		t.Fatalf("first read: %+v err=%v", first, err)
	}

	// Inside the grace: still connected, still reporting the last good frequency.
	busy = ifRejectGrace
	for i := 1; i <= ifRejectGrace; i++ {
		s, err := k.ReadState()
		if err != nil {
			t.Fatalf("reject %d dropped the link: %v", i, err)
		}
		if !s.Connected || s.FreqHz != first.FreqHz {
			t.Errorf("reject %d gave %+v — want the last good state, still connected", i, s)
		}
	}

	// The rig answers again → the tolerance resets, so a later busy spell gets
	// the full allowance instead of inheriting the previous one.
	if s, err := k.ReadState(); err != nil || !s.Connected {
		t.Fatalf("recovery read failed: %+v err=%v", s, err)
	}

	// Past the grace, a persistent refusal IS a fault and must surface.
	busy = ifRejectGrace + 1
	var lastErr error
	for i := 0; i <= ifRejectGrace; i++ {
		if _, lastErr = k.ReadState(); lastErr != nil {
			break
		}
	}
	if lastErr == nil {
		t.Error("a rig refusing IF; forever was still reported as healthy")
	}
}

// In split, the two frequencies must not swap the moment the operator keys up.
//
// IF reports the VFO "in use", and in split that is the RECEIVE VFO on receive
// and the TRANSMIT VFO on transmit. The backend took it as the receive VFO
// always, so everything read correctly until the PTT closed and then reversed —
// reported from a TS-590 on USB.
//
// It matters beyond the display: FreqHz is what a QSO is LOGGED on, so a split
// contact would have been logged on the DX's frequency instead of the operator's.
func TestKenwoodSplitDoesNotSwapOnTransmit(t *testing.T) {
	rig := &ts2000{vfoA: 14200000, vfoB: 14205000, mode: '2', split: true}
	k := NewKenwood("COM-TEST", 9600, "FT8")
	k.dialPort = dialTo(rig)
	if err := k.Connect(); err != nil {
		t.Fatalf("connect: %v", err)
	}
	defer k.Disconnect()

	// Receiving: IF reports VFO A, the receive dial.
	s, err := k.ReadState()
	if err != nil {
		t.Fatalf("read on receive: %v", err)
	}
	if !s.Split || s.FreqHz != 14205000 || s.RxFreqHz != 14200000 {
		t.Fatalf("on receive: split=%v tx=%d rx=%d — want tx 14205000, rx 14200000",
			s.Split, s.FreqHz, s.RxFreqHz)
	}

	// Keyed: the rig switches to the transmit VFO and sets IF's TX byte.
	rig.onB, rig.tx = true, true
	s, err = k.ReadState()
	if err != nil {
		t.Fatalf("read on transmit: %v", err)
	}
	if s.FreqHz != 14205000 || s.RxFreqHz != 14200000 {
		t.Errorf("on transmit: tx=%d rx=%d — the two swapped; want tx 14205000, rx 14200000",
			s.FreqHz, s.RxFreqHz)
	}
}
