"""Tests for the frozen-dataclass projections in ``mgf.hrb.domain``.

The wire ↔ Python boundary translates broker dicts into typed
records (DP-03 / EH-02). These tests pin the ``from_wire`` defensive
coercion at the seam: an older broker that omits a field, a newer
broker that adds one, or a wire-protocol skew that misnames a value
must never crash the consumer. The dataclasses are also
frozen+slotted → hashable, so we test that property explicitly (GUI
consumers treat them as Qt model rows).

Three records share the pattern:
  • ``ResourceDetail`` (FEEDBACK SDK-02 typed half) — Client.inspect
  • ``BackendCapability`` (FEEDBACK SDK-05) — Client.capabilities
  • ``LeaseRecord`` (FEEDBACK SDK-03) — Client.list_leases

The pattern is the same; the failure modes differ per shape. Each
record gets a happy-path round-trip + the failure modes that matter
for that record.
"""

from __future__ import annotations

import pytest

from mgf.hrb.domain import (
    BackendCapability,
    ClaimMode,
    Family,
    FieldFidelity,
    Kind,
    LeaseRecord,
    ResourceDetail,
)

# ============================================================================
# ResourceDetail (FEEDBACK SDK-02 typed half)
# ============================================================================


def test_resource_detail_from_wire_happy_path() -> None:
    """All fields present, well-typed: the round-trip yields the
    expected typed values."""
    d = ResourceDetail.from_wire(
        {
            "urn": "hw:/usb/bus3/port-9/dev/if0",
            "family": "claimable",
            "kind": "usb_interface",
            "fidelity": "live",
            "attrs": {
                "idVendor": "1234",
                "idProduct": "5678",
                "serial": "ABC-001",
            },
        }
    )
    assert d.urn == "hw:/usb/bus3/port-9/dev/if0"
    assert d.family is Family.CLAIMABLE
    assert d.kind is Kind.USB_INTERFACE
    assert d.fidelity is FieldFidelity.LIVE
    # attrs is a sorted tuple of (k, v) pairs.
    assert d.attrs == (
        ("idProduct", "5678"),
        ("idVendor", "1234"),
        ("serial", "ABC-001"),
    )


def test_resource_detail_attr_lookup_helper() -> None:
    """`detail.attr(key)` returns the value or None — never raises
    for an absent key (so the consumer doesn't need defensive
    try/except)."""
    d = ResourceDetail.from_wire(
        {
            "urn": "hw:/usb/bus3/port-9/dev/if0",
            "family": "claimable",
            "kind": "usb_interface",
            "fidelity": "live",
            "attrs": {"idVendor": "1234"},
        }
    )
    assert d.attr("idVendor") == "1234"
    assert d.attr("nonexistent") is None
    # G2 honest emptiness: an empty-string value is preserved (not
    # collapsed to None) — the broker reported it as empty for a
    # reason; the consumer decides what that means.
    e = ResourceDetail.from_wire(
        {
            "urn": "x",
            "family": "claimable",
            "kind": "usb_interface",
            "fidelity": "live",
            "attrs": {"serial": ""},
        }
    )
    assert e.attr("serial") == ""


def test_resource_detail_is_frozen_and_hashable() -> None:
    """DP-03: frozen across the boundary. A GUI can hash the record
    and use it as a Qt model row identity or set member."""
    d = ResourceDetail.from_wire(
        {
            "urn": "hw:/test/dev0",
            "family": "claimable",
            "kind": "usb_interface",
            "fidelity": "live",
            "attrs": {"k": "v"},
        }
    )
    # Frozen → attribute assignment raises.
    with pytest.raises((AttributeError, TypeError)):
        d.urn = "mutated"  # type: ignore[misc]
    # Hashable.
    s = {d}
    assert d in s


def test_resource_detail_unknown_enum_falls_back_to_default() -> None:
    """G2 defensive coercion: an unknown enum value (wire-protocol
    skew, future broker that added a new variant) falls back to
    the per-field default rather than raising. The consumer keeps
    rendering with honest defaults."""
    d = ResourceDetail.from_wire(
        {
            "urn": "x",
            "family": "some_future_family",  # unknown
            "kind": "some_future_kind",  # unknown
            "fidelity": "live",
        }
    )
    # Falls back to the documented per-field defaults.
    assert d.family is Family.CLAIMABLE
    assert d.kind is Kind.PCIE_FUNCTION
    assert d.fidelity is FieldFidelity.LIVE


def test_resource_detail_missing_keys_default_sanely() -> None:
    """An empty or partial wire dict doesn't crash — every absent
    key falls back to a sensible default. This is the
    older-broker-talking-to-newer-consumer path; it must not
    raise."""
    d = ResourceDetail.from_wire({})
    assert d.urn == ""
    assert d.family is Family.CLAIMABLE
    assert d.attrs == ()
    assert d.parent is None
    assert d.state is None


