package rigctld

import (
	"bufio"
	"errors"
	"fmt"
	"net"
	"strings"
	"sync"
	"testing"
	"time"
)

// fakeRig stands in for the CAT manager.
type fakeRig struct {
	mu       sync.Mutex
	freq     int64
	mode     string
	split    bool
	txFreq   int64
	ptt      bool
	setFreqs []int64
	setModes []string
	failSet  bool
	// noSplit models a backend that cannot set split — the case that must reach
	// the client as a refusal instead of a silent success.
	noSplit    bool
	splitCalls []string
}

func (f *fakeRig) Freq() int64          { f.mu.Lock(); defer f.mu.Unlock(); return f.freq }
func (f *fakeRig) Mode() string         { f.mu.Lock(); defer f.mu.Unlock(); return f.mode }
func (f *fakeRig) Split() (bool, int64) { f.mu.Lock(); defer f.mu.Unlock(); return f.split, f.txFreq }

func (f *fakeRig) SetSplit(on bool, txHz int64) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	if f.noSplit {
		return errors.New("this radio cannot set split from software")
	}
	f.splitCalls = append(f.splitCalls, fmt.Sprintf("%v:%d", on, txHz))
	f.split, f.txFreq = on, txHz
	return nil
}
func (f *fakeRig) SetFreq(hz int64) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	if f.failSet {
		return fmt.Errorf("rig refused")
	}
	f.freq = hz
	f.setFreqs = append(f.setFreqs, hz)
	return nil
}
func (f *fakeRig) SetMode(m string) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	if f.failSet {
		return fmt.Errorf("rig refused")
	}
	f.mode = m
	f.setModes = append(f.setModes, m)
	return nil
}
func (f *fakeRig) SetPTT(on bool) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	if f.failSet {
		return fmt.Errorf("rig refused")
	}
	f.ptt = on
	return nil
}

// The command table. These exact strings are what WSJT-X and MSHV put on the
// wire, so they are the contract — a reply in the wrong shape does not degrade
// gracefully, the client simply refuses to work with the rig.
func TestHandleCommands(t *testing.T) {
	rig := &fakeRig{freq: 14074000, mode: "FT8", split: true, txFreq: 14100000}
	s := New(0, rig, nil)

	cases := []struct{ in, want string }{
		{"f", "14074000\n"},
		{"\\get_freq", "14074000\n"},
		{"m", "PKTUSB\n3000\n"}, // a digital mode reads as PKTUSB
		{"t", "0\n"},            // PTT always reads RX — see the comment
		{"v", "VFOA\n"},
		{"s", "1\nVFOB\n"},  // split on, TX on B
		{"i", "14100000\n"}, // split TX frequency
		{"F 14200000", "RPRT 0\n"},
		{"F 14200000.000000", "RPRT 0\n"}, // the float form clients also send
		{"M USB 2400", "RPRT 0\n"},
		{"T 1", "RPRT 0\n"},
		{"V VFOB", "RPRT 0\n"}, // accepted and ignored, never an error
		{"S 1 VFOB", "RPRT 0\n"},
		{"\\chk_vfo", "CHKVFO 0\n"},
		{"F", "RPRT -1\n"}, // missing argument
		{"F not_a_number", "RPRT -1\n"},
		{"Z", "RPRT -11\n"}, // unknown → answered, never silence
		{"", ""},
	}
	for _, c := range cases {
		got, _ := s.handle(c.in)
		if got != c.want {
			t.Errorf("handle(%q) = %q, want %q", c.in, got, c.want)
		}
	}
	if q := func() bool { _, q := s.handle("q"); return q }(); !q {
		t.Error("q must end the session")
	}
}

// Hamlib's VFO dialect. JTDX names the target VFO before the value — "F VFOA
// 14074000" — where MSHV sends "F 14074000". Reading the VFO name as the
// frequency is what produced "Hamlib error: Invalid parameter while setting
// frequency" on JTDX while MSHV worked perfectly.
func TestHandleAcceptsVFOPrefixedCommands(t *testing.T) {
	rig := &fakeRig{freq: 7074000, mode: "SSB"}
	s := New(0, rig, nil)

	if got, _ := s.handle("F VFOA 14074000"); got != "RPRT 0\n" {
		t.Fatalf("handle(\"F VFOA 14074000\") = %q, want RPRT 0", got)
	}
	if got := rig.Freq(); got != 14074000 {
		t.Errorf("frequency = %d, want 14074000 — the VFO name swallowed the value", got)
	}
	if got, _ := s.handle("M VFOA USB 2400"); got != "RPRT 0\n" {
		t.Errorf("handle(\"M VFOA USB 2400\") = %q, want RPRT 0", got)
	}
	if got, _ := s.handle("T VFOA 1"); got != "RPRT 0\n" {
		t.Errorf("handle(\"T VFOA 1\") = %q, want RPRT 0", got)
	}
	// A read with the VFO named must still answer the value, not an error.
	if got, _ := s.handle("f VFOA"); got != "14074000\n" {
		t.Errorf("handle(\"f VFOA\") = %q, want the frequency", got)
	}
	// And the plain dialect must keep working — this is an ADDITION, not a swap.
	if got, _ := s.handle("F 21074000"); got != "RPRT 0\n" {
		t.Errorf("plain set_freq broke: %q", got)
	}
	// "S 1 VFOB" starts with the split flag, not a VFO: nothing must be eaten.
	if got, _ := s.handle("S 1 VFOB"); got != "RPRT 0\n" {
		t.Errorf("handle(\"S 1 VFOB\") = %q, want RPRT 0", got)
	}
}

