from __future__ import annotations

from pymulator.core.bus import CANBusEmulation
from pymulator.core.can import CANFrame, NullInterface
from pymulator.ecuconnect.protocol import Arbitration, ChannelProtocol, PDU, PDUType
from pymulator.ecuconnect.server import ECUconnectServer, _extract_firmware_version_from_image


def _capture_info_lines(server: ECUconnectServer) -> list[str]:
    sent: list[PDU] = []
    server._send_pdu = sent.append  # type: ignore[method-assign]
    server._handle_pdu(PDU(PDUType.request_info, []))
    assert sent
    info = sent[0]
    assert info.pdu_type == PDUType.info
    return bytes(info.payload).decode("utf-8").splitlines()


def test_request_info_uses_default_identity_fields() -> None:
    bus = CANBusEmulation(NullInterface())
    try:
        server = ECUconnectServer(
            bus,
            host="127.0.0.1",
            port=0,
            update_prepare_delay_seconds=0.0,
            update_chunk_delay_seconds=0.0,
            update_commit_delay_seconds=0.0,
        )
        lines = _capture_info_lines(server)
        assert lines[2] == "PVIRT"
        assert lines[4] == "0.9.999"
    finally:
        bus.shutdown()


def test_request_info_uses_configured_identity_fields() -> None:
    bus = CANBusEmulation(NullInterface())
    try:
        server = ECUconnectServer(
            bus,
            host="127.0.0.1",
            port=0,
            firmware_version="1.23.4",
            hardware_revision="HW-REV-B",
            update_prepare_delay_seconds=0.0,
            update_chunk_delay_seconds=0.0,
            update_commit_delay_seconds=0.0,
        )
        lines = _capture_info_lines(server)
        assert lines[2] == "HW-REV-B"
        assert lines[4] == "1.23.4"
    finally:
        bus.shutdown()


def test_update_pdus_apply_firmware_version_on_reset() -> None:
    bus = CANBusEmulation(NullInterface())
    try:
        server = ECUconnectServer(
            bus,
            host="127.0.0.1",
            port=0,
            firmware_version="0.9.999",
            update_prepare_delay_seconds=0.0,
            update_chunk_delay_seconds=0.0,
            update_commit_delay_seconds=0.0,
        )
        sent: list[PDU] = []
        server._send_pdu = sent.append  # type: ignore[method-assign]

        blob = bytearray(128)
        blob[0x30:0x3A] = b"1.2.3.456\x00"
        blob[0x50:0x64] = b"ECUconnectOS-ecuconnect\x00"

        server._handle_pdu(PDU(PDUType.prepare_for_update, []))
        server._handle_pdu(PDU(PDUType.send_update_data, list(blob[:64])))
        server._handle_pdu(PDU(PDUType.send_update_data, list(blob[64:])))
        server._handle_pdu(PDU(PDUType.commit_update, []))
        server._handle_pdu(PDU(PDUType.reset, []))

        assert [pdu.pdu_type for pdu in sent] == [
            PDUType.ok,
            PDUType.ok,
            PDUType.ok,
            PDUType.ok,
            PDUType.ok,
        ]
        assert server.firmware == "1.2.3.456"
        assert server._update_started is False
        assert server._update_committed is False
    finally:
        bus.shutdown()


def test_update_pdus_apply_hardware_like_wait_states() -> None:
    bus = CANBusEmulation(NullInterface())
    try:
        server = ECUconnectServer(
            bus,
            host="127.0.0.1",
            port=0,
            update_prepare_delay_seconds=3.0,
            update_chunk_delay_seconds=0.05,
            update_commit_delay_seconds=2.0,
        )
        sent: list[PDU] = []
        observed_delays: list[float] = []
        server._send_pdu = sent.append  # type: ignore[method-assign]
        server._delay_update_stage = observed_delays.append  # type: ignore[method-assign]

        server._handle_pdu(PDU(PDUType.prepare_for_update, []))
        server._handle_pdu(PDU(PDUType.send_update_data, [0xAA]))
        server._handle_pdu(PDU(PDUType.commit_update, []))

        assert observed_delays == [3.0, 0.05, 2.0]
        assert [pdu.pdu_type for pdu in sent] == [PDUType.ok, PDUType.ok, PDUType.ok]
    finally:
        bus.shutdown()


def test_update_reset_emits_reported_identity_change_event() -> None:
    bus = CANBusEmulation(NullInterface())
    events: list[dict] = []
    try:
        server = ECUconnectServer(
            bus,
            host="127.0.0.1",
            port=0,
            firmware_version="0.9.999",
            update_prepare_delay_seconds=0.0,
            update_chunk_delay_seconds=0.0,
            update_commit_delay_seconds=0.0,
            event_sink=events.append,
        )
        sent: list[PDU] = []
        server._send_pdu = sent.append  # type: ignore[method-assign]

        blob = bytearray(128)
        blob[0x30:0x3A] = b"1.2.3.456\x00"
        blob[0x50:0x64] = b"ECUconnectOS-ecuconnect\x00"

        server._handle_pdu(PDU(PDUType.prepare_for_update, []))
        server._handle_pdu(PDU(PDUType.send_update_data, list(blob)))
        server._handle_pdu(PDU(PDUType.commit_update, []))
        server._handle_pdu(PDU(PDUType.reset, []))

        reported_identity_events = [event for event in events if event.get("kind") == "reported_identity_changed"]
        assert len(reported_identity_events) == 1
        identity_event = reported_identity_events[0]
        assert identity_event.get("firmware") == "1.2.3.456"
        assert identity_event.get("hardware") == "PVIRT"
        assert identity_event.get("source") == "firmware_update"
    finally:
        bus.shutdown()


