"""Pins for the computed properties added in A2 of the
presentation-layer plan.

Computed properties on dataclasses are convenience accessors that
delegate to ``mgf.hrb.fmt`` helpers (which have their own canonical
test pins in test_fmt.py). These tests verify the WIRING is correct
— that constructing the dataclass and reading the property produces
the same string the fmt helper would, with no off-by-one in the
field mapping (e.g. accidentally passing ``ts_ms`` where
``uptime_ms`` was meant).

Coverage:
  * StateSnapshot.uptime_human delegates to format_uptime(uptime_ms).
  * StateSnapshot.memory_rss_human delegates to format_rss(memory_rss_bytes).
  * AuditRecord.duration_human delegates to format_duration(duration_us).
  * AuditTailResult.outcome_summary() tallies records correctly,
    picks the newest 'err' record, handles empty tails.
"""

from __future__ import annotations

from mgf.hrb.domain import (
    AuditOutcomeSummary,
    AuditRecord,
    AuditTailResult,
    BackendCapability,
    StateSnapshot,
)

# ---------------------------------------------------------------------------
# Helpers — minimal dataclass constructors that don't repeat the full
# field list in every test.
# ---------------------------------------------------------------------------


def _snap(uptime_ms: int = 0, memory_rss_bytes: int = 0) -> StateSnapshot:
    """Build a StateSnapshot with only the fields the tests below
    actually exercise; everything else gets zero defaults."""
    return StateSnapshot(
        ts_ms=0,
        uptime_ms=uptime_ms,
        broker_version="0.0.0-test",
        ops_total=0,
        ops_per_second_1m=0.0,
        active_connections=0,
        active_leases=0,
        active_subscriptions=0,
        cap_table_size=0,
        audit_ring_used=0,
        audit_ring_capacity=4096,
        audit_dropped_total=0,
        memory_rss_bytes=memory_rss_bytes,
        workers=(),
        fidelity="live",
    )


def _rec(
    outcome: str = "ok",
    duration_us: int = 0,
    *,
    seq: int = 1,
    op: str = "list",
    err: str | None = None,
) -> AuditRecord:
    """Build an AuditRecord with field defaults so each test names
    only the fields it cares about."""
    return AuditRecord(
        seq=seq,
        ts_ms=0,
        duration_us=duration_us,
        op=op,
        outcome=outcome,
        peer_uid=1000,
        peer_pid=2000,
        cap_id=0,
        urn=None,
        err=err,
        correlation_id=None,
    )


# ---------------------------------------------------------------------------
# StateSnapshot.uptime_human
# ---------------------------------------------------------------------------


def test_uptime_human_delegates_to_format_uptime() -> None:
    """The property reads from ``uptime_ms``, not any other field."""
    assert _snap(uptime_ms=3_725_000).uptime_human == "1h 2m 5s"


def test_uptime_human_zero_for_fresh_broker() -> None:
    """A just-started broker reads '0s', not crash."""
    assert _snap(uptime_ms=0).uptime_human == "0s"


def test_uptime_human_isnt_confused_with_ts_ms() -> None:
    """Regression guard: the property must read from `uptime_ms` not
    `ts_ms`. Construct a snap where `ts_ms` is HUGE (epoch ms) but
    `uptime_ms` is small; expect the small value's format."""
    snap = StateSnapshot(
        ts_ms=1_700_000_000_000,  # ~Nov 2023 in epoch ms (would render '500_000h...')
        uptime_ms=120_000,
        broker_version="x",
        ops_total=0,
        ops_per_second_1m=0.0,
        active_connections=0,
        active_leases=0,
        active_subscriptions=0,
        cap_table_size=0,
        audit_ring_used=0,
        audit_ring_capacity=4096,
        audit_dropped_total=0,
        memory_rss_bytes=0,
        workers=(),
        fidelity="live",
    )
    assert snap.uptime_human == "2m 0s"


# ---------------------------------------------------------------------------
# StateSnapshot.memory_rss_human
# ---------------------------------------------------------------------------


def test_memory_rss_human_uses_binary_prefix() -> None:
    """Binary not decimal — see test_fmt.py's
    ``test_rss_uses_binary_prefixes_not_decimal`` for the
    same-shaped pin one layer down."""
    assert _snap(memory_rss_bytes=44_400_000).memory_rss_human == "42.3 MiB"