// A rig that refuses must produce an error report, not a success — a client told
// "RPRT 0" believes the radio moved and will log the wrong frequency.
func TestHandleReportsBackendFailure(t *testing.T) {
	s := New(0, &fakeRig{failSet: true}, nil)
	for _, in := range []string{"F 14200000", "M USB 2400", "T 1"} {
		if got, _ := s.handle(in); got != "RPRT -9\n" {
			t.Errorf("handle(%q) with a failing rig = %q, want RPRT -9", in, got)
		}
	}
}

// get_ptt must echo the last commanded PTT state. WSJT-X/JTDX poll get_ptt while
// transmitting to confirm the rig is keyed; a blanket "0" made them decide PTT
// had failed and abort the over after a second or two.
func TestGetPTTEchoesSetPTT(t *testing.T) {
	s := New(0, &fakeRig{}, nil)
	if got, _ := s.handle("t"); got != "0\n" {
		t.Fatalf("initial get_ptt = %q, want 0", got)
	}
	if got, _ := s.handle("T 1"); got != "RPRT 0\n" {
		t.Fatalf("set_ptt 1 = %q, want RPRT 0", got)
	}
	if got, _ := s.handle("t"); got != "1\n" {
		t.Fatalf("get_ptt after T 1 = %q, want 1 — client would abort TX", got)
	}
	if got, _ := s.handle("T 0"); got != "RPRT 0\n" {
		t.Fatalf("set_ptt 0 = %q, want RPRT 0", got)
	}
	if got, _ := s.handle("t"); got != "0\n" {
		t.Fatalf("get_ptt after T 0 = %q, want 0", got)
	}
}

// dump_state is parsed POSITIONALLY by Hamlib clients: WSJT-X reads the first
// line as the protocol version and refuses to continue if the block is short or
// misshapen. Pinning its shape is what stops a well-meaning edit from silently
// breaking every client.
func TestDumpStateShape(t *testing.T) {
	lines := strings.Split(strings.TrimRight(dumpState, "\n"), "\n")
	if len(lines) < 20 {
		t.Fatalf("dump_state has %d lines — clients expect the full capability block", len(lines))
	}
	if lines[0] != "0" {
		t.Errorf("dump_state protocol version = %q, want \"0\"", lines[0])
	}
	// The frequency-range lines must carry seven fields, or the client's parse
	// slides and every later capability is read from the wrong place.
	for _, i := range []int{3, 5} {
		if n := len(strings.Fields(lines[i])); n != 7 {
			t.Errorf("dump_state line %d has %d fields, want 7: %q", i, n, lines[i])
		}
	}
}

// End to end over a real socket, because the framing (one reply per line,
// flushed immediately) is as much a part of the contract as the text.
func TestServerOverTCP(t *testing.T) {
	rig := &fakeRig{freq: 7074000, mode: "SSB"}
	s := New(0, rig, nil)
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("listen: %v", err)
	}
	s.mu.Lock()
	s.ln = ln
	s.mu.Unlock()
	go func() {
		for {
			c, err := ln.Accept()
			if err != nil {
				return
			}
			go s.serve(c)
		}
	}()
	defer s.Stop()

	c, err := net.DialTimeout("tcp", ln.Addr().String(), dialTimeout)
	if err != nil {
		t.Fatalf("dial: %v", err)
	}
	defer c.Close()
	r := bufio.NewReader(c)

	if _, err := c.Write([]byte("f\n")); err != nil {
		t.Fatalf("write: %v", err)
	}
	line, err := r.ReadString('\n')
	if err != nil {
		t.Fatalf("read: %v", err)
	}
	if strings.TrimSpace(line) != "7074000" {
		t.Errorf("get_freq over TCP = %q, want 7074000", strings.TrimSpace(line))
	}

	if _, err := c.Write([]byte("F 14074000\n")); err != nil {
		t.Fatalf("write: %v", err)
	}
	line, _ = r.ReadString('\n')
	if strings.TrimSpace(line) != "RPRT 0" {
		t.Errorf("set_freq over TCP = %q, want RPRT 0", strings.TrimSpace(line))
	}
	if got := rig.Freq(); got != 14074000 {
		t.Errorf("rig frequency = %d, want 14074000 — the command never reached it", got)
	}
}

