"""Tests for BCM4775 binary protocol layer.

Tests packet encoding/decoding, checksum computation, sequence wrapping,
reliable transport behavior, and malformed packet rejection.
"""

import struct

import pytest

from lc29h.bcm_protocol import (
    AUTOBAUD_BYTE,
    AUTOBAUD_COUNT,
    AUTOBAUD_SEQUENCE,
    BOOTLOADER_HEADER_SIZE,
    BOOTLOADER_MAGIC,
    PACKET_SOF,
    PACKET_TYPE_ACK,
    PACKET_TYPE_DATA,
    BcmPacket,
    TransportStats,
    compute_bcm_checksum,
)

# ---------------------------------------------------------------------------
# Checksum tests
# ---------------------------------------------------------------------------


class TestChecksum:
    def test_empty(self):
        assert compute_bcm_checksum(b"") == 0

    def test_single_byte(self):
        assert compute_bcm_checksum(b"\x42") == 0x42

    def test_two_bytes_xor(self):
        # XOR: 0xAA ^ 0x55 = 0xFF
        assert compute_bcm_checksum(b"\xAA\x55") == 0xFF

    def test_all_same_bytes_even(self):
        # XOR of even count of identical bytes = 0
        assert compute_bcm_checksum(b"\xFF\xFF") == 0

    def test_all_same_bytes_odd(self):
        # XOR of odd count = the byte value
        assert compute_bcm_checksum(b"\xFF\xFF\xFF") == 0xFF

    def test_sequential(self):
        data = bytes(range(256))
        # XOR of 0..255 = 0 (each bit cancels)
        assert compute_bcm_checksum(data) == 0

    def test_returns_single_byte(self):
        result = compute_bcm_checksum(b"\xFF\xFF\x01")
        assert 0 <= result <= 0xFF


# ---------------------------------------------------------------------------
# Packet encode/decode tests
# ---------------------------------------------------------------------------


class TestBcmPacket:
    def test_encode_empty_payload(self):
        pkt = BcmPacket(packet_type=PACKET_TYPE_DATA, seq=0, payload=b"")
        encoded = pkt.encode()
        # SOF(1) + type(1) + seq(1) + length(2) + checksum(1) = 6 bytes
        assert len(encoded) == 6
        assert encoded[0] == PACKET_SOF
        assert encoded[1] == PACKET_TYPE_DATA
        assert encoded[2] == 0  # seq
        assert struct.unpack_from("<H", encoded, 3)[0] == 0  # length
        # Checksum is XOR of header bytes
        expected_cs = compute_bcm_checksum(encoded[:5])
        assert encoded[5] == expected_cs

    def test_encode_with_payload(self):
        payload = b"\x01\x02\x03\x04"
        pkt = BcmPacket(packet_type=PACKET_TYPE_DATA, seq=42, payload=payload)
        encoded = pkt.encode()
        # 5 header + 4 payload + 1 checksum = 10
        assert len(encoded) == 10
        assert encoded[2] == 42  # seq
        assert struct.unpack_from("<H", encoded, 3)[0] == 4  # length
        assert encoded[5:9] == payload

    def test_decode_roundtrip(self):
        original = BcmPacket(packet_type=PACKET_TYPE_DATA, seq=100, payload=b"hello")
        encoded = original.encode()
        decoded = BcmPacket.decode(encoded)
        assert decoded.packet_type == original.packet_type
        assert decoded.seq == original.seq
        assert decoded.payload == original.payload

    def test_decode_ack(self):
        ack = BcmPacket.make_ack(7)
        encoded = ack.encode()
        decoded = BcmPacket.decode(encoded)
        assert decoded.packet_type == PACKET_TYPE_ACK
        assert decoded.seq == 7
        assert decoded.payload == b""

    def test_decode_too_short(self):
        with pytest.raises(ValueError, match="too short"):
            BcmPacket.decode(b"\x04\x01")

    def test_decode_bad_sof(self):
        # Build a valid packet then corrupt SOF
        pkt = BcmPacket.make_data(0, b"test").encode()
        corrupted = b"\xFF" + pkt[1:]
        with pytest.raises(ValueError, match="SOF mismatch"):
            BcmPacket.decode(corrupted)

    def test_decode_bad_checksum(self):
        pkt = BcmPacket.make_data(0, b"test").encode()
        # Corrupt the last byte (checksum)
        corrupted = pkt[:-1] + bytes([(pkt[-1] ^ 0xFF)])
        with pytest.raises(ValueError, match="Checksum mismatch"):
            BcmPacket.decode(corrupted)

    def test_decode_truncated_payload(self):
        # Build a packet claiming 100 bytes payload but only provide 5
        header = struct.pack("<BBBH", PACKET_SOF, PACKET_TYPE_DATA, 0, 100)
        data = header + b"\x00" * 5
        with pytest.raises(ValueError, match="truncated"):
            BcmPacket.decode(data)

    def test_sequence_wrapping(self):
        """Sequence numbers should work at boundaries (0, 127, 255)."""
        for seq in [0, 1, 127, 128, 254, 255]:
            pkt = BcmPacket.make_data(seq, b"\xAB")
            decoded = BcmPacket.decode(pkt.encode())
            assert decoded.seq == seq

    def test_large_payload(self):
        """Test with maximum-ish payload."""
        payload = bytes(range(256)) * 4  # 1024 bytes
        pkt = BcmPacket.make_data(0, payload)
        decoded = BcmPacket.decode(pkt.encode())
        assert decoded.payload == payload

    def test_make_data_factory(self):
        pkt = BcmPacket.make_data(5, b"\x01\x02")
        assert pkt.packet_type == PACKET_TYPE_DATA
        assert pkt.seq == 5
        assert pkt.payload == b"\x01\x02"

    def test_make_ack_factory(self):
        pkt = BcmPacket.make_ack(42)
        assert pkt.packet_type == PACKET_TYPE_ACK
        assert pkt.seq == 42
        assert pkt.payload == b""


# ---------------------------------------------------------------------------
# Protocol constants tests
# ---------------------------------------------------------------------------


class TestProtocolConstants:
    def test_autobaud_sequence_length(self):
        assert len(AUTOBAUD_SEQUENCE) == AUTOBAUD_COUNT

    def test_autobaud_sequence_content(self):
        assert all(b == AUTOBAUD_BYTE for b in AUTOBAUD_SEQUENCE)

    def test_bootloader_magic_length(self):
        assert len(BOOTLOADER_MAGIC) == 4

    def test_bootloader_magic_bytes(self):
        assert BOOTLOADER_MAGIC == b"\xf3\xf2\xf1\xf0"

    def test_bootloader_header_size(self):
        assert BOOTLOADER_HEADER_SIZE == 32


# ---------------------------------------------------------------------------
# Transport statistics tests
# ---------------------------------------------------------------------------


class TestTransportStats:
    def test_default_zero(self):
        stats = TransportStats()
        assert stats.rx_packets == 0
        assert stats.tx_packets == 0
        assert stats.tx_retries == 0
        assert stats.max_retry_reached == 0

    def test_increment(self):
        stats = TransportStats()
        stats.rx_packets += 1
        stats.tx_packets += 5
        assert stats.rx_packets == 1
        assert stats.tx_packets == 5
