"""Tests for BLE protocol encoding and decoding functions."""

import pytest

from acaia_lunar_ble import (
    HEARTBEAT,
    IDENTIFY,
    NOTIFICATION_REQUEST,
    ScaleSettings,
    decode_weight,
)
from acaia_lunar_ble import (
    HEADER1,
    HEADER2,
    CMD_EVENT,
    CMD_SETTINGS_RESPONSE,
    EVENT_WEIGHT,
    EVENT_TIMER,
    EVENT_BUTTON,
    EVENT_WEIGHT_STABLE,
    _encode_packet,
    _decode_weight_payload,
    _decode_timer_payload,
    _decode_settings,
    _decode_button_event,
    _BUTTON_MAP,
)


# ---------------------------------------------------------------------------
# _encode_packet
# ---------------------------------------------------------------------------

class TestEncodePacket:
    def test_heartbeat_matches_constant(self):
        """Heartbeat built via _encode_packet should match the pre-built constant."""
        packet = _encode_packet(0x00, [0x02, 0x00])
        assert packet == HEARTBEAT

    def test_header_and_cmd(self):
        packet = _encode_packet(0x04, [0x00])
        assert packet[0] == HEADER1
        assert packet[1] == HEADER2
        assert packet[2] == 0x04

    def test_checksum_even_odd_split(self):
        """Checksums: even-indexed payload bytes go to cksum1, odd to cksum2."""
        # payload = [0x10, 0x20, 0x30, 0x40]
        # even indices (0, 2): 0x10 + 0x30 = 0x40
        # odd indices (1, 3): 0x20 + 0x40 = 0x60
        packet = _encode_packet(0x00, [0x10, 0x20, 0x30, 0x40])
        assert packet[-2] == 0x40  # cksum1
        assert packet[-1] == 0x60  # cksum2

    def test_checksum_overflow_wraps(self):
        """Checksums wrap at 0xFF."""
        packet = _encode_packet(0x00, [0xFF, 0x00, 0x02])
        # even indices (0, 2): 0xFF + 0x02 = 0x101 & 0xFF = 0x01
        # odd indices (1): 0x00
        assert packet[-2] == 0x01
        assert packet[-1] == 0x00

    def test_empty_payload(self):
        packet = _encode_packet(0x00, [])
        assert len(packet) == 5  # header(2) + cmd(1) + cksum(2)
        assert packet[-2] == 0x00
        assert packet[-1] == 0x00

    def test_notification_request_checksums(self):
        """Verify the pre-built NOTIFICATION_REQUEST has correct checksums."""
        # Payload bytes (index 3 onwards): 0x09 0x00 0x01 0x01 0x02 0x02 0x05 0x03 0x04
        payload = list(NOTIFICATION_REQUEST[3:-2])
        cksum1 = sum(payload[i] for i in range(0, len(payload), 2)) & 0xFF
        cksum2 = sum(payload[i] for i in range(1, len(payload), 2)) & 0xFF
        assert NOTIFICATION_REQUEST[-2] == cksum1
        assert NOTIFICATION_REQUEST[-1] == cksum2


# ---------------------------------------------------------------------------
# _decode_weight_payload
# ---------------------------------------------------------------------------

class TestDecodeWeightPayload:
    def test_zero_weight(self):
        # raw=0, scale=10^1=10, positive → 0.0
        payload = bytearray([0x00, 0x00, 0x00, 0x00, 0x01, 0x00])
        assert _decode_weight_payload(payload) == 0.0

    def test_positive_weight(self):
        # raw = 0x00E8 (low=0xE8, high=0x00) = 232, scale=10^1=10 → 23.2
        payload = bytearray([0xE8, 0x00, 0x00, 0x00, 0x01, 0x00])
        assert _decode_weight_payload(payload) == pytest.approx(23.2)

    def test_negative_weight(self):
        # raw = 50 (low=0x32, high=0x00), scale=10^1=10, sign bit set → -5.0
        payload = bytearray([0x32, 0x00, 0x00, 0x00, 0x01, 0x02])
        assert _decode_weight_payload(payload) == pytest.approx(-5.0)

    def test_high_precision(self):
        # raw = 12345, scale=10^2=100 → 123.45
        raw = 12345
        payload = bytearray([raw & 0xFF, (raw >> 8) & 0xFF, 0x00, 0x00, 0x02, 0x00])
        assert _decode_weight_payload(payload) == pytest.approx(123.45)

    def test_too_short(self):
        assert _decode_weight_payload(bytearray([0x00] * 5)) is None

    def test_scale_exponent_zero(self):
        # raw = 500, scale=10^0=1 → 500.0
        raw = 500
        payload = bytearray([raw & 0xFF, (raw >> 8) & 0xFF, 0x00, 0x00, 0x00, 0x00])
        assert _decode_weight_payload(payload) == pytest.approx(500.0)


# ---------------------------------------------------------------------------
# decode_weight (full message)
# ---------------------------------------------------------------------------

