"""AZ-275 — FakeFdrSink test double + production isolation guard."""

from __future__ import annotations

import ast
from pathlib import Path

import pytest

from gps_denied_onboard.fdr_client import (
    EnqueueResult,
    FdrClient,
    FdrRecord,
)
from gps_denied_onboard.fdr_client.fakes import FakeFdrSink
from gps_denied_onboard.fdr_client.records import OVERRUN_KIND, OVERRUN_PRODUCER_ID

_REPO_ROOT = Path(__file__).resolve().parents[2]
_SRC_ROOT = _REPO_ROOT / "src" / "gps_denied_onboard"


def _make_record(producer_id: str = "test.producer", frame_id: int = 0) -> FdrRecord:
    return FdrRecord(
        schema_version=1,
        ts="2026-05-11T00:00:00.000000Z",
        producer_id=producer_id,
        kind="log",
        payload={
            "level": "INFO",
            "component": producer_id,
            "frame_id": frame_id,
            "kind": "test.tick",
            "msg": "hello",
            "kv": {},
            "exc": None,
        },
    )


# ---------------------------------------------------------------------------
# AC-1: drop-in for FdrClient public surface.


def test_ac1_drop_in_for_fdr_client_public_surface() -> None:
    # Arrange
    sink = FakeFdrSink(producer_id="c1_vio")
    record = _make_record(producer_id="c1_vio")

    # Act
    result = sink.enqueue(record)
    popped = sink.pop_one()

    # Assert
    assert result == EnqueueResult.OK
    assert popped is record
    assert sink.producer_id == "c1_vio"


# ---------------------------------------------------------------------------
# AC-2: records reflects in-buffer state in FIFO order.


def test_ac2_records_reflects_in_buffer_state_fifo() -> None:
    # Arrange
    sink = FakeFdrSink(producer_id="test")
    records = [_make_record(frame_id=i) for i in range(3)]

    # Act
    for r in records:
        sink.enqueue(r)
    sink.pop_one()

    # Assert
    assert sink.records == records[1:]


# ---------------------------------------------------------------------------
# AC-3: all_records_ever captures dropped records too.


def test_ac3_all_records_ever_captures_dropped() -> None:
    # Arrange
    sink = FakeFdrSink(producer_id="test", capacity=2, with_default_overrun_policy=True)
    a = _make_record(frame_id=0)
    b = _make_record(frame_id=1)
    c = _make_record(frame_id=2)

    # Act
    sink.enqueue(a)
    sink.enqueue(b)
    sink.enqueue(c)

    # Assert
    # Buffer carries the newest 2 (b dropped first, c retried into a's slot)
    # plus the synthesised overrun record at the burst end.
    assert any(r.kind == OVERRUN_KIND for r in sink.records)
    user_records = [r for r in sink.records if r.kind != OVERRUN_KIND]
    assert c in user_records
    # all_records_ever includes a (which was dropped by drop-oldest) too.
    assert a in sink.all_records_ever
    assert b in sink.all_records_ever
    assert c in sink.all_records_ever


# ---------------------------------------------------------------------------
# AC-4: overrun policy parity with real FdrClient.


def test_ac4_overrun_policy_parity_with_real_client() -> None:
    # Arrange
    sink = FakeFdrSink(producer_id="c1_vio", capacity=4, with_default_overrun_policy=True)
    # Fill (capacity 4 holds 4 records before overrun starts).
    for i in range(4):
        sink.enqueue(_make_record(frame_id=i))

    # Act
    sink.enqueue(_make_record(frame_id=999))

    # Assert
    overruns = [r for r in sink.records if r.kind == OVERRUN_KIND]
    assert overruns, "fake must emit an overrun record when policy is wired"
    assert overruns[0].producer_id == OVERRUN_PRODUCER_ID
    assert overruns[0].payload["producer_id"] == "c1_vio"


# ---------------------------------------------------------------------------
# AC-5: pytest fixture available.


def test_ac5_fixture_provides_clean_sink_per_test(fake_fdr_sink: FakeFdrSink) -> None:
    # Arrange / Act / Assert
    assert isinstance(fake_fdr_sink, FakeFdrSink)
    assert fake_fdr_sink.records == []
    fake_fdr_sink.enqueue(_make_record())
    assert len(fake_fdr_sink.records) == 1


# ---------------------------------------------------------------------------
# AC-6: producer_id preserved on round-trip.


def test_ac6_producer_id_preserved_on_roundtrip() -> None:
    # Arrange
    sink = FakeFdrSink(producer_id="c2_vpr")
    record = _make_record(producer_id="c2_vpr")

    # Act
    sink.enqueue(record)
    popped = sink.pop_one()

    # Assert
    assert popped is not None
    assert popped.producer_id == "c2_vpr"


# ---------------------------------------------------------------------------
# Empty producer_id rejected (parity with real client).


def test_empty_producer_id_raises_value_error() -> None:
    # Arrange / Act / Assert
    with pytest.raises(ValueError, match="producer_id"):
        FakeFdrSink(producer_id="")


# ---------------------------------------------------------------------------
# Production isolation: no `src/gps_denied_onboard/**.py` imports the fakes.


def test_production_does_not_import_fakes() -> None:
    # Arrange
    violations: list[str] = []
    target_module_prefix = "gps_denied_onboard.fdr_client.fakes"

    # Act
    for path in _SRC_ROOT.rglob("*.py"):
        if path.name == "fakes.py":
            continue  # the module itself
        try:
            tree = ast.parse(path.read_text(encoding="utf-8"))
        except SyntaxError:
            continue
        for node in ast.walk(tree):
            if isinstance(node, ast.ImportFrom) and node.module:
                if node.module.startswith(target_module_prefix):
                    violations.append(str(path.relative_to(_REPO_ROOT)))
            elif isinstance(node, ast.Import):
                for alias in node.names:
                    if alias.name.startswith(target_module_prefix):
                        violations.append(str(path.relative_to(_REPO_ROOT)))

    # Assert
    assert not violations, (
        "Production code must not import gps_denied_onboard.fdr_client.fakes. "
        f"Violations: {violations}"
    )


# ---------------------------------------------------------------------------
# Contract parity: every public method on FdrClient also exists on FakeFdrSink.


def test_contract_parity_public_methods() -> None:
    # Arrange
    public_attrs = {
        name
        for name in dir(FdrClient)
        if not name.startswith("_") and callable(getattr(FdrClient, name))
    }
    public_attrs |= {"producer_id", "on_overrun"}  # property pair

    # Act
    missing = [name for name in public_attrs if not hasattr(FakeFdrSink, name)]

    # Assert
    assert not missing, f"FakeFdrSink missing public surface: {missing}"
