"""Contract: `audit_tail` + `state_snapshot` wire ops (Part 13
§5.6 — sub-phases 5A + 5B + 5C; broker self-awareness).

Pinned here:

  * **audit_tail.** Returns up to `n` most-recent records from
    the broker's audit ring. Each record carries seq / ts_ms /
    duration_us / op / outcome / peer_uid / peer_pid / cap_id,
    plus optional urn / err / correlation_id. The ring caps at
    4096 entries (`MAX_AUDIT_TAIL_N`); requests above that
    reject with BadRequest.
  * **state_snapshot.** Aggregates broker-wide counters into
    one diagnostic payload. The reported field set MUST cover
    ops_total / active_connections / active_leases /
    active_subscriptions / audit_ring_used / audit_ring_capacity
    / workers / uptime_ms / broker_version.
  * **Audit-read cap gate.** Both ops require a cap_id carrying
    the `audit_read` right (Part 13 §8). Requests without
    cap_id (or with a wrong cap_id) reject NotPermitted; the
    broker hello tells the client which id to present.
  * **Every wire op records itself.** A successful `list` op
    is in the next `audit_tail`'s results. The audit_tail call
    records itself too — its seq is the newest in subsequent
    queries.
  * **correlation_id echoes into the audit record.** When a
    consumer passes `correlation_id` on a request, the audit
    entry carries it — making multi-op tracing reconstructable
    from broker side alone.
"""

from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING, Any

import pytest

from tests import _broker
from tests.contract.conftest import _SOCK_PATH, rt

if TYPE_CHECKING:
    import socket

    from mgf.hrb import Client

pytestmark = pytest.mark.contract


def _tail(sock: socket.socket, cap_id: int, **kwargs: Any) -> dict[str, Any]:
    """Convenience: an authorised audit_tail with extra kwargs spliced."""
    return rt(sock, {"op": "audit_tail", "cap_id": cap_id, **kwargs})


def _snap(sock: socket.socket, cap_id: int) -> dict[str, Any]:
    """Convenience: an authorised state_snapshot."""
    return rt(sock, {"op": "state_snapshot", "cap_id": cap_id})


# ---------------------------------------------------------------------------
# Audit-read cap gate (Part 13 §8) — sub-phase 5A follow-up.
# ---------------------------------------------------------------------------


