"""Scripted in-memory :class:`TelematicsProvider` for consumers' tests.

Construct with a queue of pre-built *batches* of
:class:`~mgf.tele.core.DecodedAvlRecord` (one batch = one decoded
packet); each :meth:`receive_packet` pops the next batch. When the
queue empties, it raises :class:`MockExhaustedError` so a test never
silently hangs. Command-channel methods raise ``NotImplementedError``
(matching a read-only consumer). Per rule **DP-02**, shipping a mock
beside the ABC is the seam's contract.
"""

from __future__ import annotations

from collections import deque
from datetime import UTC, datetime
from typing import TYPE_CHECKING

from mgf.tele.core._errors import TelematicsError
from mgf.tele.core._frames import ReceivedPacket
from mgf.tele.core._provider import (
    CommandAck,
    SessionHandle,
    TelematicsProvider,
    TrackerCommand,
)

if TYPE_CHECKING:
    import asyncio
    from collections.abc import Iterable

    from mgf.tele.core._frames import DecodedAvlRecord

__all__ = ["MockExhaustedError", "MockTelematicsProvider"]


class MockExhaustedError(TelematicsError):
    """Raised when a test consumes more batches than the mock was scripted
    with — a subclass of :class:`~mgf.tele.core.TelematicsError` so a test
    catching the category catches "ran out of script" too."""


class MockTelematicsProvider(TelematicsProvider):
    """Scripted provider. Construct with a sequence of record *batches*
    (each = one decoded packet); each ``receive_packet`` pops one off the
    front. ``open_session`` succeeds for any reader/writer (ignored);
    ``send_command`` raises (read-only)."""

    def __init__(
        self,
        batches: Iterable[list[DecodedAvlRecord]],
        *,
        imei: str = "test-imei",
        codec_id: int = 8,
    ) -> None:
        self._batches: deque[list[DecodedAvlRecord]] = deque(batches)
        self._open_sessions: dict[str, SessionHandle] = {}
        self._imei = imei
        self._codec_id = codec_id

    async def open_session(
        self,
        reader: asyncio.StreamReader,
        writer: asyncio.StreamWriter,
    ) -> SessionHandle:
        handle = SessionHandle(imei=self._imei, opened_at=datetime.now(UTC))
        self._open_sessions[self._imei] = handle
        return handle

    async def receive_packet(self, session: SessionHandle) -> ReceivedPacket:
        """Pop the next scripted batch into a ReceivedPacket envelope.

        ``raw_bytes`` is a placeholder (``b""``) — the mock carries no
        wire bytes by construction; tests asserting raw-byte handling use
        a real codec/provider's integration path.
        """
        if not self._batches:
            msg = f"mock provider exhausted on session {session.imei!r}"
            raise MockExhaustedError(msg)
        records = self._batches.popleft()
        return ReceivedPacket(
            raw_bytes=b"",
            transport="tcp",
            codec_id=self._codec_id,
            parse_status="ok",
            records=records,
        )

    async def send_command(self, session: SessionHandle, cmd: TrackerCommand) -> CommandAck:
        msg = (
            f"send_command not implemented in MockTelematicsProvider "
            f"(session {session.imei!r}, payload {len(cmd.payload)} bytes)"
        )
        raise NotImplementedError(msg)

    async def close_session(self, session: SessionHandle) -> None:
        # Idempotent — silently ignore unknown imei.
        self._open_sessions.pop(session.imei, None)