def test_memory_rss_human_zero() -> None:
    """A broker that couldn't read /proc/self/status (non-Linux,
    or sandbox without /proc) reports 0 RSS; we render '0 B' cleanly."""
    assert _snap(memory_rss_bytes=0).memory_rss_human == "0 B"


# ---------------------------------------------------------------------------
# AuditRecord.duration_human
# ---------------------------------------------------------------------------


def test_duration_human_microsecond_band() -> None:
    """Most audit records sit in the sub-ms band on a fast broker."""
    assert _rec(duration_us=500).duration_human == "500µs"


def test_duration_human_millisecond_band() -> None:
    assert _rec(duration_us=4_500).duration_human == "4.5ms"


def test_duration_human_second_band() -> None:
    """A 1.2s record is operator-interesting (something slow)."""
    assert _rec(duration_us=1_200_000).duration_human == "1.2s"


# ---------------------------------------------------------------------------
# AuditTailResult.outcome_summary
# ---------------------------------------------------------------------------


def test_outcome_summary_empty_tail() -> None:
    """Empty tail returns the zero-shaped summary."""
    result = AuditTailResult(records=(), ring_capacity=4096, earliest_ts_ms=0)
    summary = result.outcome_summary()
    assert summary == AuditOutcomeSummary(ok=0, err=0, other=0, newest_error=None)


def test_outcome_summary_all_ok() -> None:
    """Five ok records → ok=5, err=0, other=0, newest_error=None."""
    result = AuditTailResult(
        records=tuple(_rec(outcome="ok", seq=i) for i in range(5)),
        ring_capacity=4096,
        earliest_ts_ms=0,
    )
    summary = result.outcome_summary()
    assert summary.ok == 5
    assert summary.err == 0
    assert summary.other == 0
    assert summary.newest_error is None


def test_outcome_summary_mixed_picks_newest_err() -> None:
    """Records arrive newest-first. With two err records, the FIRST
    one in the tuple (the newest) is the one picked."""
    newest_err = _rec(outcome="err", seq=10, op="claim", err="Conflict")
    older_err = _rec(outcome="err", seq=5, op="raw", err="Revoked")
    records = (
        _rec(outcome="ok", seq=12),
        newest_err,  # newest-first ordering: this is the first err
        _rec(outcome="ok", seq=8),
        older_err,
    )
    result = AuditTailResult(records=records, ring_capacity=4096, earliest_ts_ms=0)
    summary = result.outcome_summary()
    assert summary.ok == 2
    assert summary.err == 2
    assert summary.other == 0
    assert summary.newest_error is newest_err
    # Cross-check: the picked record is identified by its op/err fields.
    assert summary.newest_error.op == "claim"
    assert summary.newest_error.err == "Conflict"


def test_outcome_summary_counts_other_buckets() -> None:
    """timeout / conflict / future broker variants land in `other`
    rather than being mis-classified as err or silently dropped."""
    records = (
        _rec(outcome="timeout", seq=3),
        _rec(outcome="conflict", seq=2),
        _rec(outcome="future_variant", seq=1),
    )
    result = AuditTailResult(records=records, ring_capacity=4096, earliest_ts_ms=0)
    summary = result.outcome_summary()
    assert summary.ok == 0
    assert summary.err == 0
    assert summary.other == 3
    assert summary.newest_error is None


def test_outcome_summary_is_hashable_typed_shape() -> None:
    """AuditOutcomeSummary is a frozen-slots dataclass; consumers
    can use it as a dict key or set member."""
    summary = AuditOutcomeSummary(ok=1, err=0, other=0, newest_error=None)
    # Hashable test
    {summary}  # noqa: B018 — intentional: assert no TypeError on hash
    # Equality test (value-equal summaries are equal)
    assert summary == AuditOutcomeSummary(ok=1, err=0, other=0, newest_error=None)


# ---------------------------------------------------------------------------
# BackendCapability.KNOWN_OPS + ops_dict (A3)
# ---------------------------------------------------------------------------


