#!/usr/bin/env python3
"""
can_mock.py — Mock CAN bus for unit/integration tests.

Implements the same minimal interface as python-can's Bus class so test code
can inject frames and capture outbound traffic without real hardware or a
running SocketCAN interface.

Interface
---------
  MockCANBus.send(msg, timeout=None)     — capture frame; if loopback, also enqueue for recv
  MockCANBus.recv(timeout=None)          — return next injected frame (or None on timeout)
  MockCANBus.inject(arb_id, data,        — queue a frame as if received from the bus
                    is_extended_id=False)
  MockCANBus.get_sent_frames()           — return copy of all sent frames list
  MockCANBus.reset()                     — clear all state
  MockCANBus.shutdown()                  — mark as shut down
"""

import queue
import threading
import time
from typing import List, Optional

try:
    import can
    _Message = can.Message
except ImportError:
    # Lightweight stand-in when python-can is not installed
    class _Message:  # type: ignore[no-redef]
        def __init__(
            self,
            arbitration_id: int = 0,
            data: bytes = b"",
            is_extended_id: bool = False,
            timestamp: Optional[float] = None,
        ) -> None:
            self.arbitration_id = arbitration_id
            self.data = bytearray(data)
            self.is_extended_id = is_extended_id
            self.timestamp = timestamp if timestamp is not None else time.monotonic()


class MockCANBus:
    """
    Thread-safe mock CAN bus.

    Parameters
    ----------
    loopback: bool
        When True, every frame passed to send() is also placed in the recv
        queue, simulating a loopback interface.
    filters: list[dict] or None
        Optional list of {"can_id": int, "can_mask": int} dicts.  Only frames
        matching at least one filter are returned by recv().  If None, all
        frames are returned.
    """

    def __init__(self, loopback: bool = False, filters=None) -> None:
        self._loopback = loopback
        self._filters = filters
        self._recv_q: queue.Queue = queue.Queue()
        self._sent: List[_Message] = []
        self._sent_lock = threading.Lock()
        self._shutdown = False

    # ------------------------------------------------------------------
    # python-can Bus interface (subset used by CanBridgeNode)
    # ------------------------------------------------------------------

    def send(self, msg, timeout: Optional[float] = None) -> None:
        """Record an outbound frame.  If loopback is enabled, also enqueue it."""
        if self._shutdown:
            raise RuntimeError("MockCANBus is shut down")
        # Normalise to _Message so callers can pass any object with the right attrs
        with self._sent_lock:
            self._sent.append(msg)
        if self._loopback:
            self._recv_q.put(msg)

    def recv(self, timeout: Optional[float] = None) -> Optional[_Message]:
        """
        Return the next injected frame.  Blocks up to timeout seconds.
        Returns None if nothing arrives within the timeout.
        """
        if self._shutdown:
            return None
        try:
            return self._recv_q.get(block=True, timeout=timeout)
        except queue.Empty:
            return None

    def shutdown(self) -> None:
        """Mark the bus as shut down; subsequent recv() returns None."""
        self._shutdown = True

    # ------------------------------------------------------------------
    # Test helpers
    # ------------------------------------------------------------------

    def inject(
        self,
        arbitration_id: int,
        data: bytes,
        is_extended_id: bool = False,
        timestamp: Optional[float] = None,
    ) -> None:
        """
        Inject a frame into the receive queue as if it arrived from the bus.

        Parameters
        ----------
        arbitration_id: CAN arbitration ID
        data: frame payload bytes
        is_extended_id: True for 29-bit extended frames (VESC style)
        timestamp: optional monotonic timestamp; defaults to time.monotonic()
        """
        msg = _Message(
            arbitration_id=arbitration_id,
            data=data,
            is_extended_id=is_extended_id,
            timestamp=timestamp if timestamp is not None else time.monotonic(),
        )
        self._recv_q.put(msg)

    def get_sent_frames(self) -> List[_Message]:
        """Return a snapshot of all frames sent through this bus."""
        with self._sent_lock:
            return list(self._sent)

    def get_sent_frames_by_id(self, arbitration_id: int) -> List[_Message]:
        """Return only sent frames whose arbitration_id matches."""
        with self._sent_lock:
            return [f for f in self._sent if f.arbitration_id == arbitration_id]

    def reset(self) -> None:
        """Clear all sent frames and drain the receive queue."""
        with self._sent_lock:
            self._sent.clear()
        while not self._recv_q.empty():
            try:
                self._recv_q.get_nowait()
            except queue.Empty:
                break
        self._shutdown = False

    def pending_recv(self) -> int:
        """Return the number of frames waiting in the receive queue."""
        return self._recv_q.qsize()

    # ------------------------------------------------------------------
    # Context manager support
    # ------------------------------------------------------------------

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.shutdown()
        return False