def test_audit_tail_without_cap_rejects(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    """Without `cap_id`, audit_tail rejects NotPermitted — the
    `audit_read` right gates it."""
    sock, _ = raw_broker_with_cap
    r = rt(sock, {"op": "audit_tail"})
    assert r.get("ok") is False
    assert r.get("err") == "NotPermitted"


def test_state_snapshot_without_cap_rejects(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    sock, _ = raw_broker_with_cap
    r = rt(sock, {"op": "state_snapshot"})
    assert r.get("ok") is False
    assert r.get("err") == "NotPermitted"


def test_audit_tail_wrong_cap_rejects(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    """A `cap_id` the broker doesn't know rejects NotPermitted —
    same path the verifier takes for any cap-gated op."""
    sock, _ = raw_broker_with_cap
    r = rt(sock, {"op": "audit_tail", "cap_id": 42})
    assert r.get("ok") is False
    assert r.get("err") == "NotPermitted"


# ---------------------------------------------------------------------------
# audit_tail shape (authorised).
# ---------------------------------------------------------------------------


def test_audit_tail_success_shape(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    """Success response carries the documented keys."""
    sock, cap_id = raw_broker_with_cap
    r = _tail(sock, cap_id, n=5)
    assert r.get("ok") is True
    required = {"records", "ring_capacity", "earliest_ts_ms"}
    assert required <= r.keys()
    assert isinstance(r["records"], list)
    assert r["ring_capacity"] == 4096


def test_audit_record_field_shape(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    """Each record MUST carry the pinned per-record fields. Optional
    fields (urn / err / correlation_id) are only present when
    applicable; required fields are always present."""
    sock, cap_id = raw_broker_with_cap
    rt(sock, {"op": "inspect", "urn": "hw:/test/dev0", "correlation_id": "trace-1"})
    r = _tail(sock, cap_id, n=10)
    # Note the broker pushes the audit record AFTER building the
    # response, so an audit_tail call won't see ITSELF in its own
    # results — but it sees every prior op (inspect above is in).
    assert len(r["records"]) >= 1
    required = {"seq", "ts_ms", "duration_us", "op", "outcome", "peer_uid", "peer_pid", "cap_id"}
    for rec in r["records"]:
        assert required <= rec.keys(), f"missing in {rec!r}"
        assert isinstance(rec["seq"], int)
        assert isinstance(rec["ts_ms"], int)
        assert rec["outcome"] in {"ok", "err", "timeout", "conflict"}


def test_audit_tail_records_self(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    """The audit_tail op records itself — successive audit_tail
    calls each appear as the newest record in the next call's
    results."""
    sock, cap_id = raw_broker_with_cap
    r1 = _tail(sock, cap_id, n=5)
    r2 = _tail(sock, cap_id, n=5)
    assert len(r2["records"]) > 0
    if r1["records"]:
        assert r2["records"][0]["seq"] > r1["records"][0]["seq"]
    assert r2["records"][0]["op"] == "audit_tail"


def test_audit_tail_captures_correlation_id(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    """A request with `correlation_id` MUST flow into the audit
    record's correlation_id field — proves the broker side can
    reconstruct multi-op causal traces."""
    sock, cap_id = raw_broker_with_cap
    rt(sock, {"op": "inspect", "urn": "hw:/test/dev0", "correlation_id": "trace-X"})
    r = _tail(sock, cap_id, n=5)
    inspect_recs = [
        rec for rec in r["records"] if rec["op"] == "inspect" and rec.get("urn") == "hw:/test/dev0"
    ]
    assert len(inspect_recs) > 0
    assert inspect_recs[0].get("correlation_id") == "trace-X"


def test_audit_tail_outcome_err_records_err_code(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    """An op that yields an error response MUST be recorded with
    outcome=err + an err code."""
    sock, cap_id = raw_broker_with_cap
    rt(sock, {"op": "inspect", "urn": "hw:/never-existed"})
    r = _tail(sock, cap_id, n=5)
    err_recs = [rec for rec in r["records"] if rec["op"] == "inspect" and rec["outcome"] == "err"]
    assert len(err_recs) > 0
    assert err_recs[0].get("err") == "NotFound"


def test_audit_tail_n_default_is_100(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    """Omitting `n` MUST work; the broker uses 100 as the default."""
    sock, cap_id = raw_broker_with_cap
    r = _tail(sock, cap_id)
    assert r.get("ok") is True
    assert isinstance(r["records"], list)


def test_audit_tail_n_negative_rejects(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    sock, cap_id = raw_broker_with_cap
    r = _tail(sock, cap_id, n=-1)
    assert r.get("ok") is False
    assert r.get("err") == "BadRequest"


def test_audit_tail_n_above_cap_rejects(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    """`n` above the ring capacity (4096) MUST reject — defends
    against unreasonable response sizes."""
    sock, cap_id = raw_broker_with_cap
    r = _tail(sock, cap_id, n=99999)
    assert r.get("ok") is False
    assert r.get("err") == "BadRequest"


def test_audit_tail_n_non_integer_rejects(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    sock, cap_id = raw_broker_with_cap
    for bad in ("five", 1.5, [5], None):
        r = _tail(sock, cap_id, n=bad)
        assert r.get("ok") is False, f"n={bad!r}: {r}"
        assert r.get("err") == "BadRequest"


# ---------------------------------------------------------------------------
# state_snapshot shape.
# ---------------------------------------------------------------------------


REQUIRED_SNAPSHOT_FIELDS = {
    "ts_ms",
    "uptime_ms",
    "broker_version",
    "ops_total",
    "ops_per_second_1m",
    "active_connections",
    "active_leases",
    "active_subscriptions",
    "cap_table_size",
    "audit_ring_used",
    "audit_ring_capacity",
    "memory",
    "workers",
}


def test_state_snapshot_shape(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    """Success response carries every pinned field."""
    sock, cap_id = raw_broker_with_cap
    r = _snap(sock, cap_id)
    assert r.get("ok") is True
    missing = REQUIRED_SNAPSHOT_FIELDS - r.keys()
    assert not missing, f"missing fields: {missing}"
    # Derived, never restated (M-B B2). This assertion read `== "0.1.0"`
    # for nine releases and was green the whole time, because the field it
    # pinned was hardcoded to the same wrong literal — the pin agreed with
    # the bug. `_broker.build_version()` reads the one authority the build
    # itself reads.
    assert r["broker_version"] == _broker.build_version()
    assert r["audit_ring_capacity"] == 4096
    assert isinstance(r["workers"], dict)


def test_state_snapshot_workers_carry_three_fields(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    """Each per-worker entry MUST carry backend / mode / url_count."""
    sock, cap_id = raw_broker_with_cap
    r = _snap(sock, cap_id)
    for scope, w in r["workers"].items():
        assert isinstance(scope, str)
        assert set(w.keys()) >= {"backend", "mode", "url_count"}
        assert isinstance(w["url_count"], int)


def test_state_snapshot_ops_total_increments(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    """Successive state_snapshot calls show monotonically-growing
    `ops_total` — proves the audit ring is wired."""
    sock, cap_id = raw_broker_with_cap
    r1 = _snap(sock, cap_id)
    rt(sock, {"op": "list", "scope": "hw:/**"})
    r2 = _snap(sock, cap_id)
    assert r2["ops_total"] >= r1["ops_total"] + 2


def test_state_snapshot_active_connections_at_least_one(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    sock, cap_id = raw_broker_with_cap
    r = _snap(sock, cap_id)
    assert r["active_connections"] >= 1


def test_state_snapshot_carries_top_level_fidelity(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    sock, cap_id = raw_broker_with_cap
    r = _snap(sock, cap_id)
    assert r.get("fidelity") == "live"


def test_state_snapshot_ops_per_second_is_float(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    """`ops_per_second_1m` MUST be a numeric (the Zig emit uses
    `{d}.{d}` for fractional ops/sec). For a freshly-started
    broker doing a handful of ops, the rate is well under 1
    op/sec."""
    sock, cap_id = raw_broker_with_cap
    r = _snap(sock, cap_id)
    assert isinstance(r["ops_per_second_1m"], (int, float))
    assert r["ops_per_second_1m"] >= 0.0


def test_state_snapshot_cap_table_size_at_least_one(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    """The broker mints a root cap at startup; cap_table_size
    MUST report at least 1."""
    sock, cap_id = raw_broker_with_cap
    r = _snap(sock, cap_id)
    assert r["cap_table_size"] >= 1


def test_state_snapshot_memory_rss_is_positive(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    """On Linux (the only platform we care about today),
    /proc/self/statm read succeeds + reports a positive RSS.
    Falls back to 0 on read failure (the field is still present
    — honest emptiness)."""
    sock, cap_id = raw_broker_with_cap
    r = _snap(sock, cap_id)
    assert "rss_bytes" in r["memory"]
    # On Linux, a running broker is always at least 1 MiB.
    if Path("/proc/self/statm").exists():
        assert r["memory"]["rss_bytes"] > 1_000_000


# ---------------------------------------------------------------------------
# SDK projection.
# ---------------------------------------------------------------------------


def test_sdk_audit_tail_returns_typed_records(broker: Client) -> None:
    # The SDK reads cap_id from the hello and forwards it
    # automatically on audit_tail / state_snapshot — same
    # pattern as claim/raw/control.
    broker.list("hw:/**")
    tail = broker.audit_tail(n=5)
    assert isinstance(tail.records, tuple)
    assert tail.ring_capacity == 4096
    assert len(tail.records) > 0
    for rec in tail.records:
        assert rec.outcome in {"ok", "err", "timeout", "conflict"}

    snap = broker.state_snapshot()
    assert snap.broker_version == _broker.build_version()
    assert snap.audit_ring_capacity == 4096
    assert snap.active_connections >= 1
    assert snap.fidelity == "live"
    assert snap.cap_table_size >= 1
    assert snap.ops_per_second_1m >= 0.0
    # On Linux, rss > 0; on other platforms, 0 is fine.
    assert snap.memory_rss_bytes >= 0
    assert len(snap.workers) > 0
    # workers is now a tuple of WorkerSnapshot (each carries
    # its own scope_prefix) — C1: changed from a dict so the
    # frozen StateSnapshot is genuinely hashable.
    for w in snap.workers:
        assert w.scope_prefix.startswith("hw:/")
        assert w.backend
        assert w.mode
        assert isinstance(w.url_count, int)


# ---------------------------------------------------------------------------
# M-B B2 — the two version surfaces must agree, and must not be the literal.
# ---------------------------------------------------------------------------


def test_both_version_surfaces_report_the_build_and_agree(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    """The hello's ``version`` and ``state_snapshot``'s ``broker_version``
    are two renderings of ONE fact, and both were independently hardcoded
    to ``"0.1.0"`` while the project shipped 0.1.9.

    This pin is deliberately shaped so it cannot rot the way the literals
    did. It asserts three things, none of which is a version number typed
    into a test file:

      1. Both surfaces equal ``_broker.build_version()`` — the same
         ``pyproject.toml`` field ``build.zig`` compiles into the binary.
      2. They equal each other, so a future change to one that misses the
         other is caught even if the authority itself moved.
      3. Neither is ``"0.1.0"`` — the specific stale literal B2 removed,
         named explicitly so a revert reports itself rather than passing
         vacuously on some hypothetical day the project is at 0.1.0.
    """
    sock, cap_id = raw_broker_with_cap

    # A second connection, opened by hand so its hello line is readable
    # (the fixture drains its own).
    import json
    import socket as _socket

    hello_sock = _socket.socket(_socket.AF_UNIX, _socket.SOCK_STREAM)
    hello_sock.connect(_SOCK_PATH)
    try:
        buf = b""
        while b"\n" not in buf:
            buf += hello_sock.recv(4096)
        hello = json.loads(buf.split(b"\n", 1)[0])
    finally:
        hello_sock.close()

    snap = _snap(sock, cap_id)
    expected = _broker.build_version()

    assert hello["version"] == expected, hello
    assert snap["broker_version"] == expected, snap
    assert hello["version"] == snap["broker_version"]
    assert hello["version"] != "0.1.0", (
        "the broker is reporting the stale hardcoded version M-B B2 removed"
    )
