import struct

from meshy.meshcore_packets import PacketType
from meshy.protocol import (
    CMD_GET_ALLOWED_REPEAT_FREQ,
    CMD_GET_CONTACT_BY_KEY,
    CMD_LOGOUT,
    CMD_SEND_TXT_MSG,
    CMD_SET_ADVERT_LATLON,
    CMD_SET_RADIO_TX_POWER,
    LPP_GPS,
    LPP_HUMIDITY,
    LPP_TEMPERATURE,
    RESP_CODE_ALLOWED_REPEAT_FREQ,
    RESP_CODE_BATT_AND_STORAGE,
    RESP_CODE_CHANNEL_MSG_RECV,
    RESP_CODE_CHANNEL_MSG_RECV_V3,
    RESP_CODE_CONTACT,
    RESP_CODE_CONTACT_MSG_RECV,
    RESP_CODE_CUSTOM_VARS,
    RESP_CODE_DEVICE_INFO,
    RESP_CODE_ERR,
    RESP_CODE_SENT,
    TXT_TYPE_PLAIN,
    _parse_path_length_byte,
    _parse_snr,
    build_get_allowed_repeat_freq,
    build_get_contact_by_key,
    build_logout,
    build_send_text_msg,
    build_set_advert_latlon,
    build_set_radio_tx_power,
    max_channel_text_bytes,
    max_dm_text_bytes,
    parse_advert_path_response,
    parse_allowed_repeat_freq,
    parse_batt_and_storage,
    parse_cayenne_lpp,
    parse_channel_message,
    parse_contact_frame,
    parse_contact_message,
    parse_custom_vars,
    parse_device_info,
    parse_error_response,
    parse_sent_response,
)

# ─── Helpers ──────────────────────────────────────────────────


class TestParsePathLengthByte:
    def test_flood(self):
        assert _parse_path_length_byte(255) == (-1, 1)

    def test_direct(self):
        hop, hs = _parse_path_length_byte(0)
        assert hop == 0 and hs == 1

    def test_normal_1byte_hash(self):
        hop, hs = _parse_path_length_byte(3)
        assert hop == 3 and hs == 1

    def test_2byte_hash(self):
        hop, hs = _parse_path_length_byte(0b01_000010)
        assert hop == 2 and hs == 2

    def test_3byte_hash(self):
        hop, hs = _parse_path_length_byte(0b10_000001)
        assert hop == 1 and hs == 3


class TestParseSNR:
    def test_positive(self):
        assert _parse_snr(bytes([20])) == 5.0

    def test_negative(self):
        val = _parse_snr(b"\xf0")  # -16 signed
        assert val == -4.0

    def test_zero(self):
        assert _parse_snr(bytes([0])) == 0.0


# ─── Frame Parsers ────────────────────────────────────────────


class TestParseContactFrame:
    def _build_frame(self):
        frame = bytes([RESP_CODE_CONTACT])
        frame += bytes(32)  # public_key
        frame += bytes([1])  # type = CHAT
        frame += bytes([0])  # flags
        frame += bytes([2])  # path_len_byte: 2 hops, hash_size 1
        frame += bytes(64)  # path (64 bytes)
        frame += b"TestNode" + bytes(24)  # name (32 bytes)
        frame += struct.pack("<I", 1700000000)  # last_advert
        frame += struct.pack("<i", 50123456)  # lat (50.123456)
        frame += struct.pack("<i", 14456789)  # lon (14.456789)
        return frame

    def test_parse_valid(self):
        frame = self._build_frame()
        result = parse_contact_frame(frame)
        assert result is not None
        assert result["type"] == 1
        assert result["adv_name"] == "TestNode"
        assert result["out_path_len"] == 2
        assert abs(result["adv_lat"] - 50.123456) < 0.001
        assert abs(result["adv_lon"] - 14.456789) < 0.001

    def test_too_short(self):
        assert parse_contact_frame(bytes(50)) is None


