"""AZ-295 — RecordKindPolicy: forbidden-kind + thumbnail rate-cap gates."""

from __future__ import annotations

import time
from unittest import mock

import pytest

from gps_denied_onboard.components.c13_fdr import (
    GateDecision,
    RawFrameWriteForbiddenError,
    make_record_kind_policy,
)
from gps_denied_onboard.config import (
    DEFAULT_FORBIDDEN_RECORD_KINDS,
    ConfigError,
    RecordKindPolicyConfig,
)
from gps_denied_onboard.fdr_client.records import OVERRUN_KIND, FdrRecord

_TS = "2026-05-11T00:00:00.000000Z"


def _rec(kind: str, *, producer_id: str = "c1_vio", payload: dict | None = None) -> FdrRecord:
    return FdrRecord(
        schema_version=1,
        ts=_TS,
        producer_id=producer_id,
        kind=kind,
        payload=payload or {},
    )


def test_ac1_enforce_or_raise_rejects_raw_nav_frame() -> None:
    # Arrange
    policy = make_record_kind_policy(RecordKindPolicyConfig())

    # Act + Assert
    with pytest.raises(RawFrameWriteForbiddenError) as ei:
        policy.enforce_or_raise(_rec("raw_nav_frame", producer_id="c1_vio"))
    msg = str(ei.value)
    assert "raw_nav_frame" in msg
    assert "c1_vio" in msg


def test_ac2_enforce_or_raise_rejects_raw_ai_cam_frame() -> None:
    # Arrange
    policy = make_record_kind_policy(RecordKindPolicyConfig())

    # Act + Assert
    with pytest.raises(RawFrameWriteForbiddenError):
        policy.enforce_or_raise(_rec("raw_ai_cam_frame"))


def test_ac3_enforce_or_raise_allows_failed_tile_thumbnail() -> None:
    # Arrange
    policy = make_record_kind_policy(RecordKindPolicyConfig())

    # Act
    policy.enforce_or_raise(
        _rec(
            "failed_tile_thumbnail",
            payload={"frame_id": 1, "tile_id": "x", "jpeg_bytes_b64": "AAAA"},
        )
    )


def test_ac4_gate_admits_first_thumbnail_in_fresh_window() -> None:
    # Arrange
    policy = make_record_kind_policy(RecordKindPolicyConfig(failed_tile_thumbnail_max_hz=0.1))

    # Act + Assert
    assert policy.gate_for_writer(_rec("failed_tile_thumbnail")) is GateDecision.ENQUEUE


def test_ac5_gate_drops_overflow_then_emits_coalesced_overrun() -> None:
    # Arrange
    policy = make_record_kind_policy(RecordKindPolicyConfig(failed_tile_thumbnail_max_hz=0.1))

    # Act — 5 thumbnails in immediate succession (well within 10 s window)
    decisions = [
        policy.gate_for_writer(_rec("failed_tile_thumbnail", producer_id="c6_tile_cache"))
        for _ in range(5)
    ]

    # Assert — first ENQUEUE, next 4 DROP
    assert decisions[0] is GateDecision.ENQUEUE
    assert decisions[1:] == [GateDecision.DROP] * 4

    overrun = policy.drain_pending_overrun()
    assert overrun is not None
    assert overrun.kind == OVERRUN_KIND
    assert overrun.payload["dropped_count"] == 4
    assert overrun.payload["producer_id"] == "c6_tile_cache"

    # Second drain is empty (counter cleared after drain).
    assert policy.drain_pending_overrun() is None


def test_ac6_forbidden_set_rejects_removal_of_defaults() -> None:
    # Arrange + Act + Assert
    with pytest.raises(ConfigError, match=r"raw_nav_frame|raw_ai_cam_frame"):
        RecordKindPolicyConfig(forbidden_record_kinds=frozenset())


def test_ac7_forbidden_set_allows_additions() -> None:
    # Arrange
    extra = DEFAULT_FORBIDDEN_RECORD_KINDS | {"raw_thermal_frame"}
    policy = make_record_kind_policy(
        RecordKindPolicyConfig(forbidden_record_kinds=frozenset(extra))
    )

    # Act + Assert
    for kind in extra:
        with pytest.raises(RawFrameWriteForbiddenError):
            policy.enforce_or_raise(_rec(kind))


