from __future__ import annotations

import queue
import time
from pathlib import Path
from typing import List

import pytest

from pymulator.core.addressing import Addressing
from pymulator.core.bus import CANBusEmulation, CANBusParticipant
from pymulator.core.can import CANFrame, NullInterface
from pymulator.core.ecu import ECU
from pymulator.core.isotp import ISOTPTransceiver, ISOTPAction
from pymulator.core.message import Message
from pymulator.specs.parser import load_command_handler

FIXTURES = Path(__file__).parent / "fixtures"


def load_handler(name: str):
    return load_command_handler(str(FIXTURES / name))


def make_message(arbitration_id: int, payload: List[int]) -> Message:
    return Message(addressing=Addressing.id_only(arbitration_id), bytes=payload)


class TestClient(CANBusParticipant):
    def __init__(self) -> None:
        super().__init__()
        self.frames: "queue.Queue[CANFrame]" = queue.Queue()

    @property
    def arbitration_ids(self):
        return []

    def feed(self, frame: CANFrame, bus: CANBusEmulation) -> None:
        self.frames.put(frame)


@pytest.fixture()
def bus_and_client():
    bus = CANBusEmulation(NullInterface())
    client = TestClient()
    bus.attach(client)
    yield bus, client
    bus.shutdown()


def attach_ecu(bus: CANBusEmulation, name: str, handler) -> ECU:
    ecu = ECU(name, [handler])
    bus.attach(ecu)
    return ecu


def send_isotp_request(bus: CANBusEmulation, client: TestClient, arbitration_id: int, payload: List[int]) -> None:
    tx = ISOTPTransceiver()
    action = tx.write(payload)
    if action.kind != "write_frames" or not action.frames:
        raise AssertionError("unexpected ISOTP action when sending request")
    for frame_bytes in action.frames:
        frame = CANFrame(arbitration_id=arbitration_id, data=frame_bytes, dlc=len(frame_bytes))
        bus.write(frame, originator=client)


def collect_isotp_payload(
    bus: CANBusEmulation,
    client: TestClient,
    request_id: int,
    timeout: float = 1.0,
) -> List[int]:
    rx = ISOTPTransceiver()
    deadline = time.time() + timeout
    while time.time() < deadline:
        try:
            frame = client.frames.get(timeout=0.05)
        except queue.Empty:
            continue
        payload = list(frame.data[: frame.dlc])
        action = rx.did_read(payload)
        if action.kind == "write_frames":
            for fc_bytes in action.frames or []:
                fc_frame = CANFrame(arbitration_id=request_id, data=fc_bytes, dlc=len(fc_bytes))
                bus.write(fc_frame, originator=client)
        elif action.kind == "process":
            return action.bytes or []
    raise AssertionError("Timed out waiting for ISOTP payload")