def test_resource_detail_forward_compat_fields_surface_when_present() -> None:
    """`parent` and `state` are forward-compat optionals; when the
    broker sends them they appear; when it doesn't they stay None.
    This is the newer-broker-talking-to-older-consumer path."""
    d = ResourceDetail.from_wire(
        {
            "urn": "hw:/usb/bus3/port-9/dev",
            "family": "claimable",
            "kind": "usb_interface",
            "fidelity": "live",
            "parent": "hw:/usb/bus3",
            "state": "free",
        }
    )
    assert d.parent == "hw:/usb/bus3"
    assert d.state == "free"


def test_resource_detail_attrs_with_non_string_values_coerced() -> None:
    """The wire spec says attr values are strings, but a misbehaving
    broker might send an int/bool/etc. The seam coerces with
    ``str()`` rather than raising — defensive at the boundary, same
    discipline as BackendCapability's int coercion."""
    d = ResourceDetail.from_wire(
        {
            "urn": "x",
            "family": "claimable",
            "kind": "usb_interface",
            "fidelity": "live",
            "attrs": {"count": 42, "ready": True},
        }
    )
    assert d.attr("count") == "42"
    assert d.attr("ready") == "True"


# ============================================================================
# BackendCapability (FEEDBACK SDK-05) — round out coverage retroactively
# ============================================================================


def test_backend_capability_is_frozen_and_hashable() -> None:
    """Same DP-03 contract: frozen + hashable for Qt-model-row use."""
    b = BackendCapability.from_wire(
        {
            "scope_prefix": "hw:/pci/",
            "backend_name": "mgf-hrb-linux",
            "resource_count": 36,
            "mode": "full",
            "ops": {"enumerate": True, "claim": True},
        }
    )
    with pytest.raises((AttributeError, TypeError)):
        b.mode = "mutated"  # type: ignore[misc]
    s = {b}
    assert b in s


def test_backend_capability_ops_tuple_is_sorted() -> None:
    """`ops` projects to a sorted tuple — deterministic rendering
    across consumers (no hash-order surprises in GUI dumps)."""
    b = BackendCapability.from_wire(
        {
            "scope_prefix": "x",
            "backend_name": "x",
            "resource_count": 0,
            "ops": {"watch": True, "acquire": False, "claim": True},
        }
    )
    keys = [k for k, _ in b.ops]
    assert keys == sorted(keys)


def test_backend_capability_resource_count_negative_string_accepted() -> None:
    """`_optional_int` parses any int-like string; a negative count
    is accepted (semantic check would be at the consumer's render
    layer, not at the seam). This pins the projection's behaviour
    so future tightening is explicit."""
    b = BackendCapability.from_wire(
        {
            "scope_prefix": "x",
            "backend_name": "x",
            "resource_count": "-3",
        }
    )
    assert b.resource_count == -3


def test_backend_capability_resource_count_bool_does_not_become_one() -> None:
    """bool is int-subclass-y in Python; the seam excludes it so a
    stray True doesn't silently become resource_count=1. Pins the
    defensive coercion's most surprising case."""
    b = BackendCapability.from_wire(
        {
            "scope_prefix": "x",
            "backend_name": "x",
            "resource_count": True,
        }
    )
    # Falls back to default (0), not 1.
    assert b.resource_count == 0


def test_backend_capability_completely_empty_dict_doesnt_crash() -> None:
    """The older-broker-no-fields path. Defensive coercion at every
    field — None and missing keys land on sensible defaults."""
    b = BackendCapability.from_wire({})
    assert b.scope_prefix == ""
    assert b.backend_name == ""
    assert b.resource_count == 0
    assert b.backend_version is None
    assert b.mode is None
    assert b.ops == ()


# ============================================================================
# LeaseRecord (FEEDBACK SDK-03) — round out coverage retroactively
# ============================================================================


def test_lease_record_is_frozen_and_hashable() -> None:
    """Frozen + slots → hashable. Pinned for the same Qt-model-row
    contract the other two records have."""
    r = LeaseRecord(
        urn="hw:/test/dev0",
        lease_id=1,
        mode=ClaimMode.EXCLUSIVE,
        holder=42,
        granted_at_ms=100,
    )
    with pytest.raises((AttributeError, TypeError)):
        r.lease_id = 999  # type: ignore[misc]
    s = {r}
    assert r in s


def test_lease_record_equality_is_field_based() -> None:
    """Dataclass equality: two records with the same fields compare
    equal even if constructed separately. Lets a GUI dedupe across
    polling cycles by record identity."""
    a = LeaseRecord(urn="x", lease_id=1, mode=ClaimMode.EXCLUSIVE, holder=1, granted_at_ms=0)
    b = LeaseRecord(urn="x", lease_id=1, mode=ClaimMode.EXCLUSIVE, holder=1, granted_at_ms=0)
    c = LeaseRecord(urn="x", lease_id=1, mode=ClaimMode.EXCLUSIVE, holder=2, granted_at_ms=0)
    assert a == b
    assert a != c
    # Hash respects equality.
    assert hash(a) == hash(b)
    assert hash(a) != hash(c)