def test_known_ops_is_class_constant_not_per_instance() -> None:
    """A3 — KNOWN_OPS lives on the CLASS, not the instance. Every
    instance + the class itself sees the same tuple; consumers
    iterating `cap.KNOWN_OPS` are reading the SDK-stable list, not
    something the broker injected."""
    assert BackendCapability.KNOWN_OPS is not None
    assert len(BackendCapability.KNOWN_OPS) == 9
    # Each member is a non-empty string
    for op in BackendCapability.KNOWN_OPS:
        assert isinstance(op, str)
        assert op != ""


def test_known_ops_preserves_canonical_order() -> None:
    """The order is the canonical kabi-seam order, NOT alphabetical.
    Operators reading the doctor's ops grid (now and post-extraction)
    see ops in the same display order they always have."""
    expected = (
        "enumerate",
        "query_topology",
        "watch",
        "acquire",
        "release",
        "reset",
        "map_region",
        "raw_channel",
        "enforce_cap",
    )
    assert BackendCapability.KNOWN_OPS == expected


def test_ops_dict_converts_storage_tuple_to_flat_dict() -> None:
    """ops_dict() flattens the (key, value) tuple-of-pairs storage
    into a dict — the shape consumers actually want when checking
    'does this backend support op X?'."""
    cap = BackendCapability(
        scope_prefix="hw:/usb/",
        backend_name="mgf-hrb-usb",
        resource_count=0,
        ops=(
            ("enumerate", True),
            ("acquire", True),
            ("map_region", False),
        ),
    )
    d = cap.ops_dict()
    assert d == {"enumerate": True, "acquire": True, "map_region": False}
    assert isinstance(d, dict)


def test_ops_dict_returns_fresh_dict_per_call() -> None:
    """Mutating the returned dict mustn't mutate the underlying
    storage (the underlying tuple IS the canonical shape; consumers
    are free to do whatever they want with the dict copy)."""
    cap = BackendCapability(
        scope_prefix="hw:/usb/",
        backend_name="mgf-hrb-usb",
        resource_count=0,
        ops=(("enumerate", True),),
    )
    d1 = cap.ops_dict()
    d1["enumerate"] = False  # mutation OK on the copy
    d2 = cap.ops_dict()
    assert d2 == {"enumerate": True}  # storage unchanged


def test_ops_dict_iteration_via_known_ops_is_stable_grid() -> None:
    """The recommended consumer idiom: iterate KNOWN_OPS, look up
    in ops_dict(). Yields a stable grid order regardless of which
    ops the broker happens to report (or in what order)."""
    cap = BackendCapability(
        scope_prefix="hw:/usb/",
        backend_name="mgf-hrb-usb",
        resource_count=0,
        ops=(
            # Intentionally NOT in KNOWN_OPS order (broker emitted
            # alphabetical via the from_wire factory's sort).
            ("acquire", True),
            ("enforce_cap", False),
            ("enumerate", True),
        ),
    )
    grid = [cap.ops_dict().get(op, False) for op in BackendCapability.KNOWN_OPS]
    # KNOWN_OPS order: enumerate, query_topology, watch, acquire, release,
    #                  reset, map_region, raw_channel, enforce_cap
    assert grid == [
        True,  # enumerate
        False,  # query_topology — broker didn't report; default False
        False,  # watch — same
        True,  # acquire
        False,  # release — broker didn't report
        False,  # reset
        False,  # map_region
        False,  # raw_channel
        False,  # enforce_cap (broker reported False)
    ]


def test_ops_dict_handles_unknown_op_from_broker() -> None:
    """Forward-compat: a future broker may report a NEW op the SDK
    doesn't know about. ops_dict() returns the new key (so consumers
    that iterate `.keys()` see the live set); the KNOWN_OPS grid
    silently excludes it (so the stable-shape display doesn't shift
    without an SDK update)."""
    cap = BackendCapability(
        scope_prefix="hw:/cpu/",
        backend_name="mgf-hrb-cpu",
        resource_count=0,
        ops=(
            ("enumerate", True),
            # Hypothetical future op the SDK doesn't know yet:
            ("future_op_v2", True),
        ),
    )
    d = cap.ops_dict()
    # The new op IS in the live dict
    assert d.get("future_op_v2") is True
    # But the KNOWN_OPS grid doesn't include it (stability over
    # surprise — operators don't get a new column without an SDK bump)
    assert "future_op_v2" not in BackendCapability.KNOWN_OPS