def test_ac8_zero_hz_rejected_at_config_validation() -> None:
    # Arrange + Act + Assert
    with pytest.raises(ConfigError, match=r"failed_tile_thumbnail_max_hz"):
        RecordKindPolicyConfig(failed_tile_thumbnail_max_hz=0.0)


def test_ac9_sliding_window_resets_across_windows(monkeypatch: pytest.MonkeyPatch) -> None:
    # Arrange — drive time via mock so the test is deterministic.
    fake_clock = [0.0]

    def fake_monotonic() -> float:
        return fake_clock[0]

    monkeypatch.setattr(
        "gps_denied_onboard.components.c13_fdr.record_kind_policy.time.monotonic",
        fake_monotonic,
    )
    policy = make_record_kind_policy(RecordKindPolicyConfig(failed_tile_thumbnail_max_hz=0.1))

    # Act — t=0, t=11, t=22
    fake_clock[0] = 0.0
    d0 = policy.gate_for_writer(_rec("failed_tile_thumbnail"))
    fake_clock[0] = 11.0
    d1 = policy.gate_for_writer(_rec("failed_tile_thumbnail"))
    fake_clock[0] = 22.0
    d2 = policy.gate_for_writer(_rec("failed_tile_thumbnail"))

    # Assert
    assert [d0, d1, d2] == [GateDecision.ENQUEUE] * 3
    assert policy.drain_pending_overrun() is None


def test_ac10_producer_slug_propagates_to_overrun(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    # Arrange
    policy = make_record_kind_policy(RecordKindPolicyConfig(failed_tile_thumbnail_max_hz=0.1))

    # Act — first thumbnail (admitted) from one producer; second (dropped) from another
    policy.gate_for_writer(_rec("failed_tile_thumbnail", producer_id="c6_tile_cache"))
    policy.gate_for_writer(_rec("failed_tile_thumbnail", producer_id="c6_tile_cache"))

    overrun = policy.drain_pending_overrun()
    assert overrun is not None
    assert overrun.payload["producer_id"] == "c6_tile_cache"


def test_nfr_perf_enforce_or_raise_microbench() -> None:
    # Arrange
    policy = make_record_kind_policy(RecordKindPolicyConfig())
    rec = _rec("vio.tick")

    # Act
    start = time.perf_counter()
    for _ in range(10_000):
        policy.enforce_or_raise(rec)
    elapsed_s = time.perf_counter() - start

    # Assert: p99 ≤ 1 µs implies average should be well under 5 µs.
    avg_us = (elapsed_s / 10_000) * 1e6
    assert avg_us < 5.0, f"enforce_or_raise avg {avg_us:.2f} µs too high"


def test_nfr_reliability_immutable_forbidden_kinds() -> None:
    # Arrange
    policy = make_record_kind_policy(RecordKindPolicyConfig())

    # Act + Assert — frozenset has no add/remove
    with pytest.raises(AttributeError):
        policy.forbidden_kinds.add("foo")  # type: ignore[attr-defined]


def test_non_thumbnail_records_always_enqueue() -> None:
    # Arrange
    policy = make_record_kind_policy(RecordKindPolicyConfig())

    # Act + Assert
    for kind in ("vio.tick", "state.tick", "tile_match", "log"):
        assert policy.gate_for_writer(_rec(kind)) is GateDecision.ENQUEUE


def test_warn_log_rate_limited(monkeypatch: pytest.MonkeyPatch) -> None:
    # Arrange
    policy = make_record_kind_policy(RecordKindPolicyConfig(failed_tile_thumbnail_max_hz=0.1))

    # Capture log warnings emitted by the policy.
    with mock.patch.object(policy._log, "warning") as warn_mock:
        # Act — many drops in quick succession
        for _ in range(20):
            policy.gate_for_writer(_rec("failed_tile_thumbnail"))

    # Assert — at most 1 warning fires (≤ 1 WARN/sec rate cap; first drop fires it)
    assert warn_mock.call_count <= 1
