"""Unit tests for VESC CAN telemetry — protocol parsing and state logic.

These tests cover pure functions in vesc_can_protocol and do not require
ROS2, python-can, or a live CAN bus.
"""

import struct
import time
import pytest

from saltybot_vesc_telemetry.vesc_can_protocol import (
    CAN_PACKET_STATUS,
    CAN_PACKET_STATUS_4,
    CAN_PACKET_STATUS_5,
    CAN_PACKET_PROCESS_SHORT_BUFFER,
    COMM_GET_VALUES,
    FAULT_CODE_NONE,
    FAULT_CODE_ABS_OVER_CURRENT,
    FAULT_CODE_OVER_TEMP_FET,
    FAULT_CODE_OVER_TEMP_MOTOR,
    FAULT_NAMES,
    VescState,
    make_get_values_request,
    parse_status,
    parse_status_4,
    parse_status_5,
)


# ---------------------------------------------------------------------------
# parse_status (CAN_PACKET_STATUS = 9)
# ---------------------------------------------------------------------------

class TestParseStatus:
    def test_forward_rpm(self):
        # 1000 RPM, 5.0 A, 0.300 duty
        data = struct.pack(">ihh", 1000, 50, 300)
        r = parse_status(data)
        assert r is not None
        assert r["rpm"]        == 1000
        assert r["current_a"]  == pytest.approx(5.0)
        assert r["duty_cycle"] == pytest.approx(0.300)

    def test_reverse_rpm(self):
        data = struct.pack(">ihh", -3000, -80, -500)
        r = parse_status(data)
        assert r["rpm"]        == -3000
        assert r["current_a"]  == pytest.approx(-8.0)
        assert r["duty_cycle"] == pytest.approx(-0.5)

    def test_zero(self):
        data = struct.pack(">ihh", 0, 0, 0)
        r = parse_status(data)
        assert r["rpm"]        == 0
        assert r["current_a"]  == pytest.approx(0.0)
        assert r["duty_cycle"] == pytest.approx(0.0)

    def test_max_duty(self):
        # duty 1.0 → encoded as 1000
        data = struct.pack(">ihh", 50000, 200, 1000)
        r = parse_status(data)
        assert r["duty_cycle"] == pytest.approx(1.0)

    def test_too_short_returns_none(self):
        assert parse_status(b"\x00\x01\x02") is None

    def test_exact_minimum_length(self):
        data = struct.pack(">ihh", 42, 10, 100)
        assert parse_status(data) is not None


# ---------------------------------------------------------------------------
# parse_status_4 (CAN_PACKET_STATUS_4 = 16)
# ---------------------------------------------------------------------------

class TestParseStatus4:
    def _make(self, temp_fet, temp_motor, current_in, pid_pos=0):
        return struct.pack(
            ">hhhh",
            int(temp_fet * 10),
            int(temp_motor * 10),
            int(current_in * 10),
            pid_pos,
        )

    def test_normal_temps(self):
        data = self._make(45.5, 62.3, 12.0)
        r = parse_status_4(data)
        assert r is not None
        assert r["temp_fet_c"]   == pytest.approx(45.5)
        assert r["temp_motor_c"] == pytest.approx(62.3)
        assert r["current_in_a"] == pytest.approx(12.0)

    def test_negative_current_in(self):
        data = self._make(30.0, 40.0, -5.0)
        r = parse_status_4(data)
        assert r["current_in_a"] == pytest.approx(-5.0)

    def test_too_short_returns_none(self):
        assert parse_status_4(b"\x00" * 7) is None

    def test_zero_values(self):
        data = self._make(0, 0, 0)
        r = parse_status_4(data)
        assert r["temp_fet_c"]   == pytest.approx(0.0)
        assert r["temp_motor_c"] == pytest.approx(0.0)
        assert r["current_in_a"] == pytest.approx(0.0)


# ---------------------------------------------------------------------------
# parse_status_5 (CAN_PACKET_STATUS_5 = 27)
# ---------------------------------------------------------------------------

