"""Contract: the broker's ``dump_chain`` wire op (Part 13 §5.5.2).

The wire contract pinned here:

  * Op name is ``dump_chain``. Required field: ``urn`` (string).
    No other fields.
  * Success response carries: ``ok:true``, ``urn`` (echoed),
    ``chain`` (ordered JSON array, leaf-first), ``fidelity``
    (``"live"`` today), ``captured_at_ms`` (broker monotonic ms),
    ``truncated`` (bool).
  * Each ``chain`` element is an object with three string keys:
    ``urn``, ``level``, ``summary``. ``level`` is one of a closed
    set: ``"interface"`` / ``"device"`` / ``"hub"`` /
    ``"root_hub"`` / ``"controller"`` / ``"bridge"`` /
    ``"root_complex"`` (the seven topology tags the v1 walkers
    emit). ``summary`` content is human text; its SHAPE is not
    wire-pinned.
  * The first element's ``urn`` MUST equal the request's ``urn``
    (leaf-first ordering; the queried URN is always rung 0).
  * URN that no worker owns → NotFound. URN whose backend lacks
    the op (fake backend by design) → Unsupported. Missing ``urn``
    or non-string ``urn`` → BadRequest at the parse gate.
  * For USB interface URNs: the chain ALWAYS ends with a
    ``"controller"`` rung whose URN is a synthetic
    ``hw:/pci/<bdf>/fn0`` PCI URN (the USB host controller).
  * For PCI URNs: the chain ALWAYS ends with a ``"root_complex"``
    rung whose URN is a synthetic ``hw:/pci/root/pci<dom>:<bus>``.

These pins live in their own file because dump_chain has a
distinct response shape (array of chain entries) that scattering
across the broader use-case suite would obscure.

Sub-phase 1B/3 of Part 13.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import pytest

from tests.contract.conftest import rt

if TYPE_CHECKING:
    import socket

    from mgf.hrb import Client

pytestmark = pytest.mark.contract

VALID_LEVELS = {
    "interface",
    "device",
    "hub",
    "root_hub",
    "controller",
    "bridge",
    "root_complex",
}


def _first_real_urn(sock: socket.socket, scope: str) -> str | None:
    """Pick the first URN matching a scope, or None when no devices
    of that family exist on this host."""
    resp = rt(sock, {"op": "list", "scope": scope})
    assert resp.get("ok") is True
    urns = resp.get("resources", [])
    return urns[0] if urns else None


# ---------------------------------------------------------------------------
# Shape pins.
# ---------------------------------------------------------------------------


def test_success_shape_top_level_keys(raw_broker: socket.socket) -> None:
    """Success response MUST carry the documented top-level keys
    exactly. Future additive fields are fine; missing the pinned
    set breaks the contract."""
    urn = _first_real_urn(raw_broker, "hw:/usb/**") or _first_real_urn(raw_broker, "hw:/pci/**")
    if urn is None:
        pytest.skip("no real USB/PCI devices on this host")
    resp = rt(raw_broker, {"op": "dump_chain", "urn": urn})
    assert resp.get("ok") is True
    assert resp.get("urn") == urn
    assert resp.get("fidelity") == "live"
    assert isinstance(resp.get("captured_at_ms"), int)
    assert resp["captured_at_ms"] > 0
    assert isinstance(resp.get("chain"), list)
    assert resp.get("truncated") is False


def test_chain_entries_have_three_string_fields(
    raw_broker: socket.socket,
) -> None:
    """Each chain entry MUST have ``urn``, ``level``, and
    ``summary`` — all strings. No extra fields are pinned (a
    backend MAY add additive keys later)."""
    urn = _first_real_urn(raw_broker, "hw:/usb/**") or _first_real_urn(raw_broker, "hw:/pci/**")
    if urn is None:
        pytest.skip("no real USB/PCI devices on this host")
    resp = rt(raw_broker, {"op": "dump_chain", "urn": urn})
    chain = resp["chain"]
    assert len(chain) > 0
    for entry in chain:
        assert isinstance(entry.get("urn"), str)
        assert isinstance(entry.get("level"), str)
        assert isinstance(entry.get("summary"), str)


def test_first_rung_is_the_queried_urn(raw_broker: socket.socket) -> None:
    """Leaf-first ordering: the chain's first element MUST be the
    queried URN. Anything else is a contract break (the consumer
    relies on `chain[0]` being the device they asked about)."""
    urn = _first_real_urn(raw_broker, "hw:/usb/**") or _first_real_urn(raw_broker, "hw:/pci/**")
    if urn is None:
        pytest.skip("no real USB/PCI devices on this host")
    resp = rt(raw_broker, {"op": "dump_chain", "urn": urn})
    assert resp["chain"][0]["urn"] == urn


def test_levels_are_from_the_closed_set(raw_broker: socket.socket) -> None:
    """`level` is wire-pinned to the v1 closed set. A backend
    inventing a new level value at this layer (without bumping the
    contract) is the kind of drift this pin catches."""
    urn = _first_real_urn(raw_broker, "hw:/usb/**") or _first_real_urn(raw_broker, "hw:/pci/**")
    if urn is None:
        pytest.skip("no real USB/PCI devices on this host")
    resp = rt(raw_broker, {"op": "dump_chain", "urn": urn})
    for entry in resp["chain"]:
        assert entry["level"] in VALID_LEVELS, f"unknown level {entry['level']!r}"


# ---------------------------------------------------------------------------
# USB-specific chain shape.
# ---------------------------------------------------------------------------


def test_usb_chain_ends_at_pci_controller(
    raw_broker: socket.socket,
) -> None:
    """USB interface chains MUST terminate at a PCI controller
    rung — the host controller the USB stack hangs off of. The
    walker reads the PCI BDF out of the root hub's sysfs symlink
    and emits a synthetic ``hw:/pci/<bdf>/fn0`` URN."""
    urn = _first_real_urn(raw_broker, "hw:/usb/**")
    if urn is None:
        pytest.skip("no real USB devices on this host")
    resp = rt(raw_broker, {"op": "dump_chain", "urn": urn})
    chain = resp["chain"]
    tail = chain[-1]
    assert tail["level"] == "controller", (
        f"USB chain should end at 'controller', got {tail['level']!r}"
    )
    assert tail["urn"].startswith("hw:/pci/"), (
        f"controller URN should be a PCI URN, got {tail['urn']!r}"
    )


def test_usb_chain_carries_a_root_hub_rung(
    raw_broker: socket.socket,
) -> None:
    """Every USB interface chain MUST carry exactly one rung at
    the ``root_hub`` level — the second-to-last rung if the
    controller is present, or the last rung if the controller
    couldn't be resolved."""
    urn = _first_real_urn(raw_broker, "hw:/usb/**")
    if urn is None:
        pytest.skip("no real USB devices on this host")
    resp = rt(raw_broker, {"op": "dump_chain", "urn": urn})
    levels = [e["level"] for e in resp["chain"]]
    assert levels.count("root_hub") == 1, (
        f"expected exactly one root_hub rung, got levels={levels!r}"
    )


