package spid

import (
	"math"
	"testing"
)

// The frames are pinned against Hamlib's spid.c, the reference implementation.
// This protocol has one trap and it is here: digits go out as ASCII and come
// back RAW. Getting that backwards points an antenna at a heading nobody asked
// for, and nothing in the app would notice.
func TestSetFrameMatchesTheReference(t *testing.T) {
	// Hamlib: u_az = PH × (360 + az), then the four decimal digits as ASCII;
	// PH and PV raw; K = 0x2F.
	got := BuildSet(0, 0, 1, Rot2Prog) // 360 → "0360"
	want := []byte{0x57, '0', '3', '6', '0', 0x01, '0', '3', '6', '0', 0x01, 0x2F, 0x20}
	assertBytes(t, "az 0 res 1", got, want)

	// 90° at half-degree resolution: 2 × 450 = 900 → "0900".
	got = BuildSet(90, 0, 2, Rot2Prog)
	want = []byte{0x57, '0', '9', '0', '0', 0x02, '0', '7', '2', '0', 0x02, 0x2F, 0x20}
	assertBytes(t, "az 90 res 2", got, want)

	// A quarter-degree controller, 359°: 4 × 719 = 2876.
	got = BuildSet(359, 0, 4, Rot2Prog)
	want = []byte{0x57, '2', '8', '7', '6', 0x04, '1', '4', '4', '0', 0x04, 0x2F, 0x20}
	assertBytes(t, "az 359 res 4", got, want)
}

// A Rot1Prog reads its azimuth from offsets 1, 2 and 3 — three digits, the same
// field its five-byte replies use.
//
// This is field evidence, not a reading of the reference: sending the
// four-digit Rot2Prog form to a RAK/RAU made every command turn the antenna
// nearly a full circle ANTICLOCKWISE. "0450" for 90° was read as 045, which is
// 45 − 360 = −315°, and the same shift made 0°, 180° and every other target go
// the wrong way round too. The three cases below are the ones that were tried
// on the tower.
func TestRot1ProgSetFrameIsThreeDigits(t *testing.T) {
	// 0° → 360 → "360". The "point to 0°" test that did nothing.
	assertBytes(t, "rot1prog az 0", BuildSet(0, 0, 1, Rot1Prog),
		[]byte{0x57, '3', '6', '0', '0', 0x01, '3', '6', '0', '0', 0x01, 0x2F, 0x20})

	// 90° → 450 → "450". Sent as "0450" it read as 45 − 360 = −315°, which from
	// 45° is a full turn the wrong way.
	assertBytes(t, "rot1prog az 90", BuildSet(90, 0, 1, Rot1Prog),
		[]byte{0x57, '4', '5', '0', '0', 0x01, '3', '6', '0', '0', 0x01, 0x2F, 0x20})

	// 359° → 719 → "719": three digits still, at the top of the range.
	assertBytes(t, "rot1prog az 359", BuildSet(359, 0, 1, Rot1Prog),
		[]byte{0x57, '7', '1', '9', '0', 0x01, '3', '6', '0', '0', 0x01, 0x2F, 0x20})
}

// What a Rot1Prog is SENT and what it REPORTS have to be the same number, or
// the antenna goes somewhere nobody asked for. Round-tripping every degree of
// the circle through the command builder and the reply parser is the cheapest
// way to say that, and it is the check that would have caught the four-digit
// frame before it reached a tower.
func TestRot1ProgCommandAndReplyAgree(t *testing.T) {
	for deg := 0; deg < 360; deg++ {
		cmd := BuildSet(float64(deg), 0, 1, Rot1Prog)
		// The controller reads three ASCII digits and answers with the same value
		// in raw bytes — the asymmetry this protocol is built on.
		reply := []byte{0x57, cmd[1] - '0', cmd[2] - '0', cmd[3] - '0', 0x20}
		az, _, _, err := ParseStatus(reply, Rot1Prog)
		if err != nil {
			t.Fatalf("%d°: %v", deg, err)
		}
		if int(az+0.5) != deg {
			t.Fatalf("commanded %d°, the controller would report %v° — a %v° error",
				deg, az, az-float64(deg))
		}
	}
}

// Status and stop carry no position: every data byte is zero, only K differs.
func TestStatusAndStopFrames(t *testing.T) {
	assertBytes(t, "status", BuildStatus(),
		[]byte{0x57, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x1F, 0x20})
	assertBytes(t, "stop", BuildStop(),
		[]byte{0x57, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x0F, 0x20})
}

// A reply's digits are RAW, and the 360 offset is what lets a pulse-counting
// controller report a rotator that has turned past north — the whole reason
// these rotators exist.
func TestParseStatusRot2Prog(t *testing.T) {
	// 0x57 H1 H2 H3 H4 PH V1 V2 V3 V4 PV 0x20
	// az digits 4,5,1,5 → 451.5 − 360 = 91.5
	frame := []byte{0x57, 4, 5, 1, 5, 0x02, 3, 6, 0, 0, 0x02, 0x20}
	az, el, res, err := ParseStatus(frame, Rot2Prog)
	if err != nil {
		t.Fatalf("ParseStatus: %v", err)
	}
	if math.Abs(az-91.5) > 0.001 {
		t.Errorf("az = %v, want 91.5", az)
	}
	if math.Abs(el-0) > 0.001 {
		t.Errorf("el = %v, want 0", el)
	}
	if res != 2 {
		t.Errorf("resolution = %d, want 2 — the controller's own value must win", res)
	}
}

// Rot1Prog: five bytes, three digits, no elevation.
func TestParseStatusRot1Prog(t *testing.T) {
	az, el, res, err := ParseStatus([]byte{0x57, 4, 5, 1, 0x20}, Rot1Prog)
	if err != nil {
		t.Fatalf("ParseStatus: %v", err)
	}
	if math.Abs(az-91) > 0.001 {
		t.Errorf("az = %v, want 91", az)
	}
	if el != 0 || res != 1 {
		t.Errorf("el = %v, res = %d — Rot1Prog has neither", el, res)
	}
}

// A truncated or foreign frame must be refused rather than decoded into a
// heading: half a reply read as a position turns an antenna somewhere real.
func TestParseStatusRefusesRubbish(t *testing.T) {
	for name, frame := range map[string][]byte{
		"short":    {0x57, 4, 5, 1},
		"no start": {0x00, 4, 5, 1, 5, 1, 3, 6, 0, 0, 1, 0x20},
		"no end":   {0x57, 4, 5, 1, 5, 1, 3, 6, 0, 0, 1, 0x00},
		"empty":    {},
	} {
		if _, _, _, err := ParseStatus(frame, Rot2Prog); err == nil {
			t.Errorf("%s: decoded without complaint", name)
		}
	}
}

func assertBytes(t *testing.T, what string, got, want []byte) {
	t.Helper()
	if len(got) != len(want) {
		t.Fatalf("%s: % X\nwant     % X", what, got, want)
	}
	for i := range want {
		if got[i] != want[i] {
			t.Fatalf("%s: % X\nwant     % X", what, got, want)
		}
	}
}