class TestParseStatus5:
    def _make(self, voltage_v, tacho=0):
        return struct.pack(">ih", tacho, int(voltage_v * 10))

    def test_nominal_voltage(self):
        data = self._make(25.2)
        r = parse_status_5(data)
        assert r is not None
        assert r["voltage_v"] == pytest.approx(25.2)

    def test_low_voltage(self):
        data = self._make(18.6)
        r = parse_status_5(data)
        assert r["voltage_v"] == pytest.approx(18.6)

    def test_too_short_returns_none(self):
        assert parse_status_5(b"\x00\x01\x02\x03\x04") is None

    def test_with_tachometer(self):
        data = struct.pack(">ih", 123456, 252)
        r = parse_status_5(data)
        assert r["voltage_v"] == pytest.approx(25.2)


# ---------------------------------------------------------------------------
# make_get_values_request
# ---------------------------------------------------------------------------

class TestMakeGetValuesRequest:
    def test_arb_id_encodes_packet_type_and_target(self):
        arb_id, payload = make_get_values_request(sender_id=127, target_id=56)
        expected_arb = (CAN_PACKET_PROCESS_SHORT_BUFFER << 8) | 56
        assert arb_id == expected_arb

    def test_payload_contains_sender_command(self):
        _, payload = make_get_values_request(sender_id=127, target_id=56)
        assert len(payload) == 3
        assert payload[0] == 127          # sender_id
        assert payload[1] == 0x00         # send_mode
        assert payload[2] == COMM_GET_VALUES

    def test_right_vesc(self):
        arb_id, payload = make_get_values_request(sender_id=127, target_id=68)
        assert (arb_id & 0xFF) == 68
        assert payload[2] == COMM_GET_VALUES

    def test_sender_id_in_payload(self):
        _, payload = make_get_values_request(sender_id=42, target_id=56)
        assert payload[0] == 42

    def test_arb_id_is_extended(self):
        # Extended IDs can exceed 0x7FF (11-bit limit)
        arb_id, _ = make_get_values_request(127, 56)
        assert arb_id > 0x7FF


# ---------------------------------------------------------------------------
# VescState properties
# ---------------------------------------------------------------------------

class TestVescState:
    def test_defaults(self):
        s = VescState(can_id=56)
        assert s.rpm           == 0
        assert s.current_a     == pytest.approx(0.0)
        assert s.voltage_v     == pytest.approx(0.0)
        assert s.fault_code    == FAULT_CODE_NONE
        assert s.has_fault     is False

    def test_is_alive_fresh(self):
        s = VescState(can_id=56)
        s.last_status_ts = time.monotonic()
        assert s.is_alive is True

    def test_is_alive_stale(self):
        s = VescState(can_id=56)
        s.last_status_ts = time.monotonic() - 2.0  # 2 s ago > 1 s timeout
        assert s.is_alive is False

    def test_is_alive_never_received(self):
        s = VescState(can_id=56)
        # last_status_ts defaults to 0.0 — monotonic() will be >> 1 s
        assert s.is_alive is False

    def test_has_fault_true(self):
        s = VescState(can_id=56)
        s.fault_code = FAULT_CODE_ABS_OVER_CURRENT
        assert s.has_fault is True

    def test_has_fault_false_on_none(self):
        s = VescState(can_id=56)
        s.fault_code = FAULT_CODE_NONE
        assert s.has_fault is False

    def test_fault_name_known(self):
        s = VescState(can_id=56)
        s.fault_code = FAULT_CODE_OVER_TEMP_FET
        assert s.fault_name == "OVER_TEMP_FET"

    def test_fault_name_unknown(self):
        s = VescState(can_id=56)
        s.fault_code = 99
        assert "UNKNOWN" in s.fault_name

    def test_fault_name_none(self):
        s = VescState(can_id=56)
        assert s.fault_name == "NONE"


# ---------------------------------------------------------------------------
# FAULT_NAMES completeness
# ---------------------------------------------------------------------------

class TestFaultNames:
    def test_all_common_codes_named(self):
        for code in (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10):
            assert code in FAULT_NAMES, f"fault code {code} missing from FAULT_NAMES"

    def test_none_is_zero(self):
        assert FAULT_NAMES[0] == "NONE"

    def test_overcurrent_code(self):
        assert FAULT_NAMES[FAULT_CODE_ABS_OVER_CURRENT] == "ABS_OVER_CURRENT"


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