class TestParseContactMessage:
    def test_parse_v4(self):
        frame = bytes([RESP_CODE_CONTACT_MSG_RECV])
        frame += bytes(6)  # pubkey_prefix
        frame += bytes([0])  # path_len_byte (direct)
        frame += bytes([TXT_TYPE_PLAIN])  # txt_type
        frame += struct.pack("<I", 1700000000)  # sender_timestamp
        frame += b"Hello world\x00"
        result = parse_contact_message(frame)
        assert result is not None
        assert result["type"] == "PRIV"
        assert result["text"] == "Hello world"
        assert result["sender_timestamp"] == 1700000000

    def test_too_short(self):
        assert parse_contact_message(bytes(5)) is None


class TestParseDeviceInfo:
    def test_fw_ver_3(self):
        frame = bytes([RESP_CODE_DEVICE_INFO])
        frame += bytes([3])  # fw_ver
        frame += bytes([16])  # max_contacts raw (16 * 2 = 32)
        frame += bytes([8])  # max_channels
        frame += struct.pack("<I", 123456)  # ble_pin
        frame += b"Jun 10 2026\x00"  # fw_build (12 bytes)
        frame += b"Heltec V3" + bytes(31)  # model (40 bytes)
        frame += b"1.16.0" + bytes(14)  # ver (20 bytes)
        result = parse_device_info(frame)
        assert result is not None
        assert result["fw_ver"] == 3
        assert result["max_contacts"] == 32  # 16 * 2
        assert result["ble_pin"] == 123456

    def test_too_short(self):
        assert parse_device_info(bytes(2)) is None


class TestParseBattAndStorage:
    def test_valid(self):
        frame = bytes([RESP_CODE_BATT_AND_STORAGE])
        frame += struct.pack("<H", 3800)  # battery mV
        frame += struct.pack("<I", 512)  # used_kb
        frame += struct.pack("<I", 4096)  # total_kb
        result = parse_batt_and_storage(frame)
        assert result["level"] == 3800
        assert result["used_kb"] == 512
        assert result["total_kb"] == 4096


class TestParseSentResponse:
    def test_valid(self):
        frame = bytes([RESP_CODE_SENT])
        frame += bytes([0])  # type
        frame += b"\xaa\xbb\xcc\xdd"  # expected_ack (4 bytes)
        frame += struct.pack("<I", 5000)  # suggested_timeout
        result = parse_sent_response(frame)
        assert result is not None
        assert result["expected_ack"] == b"\xaa\xbb\xcc\xdd"
        assert result["suggested_timeout"] == 5000


class TestParseAdvertPathResponse:
    def test_flood(self):
        code = PacketType.ADVERT_PATH.value
        frame = bytes([code])
        frame += struct.pack("<I", 1700000000)
        frame += bytes([255])  # flood
        result = parse_advert_path_response(frame)
        assert result is not None
        assert result["path_length"] == -1

    def test_with_hops(self):
        code = PacketType.ADVERT_PATH.value
        frame = bytes([code])
        frame += struct.pack("<I", 1700000000)
        frame += bytes([0b00_000010])  # 2 hops, hash_size 1
        frame += bytes([0xAA, 0xBB])  # path data
        result = parse_advert_path_response(frame)
        assert result["path_length"] == 2
        assert result["hops"] == ["AA", "BB"]


class TestParseCustomVars:
    def test_valid(self):
        frame = bytes([RESP_CODE_CUSTOM_VARS])
        frame += b"gps:1,debug:0\x00"
        result = parse_custom_vars(frame)
        assert result == {"gps": "1", "debug": "0"}

    def test_empty(self):
        frame = bytes([RESP_CODE_CUSTOM_VARS]) + b"\x00"
        assert parse_custom_vars(frame) == {}


class TestParseErrorResponse:
    def test_known_code(self):
        result = parse_error_response(bytes([RESP_CODE_ERR, 0x06]))
        assert result["code"] == 0x06
        assert result["message"] == "Illegal argument"

    def test_unknown_code(self):
        result = parse_error_response(bytes([RESP_CODE_ERR, 0xFF]))
        assert "Unknown" in result["message"]


# ─── Cayenne LPP ──────────────────────────────────────────────