def test_usb_first_rung_level_is_interface(
    raw_broker: socket.socket,
) -> None:
    """USB URNs are interface URNs by design; the chain's leaf
    rung MUST be tagged ``"interface"`` accordingly."""
    urn = _first_real_urn(raw_broker, "hw:/usb/**")
    if urn is None:
        pytest.skip("no real USB devices on this host")
    resp = rt(raw_broker, {"op": "dump_chain", "urn": urn})
    assert resp["chain"][0]["level"] == "interface"


# ---------------------------------------------------------------------------
# PCI-specific chain shape.
# ---------------------------------------------------------------------------


def test_pci_chain_ends_at_root_complex(
    raw_broker: socket.socket,
) -> None:
    """PCI chains MUST terminate at a ``root_complex`` rung — the
    `pci<domain>:<bus>` component at the top of `/sys/devices/`.
    The terminal URN MUST be the synthetic `hw:/pci/root/...`."""
    urn = _first_real_urn(raw_broker, "hw:/pci/**")
    if urn is None:
        pytest.skip("no real PCI devices on this host")
    resp = rt(raw_broker, {"op": "dump_chain", "urn": urn})
    chain = resp["chain"]
    tail = chain[-1]
    assert tail["level"] == "root_complex", (
        f"PCI chain should end at 'root_complex', got {tail['level']!r}"
    )
    assert tail["urn"].startswith("hw:/pci/root/pci"), (
        f"root_complex URN should be hw:/pci/root/pci...; got {tail['urn']!r}"
    )


