"""``FakeFdrSink`` test double for ``FdrClient`` (AZ-275 / E-CC-FDR-CLIENT).

Drop-in replacement that conforms to ``fdr_client_protocol`` v1.0.0's
public surface. Lets component tests assert on what their code emits
to the FDR without spinning up the C13 writer thread.

Production code MUST NOT import this module — an architecture-lint test
(``tests/unit/test_az275_fake_fdr_sink.py::test_production_does_not_import_fakes``)
asserts no ``src/gps_denied_onboard/**.py`` imports from this module.
"""

from __future__ import annotations

from collections.abc import Callable
from typing import Final

from gps_denied_onboard.fdr_client.records import FdrRecord

__all__ = ["FakeFdrSink"]


# Mirror of ``EnqueueResult`` so the fake doesn't need to import the
# production client (avoids forcing the queue module into test paths
# that only need the fake).
class _FakeEnqueueResult:
    OK: Final[str] = "ok"
    OVERRUN: Final[str] = "overrun"


class FakeFdrSink:
    """List-backed in-memory test double for :class:`FdrClient`.

    Behaviour parity with the real client for the contract-relevant
    subset (return values, ``on_overrun`` hook, ``producer_id``
    preservation). Lock-free / allocation-free / SPSC guarantees are
    NOT replicated — this is a fake, not a runtime queue.
    """

    def __init__(
        self,
        producer_id: str,
        capacity: int | None = None,
        *,
        with_default_overrun_policy: bool = False,
    ) -> None:
        if not isinstance(producer_id, str) or not producer_id:
            raise ValueError(
                f"FakeFdrSink.producer_id must be a non-empty string; got {producer_id!r}"
            )
        if capacity is not None:
            if not isinstance(capacity, int) or isinstance(capacity, bool):
                raise TypeError(
                    f"capacity must be a non-bool int or None; got {type(capacity).__name__}"
                )
            if capacity <= 0:
                raise ValueError(f"capacity must be > 0 when set; got {capacity}")

        self._producer_id: str = producer_id
        self._capacity: int | None = capacity
        self._buffer: list[FdrRecord] = []
        self._all: list[FdrRecord] = []
        self._on_overrun: Callable[[FdrRecord], None] | None = None

        if with_default_overrun_policy:
            from gps_denied_onboard.fdr_client.overrun_policy import default_overrun_policy

            # default_overrun_policy expects an ``FdrClient``; the fake
            # exposes the same ``drop_oldest_for_overrun`` + ``producer_id``
            # + ``_buffer.push`` surface, so a duck-typed adapter works.
            self._on_overrun = default_overrun_policy(self._policy_adapter())  # type: ignore[arg-type]

    @property
    def producer_id(self) -> str:
        return self._producer_id

    @property
    def on_overrun(self) -> Callable[[FdrRecord], None] | None:
        return self._on_overrun

    @on_overrun.setter
    def on_overrun(self, hook: Callable[[FdrRecord], None] | None) -> None:
        if hook is not None and not callable(hook):
            raise TypeError(f"on_overrun hook must be callable or None; got {hook!r}")
        self._on_overrun = hook

    @property
    def records(self) -> list[FdrRecord]:
        """In-buffer records, FIFO order (newest at the end)."""
        return list(self._buffer)

    @property
    def all_records_ever(self) -> list[FdrRecord]:
        """Every record ever enqueued, INCLUDING records dropped by the policy."""
        return list(self._all)

    def enqueue(self, record: FdrRecord) -> str:
        """Append ``record`` to the buffer; invoke ``on_overrun`` on overflow."""
        self._all.append(record)
        if self._capacity is None or len(self._buffer) < self._capacity:
            self._buffer.append(record)
            return _FakeEnqueueResult.OK
        hook = self._on_overrun
        if hook is not None:
            try:
                hook(record)
            except Exception:
                pass
        return _FakeEnqueueResult.OVERRUN

    def pop_one(self) -> FdrRecord | None:
        if not self._buffer:
            return None
        return self._buffer.pop(0)

    def drain(self, max_records: int) -> list[FdrRecord]:
        if max_records < 0:
            raise ValueError(f"max_records must be >= 0; got {max_records}")
        if max_records == 0:
            return []
        result = self._buffer[:max_records]
        del self._buffer[:max_records]
        return result

    def flush(self) -> None:
        self._buffer.clear()

    def drop_oldest_for_overrun(self) -> FdrRecord | None:
        """Drop the oldest in-buffer record (used by ``default_overrun_policy``)."""
        if not self._buffer:
            return None
        return self._buffer.pop(0)

    def _policy_adapter(self) -> _PolicyAdapter:
        """Internal duck-typed adapter exposing the policy's expected surface."""
        return _PolicyAdapter(self)


class _PolicyAdapter:
    """Bridges :class:`FakeFdrSink` to :func:`default_overrun_policy`'s API.

    The production policy expects to call ``client.drop_oldest_for_overrun()``
    and ``client._buffer.push(record)``. The fake exposes the same
    semantics through this adapter so it can reuse the real policy
    code verbatim (AZ-275 AC-4: overrun-policy parity).
    """

    def __init__(self, sink: FakeFdrSink) -> None:
        self._sink = sink

    @property
    def producer_id(self) -> str:
        return self._sink.producer_id

    def drop_oldest_for_overrun(self) -> FdrRecord | None:
        return self._sink.drop_oldest_for_overrun()

    @property
    def _buffer(self) -> _FakeBufferShim:
        return _FakeBufferShim(self._sink)


class _FakeBufferShim:
    """Shim that lets the policy call ``client._buffer.push(record)``."""

    def __init__(self, sink: FakeFdrSink) -> None:
        self._sink = sink

    def push(self, record: FdrRecord) -> bool:
        if self._sink._capacity is None or len(self._sink._buffer) < self._sink._capacity:
            self._sink._buffer.append(record)
            self._sink._all.append(record)
            return True
        return False