def test_update_pdu_order_is_enforced() -> None:
    bus = CANBusEmulation(NullInterface())
    try:
        server = ECUconnectServer(
            bus,
            host="127.0.0.1",
            port=0,
            update_prepare_delay_seconds=0.0,
            update_chunk_delay_seconds=0.0,
            update_commit_delay_seconds=0.0,
        )
        sent: list[PDU] = []
        server._send_pdu = sent.append  # type: ignore[method-assign]

        server._handle_pdu(PDU(PDUType.send_update_data, [0x01, 0x02]))
        server._handle_pdu(PDU(PDUType.commit_update, []))
        server._handle_pdu(PDU(PDUType.prepare_for_update, []))
        server._handle_pdu(PDU(PDUType.commit_update, []))
        server._handle_pdu(PDU(PDUType.send_update_data, [0xAA]))

        assert [pdu.pdu_type for pdu in sent] == [
            PDUType.error_invalid_command,
            PDUType.error_invalid_command,
            PDUType.ok,
            PDUType.ok,
            PDUType.error_invalid_command,
        ]
    finally:
        bus.shutdown()


def test_extract_firmware_version_from_uploaded_image_header() -> None:
    blob = bytearray(256)
    blob[0x30:0x3A] = b"0.6.5.213\x00"
    blob[0x60:0x68] = b"0.0.0.0\x00"
    assert _extract_firmware_version_from_image(bytes(blob)) == "0.6.5.213"


def _u32(value: int) -> list[int]:
    return [(value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF]


def _open_fd_channel(
    server: ECUconnectServer,
    protocol: ChannelProtocol,
    bitrate: int = 500000,
    data_bitrate: int = 2000000,
) -> int:
    sent: list[PDU] = []
    server._send_pdu = sent.append  # type: ignore[method-assign]
    payload = [int(protocol)] + _u32(bitrate) + _u32(data_bitrate) + [0x00]
    server._handle_pdu(PDU(PDUType.open_fd_channel, payload))
    assert sent
    assert sent[-1].pdu_type == PDUType.channel_opened
    return sent[-1].payload[0]


def _set_arbitration(server: ECUconnectServer, channel: int, request: int = 0x7E0, reply: int = 0x7E8) -> None:
    arbitration = Arbitration(
        request=request,
        request_extension=0x00,
        reply_pattern=reply,
        reply_mask=0x7FF,
        reply_extension=0x00,
    )
    sent: list[PDU] = []
    server._send_pdu = sent.append  # type: ignore[method-assign]
    server._handle_pdu(PDU(PDUType.set_arbitration, [channel] + arbitration.to_payload()))
    assert sent
    assert sent[-1].pdu_type == PDUType.ok


def test_open_fd_channel_allocates_fd_metadata() -> None:
    bus = CANBusEmulation(NullInterface())
    try:
        server = ECUconnectServer(bus, host="127.0.0.1", port=0)
        channel = _open_fd_channel(server, ChannelProtocol.raw_fd)
        state = server._channels[channel]
        assert state.protocol == int(ChannelProtocol.raw_fd)
        assert state.is_fd is True
        assert state.bitrate == 500000
        assert state.data_bitrate == 2000000
    finally:
        bus.shutdown()


def test_send_raw_fd_allows_64_byte_payload() -> None:
    bus = CANBusEmulation(NullInterface())
    writes: list[CANFrame] = []
    bus.write = lambda frame, originator: writes.append(frame)  # type: ignore[method-assign]
    try:
        server = ECUconnectServer(bus, host="127.0.0.1", port=0)
        channel = _open_fd_channel(server, ChannelProtocol.raw_fd)
        _set_arbitration(server, channel)

        sent: list[PDU] = []
        server._send_pdu = sent.append  # type: ignore[method-assign]
        server._handle_pdu(PDU(PDUType.send, [channel] + list(range(64))))

        assert sent
        assert sent[-1].pdu_type == PDUType.ok
        assert len(writes) == 1
        frame = writes[0]
        assert frame.is_fd is True
        assert frame.dlc == 64
        assert frame.data == list(range(64))
    finally:
        bus.shutdown()


def test_isotp_fd_channel_sends_fd_frames_and_accepts_flow_control() -> None:
    bus = CANBusEmulation(NullInterface())
    writes: list[CANFrame] = []
    bus.write = lambda frame, originator: writes.append(frame)  # type: ignore[method-assign]
    try:
        server = ECUconnectServer(bus, host="127.0.0.1", port=0)
        channel = _open_fd_channel(server, ChannelProtocol.isotp_fd)
        _set_arbitration(server, channel, request=0x7E0, reply=0x7E8)

        sent: list[PDU] = []
        server._send_pdu = sent.append  # type: ignore[method-assign]
        server._handle_pdu(PDU(PDUType.send, [channel] + list(range(80))))
        assert sent
        assert sent[-1].pdu_type == PDUType.ok
        assert writes
        assert writes[0].is_fd is True

        fc = CANFrame(
            arbitration_id=0x7E8,
            data=[0x30, 0x00, 0x00],
            dlc=3,
            is_fd=True,
        )
        server.handle_incoming_frame(fc)
        assert len(writes) > 1
        assert all(frame.is_fd for frame in writes)
    finally:
        bus.shutdown()