class TestParseCayenneLPP:
    def test_temperature(self):
        data = bytes([1, LPP_TEMPERATURE]) + struct.pack(">h", 256)
        result = parse_cayenne_lpp(data)
        assert len(result) == 1
        assert result[0]["name"] == "Temperature"
        assert result[0]["value"] == 25.6
        assert result[0]["unit"] == "°C"

    def test_humidity(self):
        data = bytes([2, LPP_HUMIDITY, 140])
        result = parse_cayenne_lpp(data)
        assert result[0]["value"] == 70.0
        assert result[0]["unit"] == "%"

    def test_gps(self):
        lat_raw = int(50.1234 * 10000).to_bytes(3, "big", signed=True)
        lon_raw = int(14.4567 * 10000).to_bytes(3, "big", signed=True)
        alt_raw = int(250.0 * 100).to_bytes(3, "big", signed=True)
        data = bytes([3, LPP_GPS]) + lat_raw + lon_raw + alt_raw
        result = parse_cayenne_lpp(data)
        assert abs(result[0]["value"]["lat"] - 50.1234) < 0.001
        assert abs(result[0]["value"]["lon"] - 14.4567) < 0.001

    def test_empty_data(self):
        assert parse_cayenne_lpp(bytes([0, 0])) == []


# ─── Frame Builders ───────────────────────────────────────────


class TestBuildGetContactByKey:
    def test_layout(self):
        pub_key = bytes(range(32))
        frame = build_get_contact_by_key(pub_key)
        assert len(frame) == 33
        assert frame[0] == CMD_GET_CONTACT_BY_KEY
        assert frame[1:] == pub_key


class TestBuildSendTextMsg:
    def test_layout(self):
        pub_key = bytes(range(32))
        frame = build_send_text_msg(pub_key, "Hi", attempt=2, timestamp=1700000000)
        assert frame[0] == CMD_SEND_TXT_MSG
        assert frame[1] == TXT_TYPE_PLAIN
        assert frame[2] == 2  # attempt
        ts = struct.unpack_from("<I", frame, 3)[0]
        assert ts == 1700000000
        assert frame[7:13] == pub_key[:6]
        assert frame[13:15] == b"Hi"
        assert frame[15] == 0  # null terminator


class TestBuildSetAdvertLatlon:
    def test_encoding(self):
        frame = build_set_advert_latlon(50.123456, 14.456789)
        assert frame[0] == CMD_SET_ADVERT_LATLON
        lat = struct.unpack_from("<i", frame, 1)[0]
        lon = struct.unpack_from("<i", frame, 5)[0]
        assert lat == round(50.123456 * 1e6)
        assert lon == round(14.456789 * 1e6)


class TestParseChannelMessage:
    def _build_v4(self, sender_name="Bob", msg_text="hello", channel_idx=0, path_len_byte=0):
        frame = bytes([RESP_CODE_CHANNEL_MSG_RECV])
        frame += bytes([channel_idx])
        frame += bytes([path_len_byte])
        frame += bytes([TXT_TYPE_PLAIN])
        frame += struct.pack("<I", 1700000000)
        text = f"{sender_name}: {msg_text}" if sender_name else msg_text
        frame += text.encode("utf-8") + b"\x00"
        return frame

    def _build_v3(self, sender_name="Bob", msg_text="hello", channel_idx=0, snr_raw=20):
        frame = bytes([RESP_CODE_CHANNEL_MSG_RECV_V3])
        frame += bytes([snr_raw])  # SNR
        frame += bytes(2)  # reserved
        frame += bytes([channel_idx])
        frame += bytes([0])  # path_len_byte
        frame += bytes([TXT_TYPE_PLAIN])
        frame += struct.pack("<I", 1700000000)
        text = f"{sender_name}: {msg_text}" if sender_name else msg_text
        frame += text.encode("utf-8") + b"\x00"
        return frame

    def test_v4_basic(self):
        result = parse_channel_message(self._build_v4())
        assert result is not None
        assert result["type"] == "CHAN"
        assert result["sender_name"] == "Bob"
        assert result["msg_text"] == "hello"
        assert result["channel_idx"] == 0
        assert result["sender_timestamp"] == 1700000000

    def test_v3_includes_snr(self):
        result = parse_channel_message(self._build_v3(snr_raw=20))
        assert result is not None
        assert result["SNR"] == 5.0

    def test_too_short(self):
        assert parse_channel_message(bytes(5)) is None

    def test_wrong_code(self):
        assert parse_channel_message(bytes([0xFF] + [0] * 20)) is None

    def test_sender_name_with_colon_in_message(self):
        result = parse_channel_message(self._build_v4(msg_text="key: value"))
        assert result["sender_name"] == "Bob"
        assert result["msg_text"] == "key: value"

    def test_no_sender_name(self):
        frame = bytes([RESP_CODE_CHANNEL_MSG_RECV])
        frame += bytes([0])  # channel_idx
        frame += bytes([0])  # path_len_byte
        frame += bytes([TXT_TYPE_PLAIN])
        frame += struct.pack("<I", 1700000000)
        frame += b"no colon here\x00"
        result = parse_channel_message(frame)
        assert result["sender_name"] == ""
        assert result["msg_text"] == "no colon here"

    def test_path_length_parsed(self):
        result = parse_channel_message(self._build_v4(path_len_byte=3))
        assert result["path_len"] == 3
        assert result["path_hash_size"] == 1


