package steppir

import (
	"encoding/binary"
	"testing"
)

// The exact bytes are the correctness checksum. If buildSet ever drifts from the
// three-source-agreed layout, this fails — a wrong packet is a silently mistuned
// antenna, far worse than a compile error.
func TestBuildSetLayout(t *testing.T) {
	// 14.074 MHz, normal, set-freq. freq/10 = 1_407_400 = 0x00 0x15 0x79 0xA8.
	pkt := buildSet(14_074_000, DirNormal, '1')
	want := []byte{'@', 'A', 0x00, 0x15, 0x79, 0xA8, 0x00, 0x00, '1', 0x00, 0x0D}
	if len(pkt) != 11 {
		t.Fatalf("packet is %d bytes, want 11", len(pkt))
	}
	for i := range want {
		if pkt[i] != want[i] {
			t.Fatalf("byte %d = 0x%02X, want 0x%02X\n got %X\nwant %X", i, pkt[i], want[i], pkt, want)
		}
	}
	// Frequency must round-trip: bytes [2:6] × 10 = Hz.
	if got := int(binary.BigEndian.Uint32(pkt[2:6])) * 10; got != 14_074_000 {
		t.Fatalf("freq round-trip = %d, want 14074000", got)
	}
}

func TestBuildSetDirectionAndCommand(t *testing.T) {
	cases := []struct {
		dir     int
		cmd     byte
		wantDir byte
		wantCmd byte
	}{
		{DirNormal, '1', 0x00, '1'},
		{Dir180, '1', 0x40, '1'},
		{DirBi, '1', 0x80, '1'},
		{DirNormal, 'S', 0x00, 'S'}, // retract / home
		{DirNormal, 'R', 0x00, 'R'}, // autotrack on
	}
	for _, c := range cases {
		pkt := buildSet(21_000_000, c.dir, c.cmd)
		if pkt[7] != c.wantDir {
			t.Errorf("dir %d → byte 0x%02X, want 0x%02X", c.dir, pkt[7], c.wantDir)
		}
		if pkt[8] != c.wantCmd {
			t.Errorf("cmd %q → byte 0x%02X, want 0x%02X", c.cmd, pkt[8], c.wantCmd)
		}
	}
}

// A REAL status frame captured off an SDA controller (F4BPO's, 2026-07-15):
// 40 41 00 4C C5 84 00 87 30 38 0D — 50.313 MHz, idle, bidirectional, interface
// version "08". Using the actual device output (rather than hand-built bytes)
// pins parseStatus to real hardware, and independently confirms the ÷10 wire
// scale: 0x4CC584 × 10 = 50 313 000 Hz, a genuine 6 m frequency.
func TestParseStatusRealFrame(t *testing.T) {
	frame := []byte{0x40, 0x41, 0x00, 0x4C, 0xC5, 0x84, 0x00, 0x87, 0x30, 0x38, 0x0D}
	st, err := parseStatus(frame)
	if err != nil {
		t.Fatal(err)
	}
	if st.Frequency != 50313 {
		t.Errorf("freq = %d kHz, want 50313 (50.313 MHz)", st.Frequency)
	}
	if st.Direction != DirBi { // 0x87 & 0xE0 = 0x80 = bidirectional
		t.Errorf("direction = %d, want %d (bidirectional)", st.Direction, DirBi)
	}
	if st.MotorsMoving != 0 {
		t.Errorf("moving = %d, want 0 (motors byte 0x00)", st.MotorsMoving)
	}
}

// parseStatus decodes what the controller sends back — the inverse of buildSet's
// frequency field, plus the direction nibble and the motors-busy byte.
func TestParseStatus(t *testing.T) {
	frame := []byte{0x40, 0x41, 0x00, 0x15, 0x79, 0xA8, 0x01, 0x40, '0', '8', 0x0D}
	st, err := parseStatus(frame)
	if err != nil {
		t.Fatal(err)
	}
	if st.Frequency != 14074 {
		t.Errorf("freq = %d kHz, want 14074", st.Frequency)
	}
	if st.Direction != Dir180 {
		t.Errorf("direction = %d, want %d (180°)", st.Direction, Dir180)
	}
	if st.MotorsMoving != 0 { // 0x01 is the always-set bit, not motion
		t.Errorf("moving = %d, want 0 (only the always-on bit set)", st.MotorsMoving)
	}

	// Motors busy: a bit beyond 0x01 is set.
	frame[6] = 0x07
	st, _ = parseStatus(frame)
	if st.MotorsMoving == 0 {
		t.Error("active-motors 0x07 should read as moving")
	}

	// 0xFF is "command received", not motion.
	frame[6] = 0xFF
	st, _ = parseStatus(frame)
	if st.MotorsMoving != 0 {
		t.Error("active-motors 0xFF (command received) must not read as moving")
	}
}