def test_pci_first_rung_level_is_device(
    raw_broker: socket.socket,
) -> None:
    """PCI URNs are PCI function URNs; the leaf rung MUST be
    tagged ``"device"``."""
    urn = _first_real_urn(raw_broker, "hw:/pci/**")
    if urn is None:
        pytest.skip("no real PCI devices on this host")
    resp = rt(raw_broker, {"op": "dump_chain", "urn": urn})
    assert resp["chain"][0]["level"] == "device"


def test_pci_bridges_only_between_device_and_root(
    raw_broker: socket.socket,
) -> None:
    """``bridge`` rungs MUST appear (when present) STRICTLY
    between the leaf ``device`` rung and the terminal
    ``root_complex`` — never before the device, never after the
    root complex."""
    urn = _first_real_urn(raw_broker, "hw:/pci/**")
    if urn is None:
        pytest.skip("no real PCI devices on this host")
    resp = rt(raw_broker, {"op": "dump_chain", "urn": urn})
    levels = [e["level"] for e in resp["chain"]]
    # First is device; last is root_complex; bridges (0 or more) in between.
    assert levels[0] == "device"
    assert levels[-1] == "root_complex"
    for mid in levels[1:-1]:
        assert mid == "bridge", f"unexpected intermediate level {mid!r} in PCI chain {levels!r}"


# ---------------------------------------------------------------------------
# Error pins.
# ---------------------------------------------------------------------------


def test_unknown_urn_is_not_found(raw_broker: socket.socket) -> None:
    """A URN no worker owns yields ``ok:false, err:'NotFound'``."""
    resp = rt(raw_broker, {"op": "dump_chain", "urn": "hw:/never-existed"})
    assert resp.get("ok") is False
    assert resp.get("err") == "NotFound"


def test_fake_backend_is_unsupported(raw_broker: socket.socket) -> None:
    """The fake backend has no parent-chain topology — by design
    the op returns Unsupported rather than fabricating a chain."""
    resp = rt(raw_broker, {"op": "dump_chain", "urn": "hw:/test/dev0"})
    assert resp.get("ok") is False
    assert resp.get("err") == "Unsupported"


def test_missing_urn_is_bad_request(raw_broker: socket.socket) -> None:
    """The wire requires ``urn`` — omission is BadRequest, not
    NotFound on an empty URN."""
    resp = rt(raw_broker, {"op": "dump_chain"})
    assert resp.get("ok") is False
    assert resp.get("err") == "BadRequest"


def test_non_string_urn_is_bad_request(raw_broker: socket.socket) -> None:
    """``urn`` MUST be a string — numeric / boolean / null all
    BadRequest at the gate."""
    for bad in (42, True, None, ["hw:/usb"]):
        resp = rt(raw_broker, {"op": "dump_chain", "urn": bad})
        assert resp.get("ok") is False, f"urn={bad!r} should reject"
        assert resp.get("err") == "BadRequest", f"urn={bad!r}: {resp}"


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


def test_sdk_projection_matches_wire(broker: Client) -> None:
    """``Client.dump_chain()`` returns a typed
    :class:`DumpChainResult` whose fields mirror the wire dict
    1:1. Same broker, two paths (raw + SDK), same content."""
    urns = broker.list("hw:/usb/**") or broker.list("hw:/pci/**")
    if not urns:
        pytest.skip("no real USB/PCI devices on this host")
    result = broker.dump_chain(urns[0])
    assert result.urn == urns[0]
    assert result.fidelity == "live"
    assert result.captured_at_ms > 0
    assert result.truncated is False
    assert isinstance(result.chain, tuple)
    assert len(result.chain) > 0
    assert result.chain[0].urn == urns[0]
    for entry in result.chain:
        assert entry.level in VALID_LEVELS