// A client that vanishes mid-over must not leave the transmitter keyed. Nothing
// else would notice: the Kenwood/Elecraft backend stops polling while PTT is
// held, so a K3 was seen sitting in transmit for 29 s after WSJT-X's socket was
// closed under it by a settings save.
func TestPTTIsDroppedWhenTheClientDisappears(t *testing.T) {
	rig := &fakeRig{freq: 14074000, mode: "FT8"}
	s := New(0, rig, nil)
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("listen: %v", err)
	}
	s.mu.Lock()
	s.ln = ln
	s.mu.Unlock()
	go func() {
		for {
			c, err := ln.Accept()
			if err != nil {
				return
			}
			go s.serve(c)
		}
	}()
	defer s.Stop()

	c, err := net.DialTimeout("tcp", ln.Addr().String(), dialTimeout)
	if err != nil {
		t.Fatalf("dial: %v", err)
	}
	r := bufio.NewReader(c)
	// "T 3" is what WSJT-X sends for PTT_ON_DATA — the form seen in the field.
	if _, err := c.Write([]byte("T 3\n")); err != nil {
		t.Fatalf("write: %v", err)
	}
	line, _ := r.ReadString('\n')
	if strings.TrimSpace(line) != "RPRT 0" {
		t.Fatalf("set_ptt = %q, want RPRT 0", strings.TrimSpace(line))
	}
	rig.mu.Lock()
	keyed := rig.ptt
	rig.mu.Unlock()
	if !keyed {
		t.Fatalf("rig is not keyed after T 3 — the test proves nothing")
	}

	_ = c.Close()
	// The unkey happens on the serve goroutine's defer, so poll briefly.
	deadline := time.Now().Add(2 * time.Second)
	for time.Now().Before(deadline) {
		rig.mu.Lock()
		keyed = rig.ptt
		rig.mu.Unlock()
		if !keyed {
			return
		}
		time.Sleep(10 * time.Millisecond)
	}
	t.Error("rig still keyed after the client disconnected — the transmitter was left on air")
}

func TestModeMapping(t *testing.T) {
	for _, c := range []struct{ adif, hamlib string }{
		{"SSB", "USB"}, {"LSB", "LSB"}, {"CW", "CW"}, {"RTTY", "RTTY"},
		{"FT8", "PKTUSB"}, {"JS8", "PKTUSB"}, {"", "USB"},
	} {
		if got := adifToHamlib(c.adif); got != c.hamlib {
			t.Errorf("adifToHamlib(%q) = %q, want %q", c.adif, got, c.hamlib)
		}
	}
	// Digital comes back as DATA, never as a specific sub-mode: the CAT backend
	// applies the operator's own digital default, so a client that switches the
	// rig to data does not relabel a JS8 operator's QSOs as FT8.
	for _, c := range []struct{ hamlib, adif string }{
		{"PKTUSB", "DATA"}, {"PKTLSB", "DATA"}, {"DIGU", "DATA"},
		{"USB", "USB"}, {"CWR", "CW"}, {"FMN", "FM"},
	} {
		if got := hamlibToADIF(c.hamlib); got != c.adif {
			t.Errorf("hamlibToADIF(%q) = %q, want %q", c.hamlib, got, c.adif)
		}
	}
}

// A mode the rig is already in must not reach it. WSJT-X restates the mode on
// every band change, and on a native backend each restatement is several CI-V
// round trips with the client blocked on the answer.
func TestSetModeSkipsWhenUnchanged(t *testing.T) {
	cases := []struct {
		rig  string // what the rig reports (ADIF)
		ask  string // what the client asks for (hamlib)
		want int    // times the radio should be touched
	}{
		{"CW", "CW", 0},
		{"SSB", "USB", 0},    // "SSB" is reported as USB to a client
		{"FT8", "PKTUSB", 0}, // data rides on USB; both name it PKTUSB
		{"USB", "PKTUSB", 1}, // out of data mode into it: a real change
		{"CW", "USB", 1},     // a real change
		{"", "USB", 1},       // mode unknown: never assume
	}
	for _, c := range cases {
		f := &fakeRig{mode: c.rig, freq: 14074000}
		s := New(4532, f, nil)
		if got, _ := s.handle("M " + c.ask); got != "RPRT 0\n" {
			t.Fatalf("set_mode %q on a rig in %q = %q", c.ask, c.rig, got)
		}
		if n := len(f.setModes); n != c.want {
			t.Errorf("rig in %q, client asked %q: rig touched %d times, want %d", c.rig, c.ask, n, c.want)
		}
	}
}