class TestDecodeWeight:
    def _make_weight_message(self, raw: int, scale_exp: int = 1, negative: bool = False) -> bytearray:
        """Build a full 17-byte weight notification message."""
        data = bytearray(17)
        data[0] = HEADER1
        data[1] = HEADER2
        data[2] = CMD_EVENT
        data[3] = 0x0C  # length
        data[4] = EVENT_WEIGHT
        data[5] = raw & 0xFF
        data[6] = (raw >> 8) & 0xFF
        data[9] = scale_exp
        data[10] = 0x02 if negative else 0x00
        return data

    def test_valid_weight(self):
        msg = self._make_weight_message(1850, scale_exp=1)
        assert decode_weight(msg) == pytest.approx(185.0)

    def test_negative_weight(self):
        msg = self._make_weight_message(100, scale_exp=1, negative=True)
        assert decode_weight(msg) == pytest.approx(-10.0)

    def test_wrong_header(self):
        msg = self._make_weight_message(100)
        msg[0] = 0x00
        assert decode_weight(msg) is None

    def test_wrong_cmd(self):
        msg = self._make_weight_message(100)
        msg[2] = 0x08  # settings, not event
        assert decode_weight(msg) is None

    def test_non_weight_event(self):
        msg = self._make_weight_message(100)
        msg[4] = EVENT_TIMER  # timer, not weight
        assert decode_weight(msg) is None

    def test_too_short(self):
        assert decode_weight(bytearray([HEADER1, HEADER2, CMD_EVENT])) is None

    def test_empty(self):
        assert decode_weight(bytearray()) is None


# ---------------------------------------------------------------------------
# _decode_timer_payload
# ---------------------------------------------------------------------------

class TestDecodeTimerPayload:
    def test_zero(self):
        assert _decode_timer_payload(bytearray([0, 0, 0])) == 0.0

    def test_one_minute_thirty_seconds(self):
        # 1 min, 30 sec, 0 tenths = 90.0
        assert _decode_timer_payload(bytearray([1, 30, 0])) == pytest.approx(90.0)

    def test_with_tenths(self):
        # 0 min, 5 sec, 3 tenths = 5.3
        assert _decode_timer_payload(bytearray([0, 5, 3])) == pytest.approx(5.3)

    def test_five_minutes(self):
        # 5 min, 0 sec, 0 tenths = 300.0
        assert _decode_timer_payload(bytearray([5, 0, 0])) == pytest.approx(300.0)

    def test_too_short(self):
        assert _decode_timer_payload(bytearray([0, 0])) is None


# ---------------------------------------------------------------------------
# _decode_settings
# ---------------------------------------------------------------------------

class TestDecodeSettings:
    def _make_settings_message(
        self, battery: int = 100, units: int = 2, auto_off: int = 1, beep: int = 1
    ) -> bytearray:
        """Build a settings response message."""
        data = bytearray(10)
        data[0] = HEADER1
        data[1] = HEADER2
        data[2] = CMD_SETTINGS_RESPONSE
        # payload starts at byte 3
        data[3] = 0x00  # unknown
        data[4] = battery  # battery
        data[5] = units  # units (2=grams, 5=ounces)
        data[6] = 0x00  # unknown
        data[7] = auto_off  # auto-off (* 5 = minutes)
        data[8] = 0x00  # unknown
        data[9] = beep  # beep (1=on)
        return data

    def test_grams_beep_on(self):
        msg = self._make_settings_message(battery=85, units=2, auto_off=2, beep=1)
        result = _decode_settings(msg)
        assert result == ScaleSettings(battery=85, units="grams", auto_off_minutes=10, beep=True)

    def test_ounces_beep_off(self):
        msg = self._make_settings_message(battery=50, units=5, auto_off=3, beep=0)
        result = _decode_settings(msg)
        assert result == ScaleSettings(battery=50, units="ounces", auto_off_minutes=15, beep=False)

    def test_battery_mask(self):
        # Battery byte 0xFF should be masked to 0x7F = 127
        msg = self._make_settings_message(battery=0xFF)
        result = _decode_settings(msg)
        assert result.battery == 127

    def test_too_short(self):
        assert _decode_settings(bytearray(9)) is None


# ---------------------------------------------------------------------------
# _decode_button_event
# ---------------------------------------------------------------------------

class TestDecodeButtonEvent:
    def test_tare(self):
        assert _decode_button_event(bytearray([0, 5])) == "tare"

    def test_start(self):
        assert _decode_button_event(bytearray([8, 5])) == "start"

    def test_stop(self):
        assert _decode_button_event(bytearray([10, 7])) == "stop"

    def test_reset(self):
        assert _decode_button_event(bytearray([9, 7])) == "reset"

    def test_unknown(self):
        assert _decode_button_event(bytearray([99, 99])) is None

    def test_too_short(self):
        assert _decode_button_event(bytearray([0])) is None

    def test_empty(self):
        assert _decode_button_event(bytearray()) is None

    def test_all_known_buttons_covered(self):
        """Ensure all entries in _BUTTON_MAP are tested."""
        assert len(_BUTTON_MAP) == 4


# ---------------------------------------------------------------------------
# Pre-built packet constants
# ---------------------------------------------------------------------------

class TestPreBuiltPackets:
    def test_identify_header(self):
        assert IDENTIFY[0] == HEADER1
        assert IDENTIFY[1] == HEADER2

    def test_notification_request_header(self):
        assert NOTIFICATION_REQUEST[0] == HEADER1
        assert NOTIFICATION_REQUEST[1] == HEADER2

    def test_heartbeat_header(self):
        assert HEARTBEAT[0] == HEADER1
        assert HEARTBEAT[1] == HEADER2

    def test_identify_cmd_byte(self):
        assert IDENTIFY[2] == 0x0B  # CMD_IDENTIFY

    def test_notification_request_cmd_byte(self):
        assert NOTIFICATION_REQUEST[2] == 0x0C  # CMD_EVENT

    def test_heartbeat_cmd_byte(self):
        assert HEARTBEAT[2] == 0x00  # CMD_HEARTBEAT