class TestBuildLogout:
    def test_layout(self):
        key = bytes(range(32))
        frame = build_logout(key)
        assert len(frame) == 33
        assert frame[0] == CMD_LOGOUT
        assert frame[1:] == key


class TestBuildSetRadioTxPower:
    def test_positive(self):
        frame = build_set_radio_tx_power(20)
        assert len(frame) == 2
        assert frame[0] == CMD_SET_RADIO_TX_POWER
        assert frame[1] == 20

    def test_negative(self):
        frame = build_set_radio_tx_power(-9)
        assert len(frame) == 2
        assert frame[0] == CMD_SET_RADIO_TX_POWER
        assert frame[1] == (-9) & 0xFF  # 0xF7

    def test_zero(self):
        frame = build_set_radio_tx_power(0)
        assert frame == bytes([CMD_SET_RADIO_TX_POWER, 0])


class TestBuildGetAllowedRepeatFreq:
    def test_layout(self):
        frame = build_get_allowed_repeat_freq()
        assert len(frame) == 1
        assert frame[0] == CMD_GET_ALLOWED_REPEAT_FREQ


class TestParseAllowedRepeatFreq:
    def test_three_ranges(self):
        data = bytes([RESP_CODE_ALLOWED_REPEAT_FREQ])
        data += struct.pack("<II", 433000, 433000)
        data += struct.pack("<II", 869495, 869495)
        data += struct.pack("<II", 918000, 918000)
        result = parse_allowed_repeat_freq(data)
        assert len(result) == 3
        assert result[0] == (433000, 433000)
        assert result[1] == (869495, 869495)
        assert result[2] == (918000, 918000)

    def test_empty(self):
        data = bytes([RESP_CODE_ALLOWED_REPEAT_FREQ])
        assert parse_allowed_repeat_freq(data) == []

    def test_single_range(self):
        data = bytes([RESP_CODE_ALLOWED_REPEAT_FREQ])
        data += struct.pack("<II", 915000, 920000)
        result = parse_allowed_repeat_freq(data)
        assert result == [(915000, 920000)]

    def test_stops_at_zero_pair(self):
        data = bytes([RESP_CODE_ALLOWED_REPEAT_FREQ])
        data += struct.pack("<II", 433000, 433000)
        data += struct.pack("<II", 0, 0)
        data += struct.pack("<II", 918000, 918000)
        result = parse_allowed_repeat_freq(data)
        assert len(result) == 1


class TestTextByteLimits:
    def test_dm_limit(self):
        assert max_dm_text_bytes() == 156

    def test_channel_limit_short_name(self):
        limit = max_channel_text_bytes("Bob")
        assert limit == 155  # 160 - 3 - 2 = 155
