"""AZ-272 — FdrRecord schema + versioned serialiser AC tests.

Verifies the contract at ``_docs/02_document/contracts/shared_fdr_client/fdr_record_schema.md`` v1.0.0.
"""

from __future__ import annotations

import ast
from pathlib import Path

import orjson
import pytest

from gps_denied_onboard.fdr_client import (
    CURRENT_SCHEMA_VERSION,
    KNOWN_KINDS,
    MAX_INLINE_BLOB_BYTES,
    OVERRUN_KIND,
    OVERRUN_PRODUCER_ID,
    FdrRecord,
    FdrSchemaError,
    parse,
    serialise,
)

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


def _kind_payload(kind: str) -> dict[str, object]:
    """Return a minimal valid payload for each v1.0.0 kind."""
    if kind == "log":
        return {
            "level": "INFO",
            "component": "c2_vpr",
            "frame_id": 42,
            "kind": "vpr.warmup",
            "msg": "loaded",
            "kv": {"model": "salad"},
        }
    if kind == "vio.tick":
        return {
            "frame_id": 1,
            "R": [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
            "t": [0.0, 0.0, 0.0],
            "P": [[1.0]],
            "last_anchor_age_ms": 100,
        }
    if kind == "state.tick":
        return {
            "frame_id": 1,
            "fused_pose": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
            "covariance_2x2": [[1.0, 0.0], [0.0, 1.0]],
            "estimator_label": "primary",
        }
    if kind == "tile_match":
        return {
            "frame_id": 1,
            "tile_id": "tile-001",
            "score": 0.85,
            "match_count": 124,
            "ransac_inliers": 96,
        }
    if kind == "overrun":
        return {"producer_id": "c1_vio", "dropped_count": 42}
    if kind == "segment_rollover":
        return {
            "old_segment": "seg-0001",
            "new_segment": "seg-0002",
            "total_bytes_after": 1024 * 1024,
        }
    if kind == "failed_tile_thumbnail":
        return {"frame_id": 1, "tile_id": "tile-002", "jpeg_bytes_b64": "AAAAAA=="}
    if kind == "mid_flight_tile_snapshot":
        return {"snapshot_path": "/var/lib/gps-denied/snap.dat", "captured_at": _TS}
    if kind == "flight_header":
        return {
            "flight_id": "f-0001",
            "flight_started_at_iso": _TS,
            "flight_started_at_monotonic_ns": 0,
            "config_snapshot": {},
            "signing_key_rotation_event": {},
            "manifest_content_hashes": {},
            "build_info": {"commit": "abc123"},
        }
    if kind == "flight_footer":
        return {
            "flight_id": "f-0001",
            "flight_ended_at_iso": _TS,
            "flight_ended_at_monotonic_ns": 0,
            "records_written": 12345,
            "records_dropped_overrun": 0,
            "bytes_written": 0,
            "rollover_count": 0,
            "clean_shutdown": True,
        }
    # AZ-490 / E-C5: operator-origin cold-start ladder + bounded-delta GPS gate.
    if kind == "c5.cold_start_origin.set":
        return {
            "source": "manifest",
            "lat_deg": 50.0,
            "lon_deg": 36.2,
            "alt_m": 200.0,
            "sigma_horiz_m": 5.0,
            "sigma_vert_m": 10.0,
        }
    if kind == "c5.cold_start_origin.unavailable":
        return {"reason": "no_manifest_origin_no_fc_ekf"}
    if kind == "c5.gps_bounded_delta.accept":
        return {
            "sample_lat": 50.0,
            "sample_lon": 36.2,
            "smoother_lat": 50.0001,
            "smoother_lon": 36.2,
            "distance_m": 11.1,
            "threshold_m": 200.0,
        }
    if kind == "c5.gps_bounded_delta.reject":
        return {
            "sample_lat": 50.0,
            "sample_lon": 36.2,
            "smoother_lat": 50.005,
            "smoother_lon": 36.205,
            "distance_m": 700.0,
            "threshold_m": 200.0,
        }
    if kind == "vio.health":
        return {
            "state": "tracking",
            "consecutive_lost": 0,
            "bias_norm": 0.012,
            "strategy_label": "okvis2",
            "frame_id": "frame-0001",
        }
    if kind == "c7.thermal_transition":
        return {
            "previous_state": False,
            "new_state": True,
            "gpu_temp_c": 92.0,
            "cpu_temp_c": 85.0,
            "measured_clock_mhz": 600,
            "measured_at_ns": 1_700_000_000_000_000_000,
        }
    raise AssertionError(f"unhandled kind in fixture: {kind!r}")


def _make_record(kind: str, **overrides: object) -> FdrRecord:
    """Build a minimal valid record for ``kind`` so each AC test can mutate the field it cares about."""
    if kind == OVERRUN_KIND:
        producer_id = overrides.pop("producer_id", OVERRUN_PRODUCER_ID)
    else:
        producer_id = overrides.pop("producer_id", "c2_vpr")
    return FdrRecord(
        schema_version=int(overrides.pop("schema_version", CURRENT_SCHEMA_VERSION)),
        ts=str(overrides.pop("ts", _TS)),
        producer_id=str(producer_id),
        kind=str(overrides.pop("kind", kind)),
        payload=dict(overrides.pop("payload", _kind_payload(kind))),  # type: ignore[arg-type]
    )


@pytest.mark.parametrize("kind", sorted(KNOWN_KINDS))
def test_ac1_roundtrip_every_known_kind(kind: str) -> None:
    # Arrange
    record = _make_record(kind)
    # Act
    decoded = parse(serialise(record))
    # Assert
    assert decoded == record


def test_ac2_forward_compatible_unknown_payload_field_preserved() -> None:
    # Arrange — synthesise a future v1.1 record adding an unknown payload field.
    payload = _kind_payload("log") | {"new_field": "x"}
    wire = orjson.dumps(
        {
            "schema_version": 1,
            "ts": _TS,
            "producer_id": "c2_vpr",
            "kind": "log",
            "payload": payload,
        }
    )
    # Act
    record = parse(wire)
    # Assert
    assert record.payload.get("extra", {}).get("new_field") == "x"
    assert "new_field" not in record.payload
    # Known fields still parse out cleanly.
    assert record.payload["msg"] == "loaded"


def test_ac2b_forward_compatible_unknown_top_level_field_preserved() -> None:
    wire = orjson.dumps(
        {
            "schema_version": 1,
            "ts": _TS,
            "producer_id": "c2_vpr",
            "kind": "log",
            "payload": _kind_payload("log"),
            "trailing": "future-field",
        }
    )
    record = parse(wire)
    assert record.extra == {"trailing": "future-field"}


def test_ac3_unknown_future_kind_returned_opaquely() -> None:
    wire = orjson.dumps(
        {
            "schema_version": 1,
            "ts": _TS,
            "producer_id": "future.producer",
            "kind": "future.kind",
            "payload": {"foo": 1},
        }
    )
    record = parse(wire)
    assert record.kind == "future.kind"
    assert record.payload == {"foo": 1}
    assert record.extra == {}


def test_ac4_missing_schema_version_raises() -> None:
    wire = orjson.dumps(
        {
            "ts": _TS,
            "producer_id": "c2_vpr",
            "kind": "log",
            "payload": _kind_payload("log"),
        }
    )
    with pytest.raises(FdrSchemaError, match=r"schema_version"):
        parse(wire)


def test_ac4_non_integer_schema_version_raises() -> None:
    wire = orjson.dumps(
        {
            "schema_version": "1.0",
            "ts": _TS,
            "producer_id": "c2_vpr",
            "kind": "log",
            "payload": _kind_payload("log"),
        }
    )
    with pytest.raises(FdrSchemaError, match=r"schema_version"):
        parse(wire)


def test_ac5_overrun_missing_dropped_count_rejected_on_parse() -> None:
    wire = orjson.dumps(
        {
            "schema_version": 1,
            "ts": _TS,
            "producer_id": OVERRUN_PRODUCER_ID,
            "kind": OVERRUN_KIND,
            "payload": {"producer_id": "c1_vio"},
        }
    )
    with pytest.raises(FdrSchemaError, match=r"dropped_count"):
        parse(wire)


def test_ac5_overrun_zero_dropped_count_rejected_on_serialise() -> None:
    record = _make_record(OVERRUN_KIND, payload={"producer_id": "c1_vio", "dropped_count": 0})
    with pytest.raises(FdrSchemaError, match=r"dropped_count"):
        serialise(record)


def test_ac6_empty_producer_id_rejected_on_serialise() -> None:
    record = _make_record("log", producer_id="")
    with pytest.raises(FdrSchemaError, match=r"producer_id"):
        serialise(record)


def test_nfr_oversized_inline_blob_rejected() -> None:
    blob = b"\x00" * (MAX_INLINE_BLOB_BYTES + 1)
    payload = _kind_payload("failed_tile_thumbnail") | {"jpeg_bytes": blob}
    record = _make_record("failed_tile_thumbnail", payload=payload)
    with pytest.raises(FdrSchemaError, match=r"sidecar path"):
        serialise(record)


def test_nfr_serialise_is_pure_byte_identical() -> None:
    record = _make_record("log")
    first = serialise(record)
    second = serialise(record)
    assert first == second


def test_no_upward_imports_to_components() -> None:
    """Layer-0/cross-cutting: ``fdr_client.records`` must not import any component."""
    module_path = (
        Path(__file__).resolve().parents[2]
        / "src"
        / "gps_denied_onboard"
        / "fdr_client"
        / "records.py"
    )
    tree = ast.parse(module_path.read_text(encoding="utf-8"))
    bad: list[str] = []
    for node in ast.walk(tree):
        if isinstance(node, ast.ImportFrom) and node.module:
            if node.module.startswith("gps_denied_onboard.components"):
                bad.append(node.module)
        elif isinstance(node, ast.Import):
            for alias in node.names:
                if alias.name.startswith("gps_denied_onboard.components"):
                    bad.append(alias.name)
    assert not bad, f"fdr_client.records must not import components.*; found: {bad}"
