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

The wire contract pinned here:

  * Op name is ``inspect_full``. Required fields: ``urn``. Optional:
    ``depth`` (one of ``"shallow"`` / ``"all"``; defaults to ``"all"``).
  * Success response carries: ``ok:true``, ``urn``, ``fidelity``,
    ``captured_at_ms`` (broker monotonic ms), ``tree`` (nested dict),
    ``truncated`` (bool).
  * ``tree`` keys: string attribute name → one of
      - ``str``                              (regular sysfs attribute)
      - nested ``dict``                      (sub-directory; recursive)
      - ``{"_symlink": "<target>"}``          (symlink; never followed)
      - ``{"_binary": true}``                (binary blob; bytes elided)
  * ``depth="shallow"`` returns the URN's directory only; ``"all"``
    recurses. Any other value → BadRequest at the parse gate.
  * URN that no worker owns → NotFound. URN whose backend lacks the
    op (fake backend by design) → Unsupported.
  * Default depth is ``"all"`` — omitting ``depth`` matches sending
    ``depth="all"``.

These pins live in their own file because the inspect_full grammar
is small but its shape is wider than the rest of the wire surface;
co-locating the depth / sentinel / fidelity / truncation pins in one
place beats scattering them across the broader use-case suite.

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

from __future__ import annotations

from typing import TYPE_CHECKING, Any

import pytest

from tests.contract.conftest import rt

if TYPE_CHECKING:
    import socket

    from mgf.hrb import Client

pytestmark = pytest.mark.contract


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 (CI without USB/PCI hardware)."""
    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 — every successful response carries these top-level keys.
# ---------------------------------------------------------------------------


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": "inspect_full", "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("tree"), dict)
    assert resp.get("truncated") is False


def test_default_depth_is_all(raw_broker: socket.socket) -> None:
    """Omitting ``depth`` MUST behave identically to ``depth='all'``."""
    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")
    r_default = rt(raw_broker, {"op": "inspect_full", "urn": urn})
    r_all = rt(raw_broker, {"op": "inspect_full", "urn": urn, "depth": "all"})
    # Tree keys identical and same count (values may differ across
    # captures of dynamic attrs, but the directory shape is stable).
    assert set(r_default["tree"].keys()) == set(r_all["tree"].keys())


def test_shallow_is_subset_of_all(raw_broker: socket.socket) -> None:
    """Shallow walks only attrs + symlinks in the URN's directory;
    ``all`` ALSO emits sub-directory keys (with nested content). So
    shallow's key-set MUST be a (non-strict) subset of all's. The
    extras in ``all`` are exactly the recursed sub-directory keys.

    Concretely, this pins: subdirs are present only when ``depth='all'``.
    A shallow walk MUST NOT emit empty stubs for skipped subdirs —
    that would leak the directory's existence without its content."""
    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")
    r_sh = rt(raw_broker, {"op": "inspect_full", "urn": urn, "depth": "shallow"})
    r_all = rt(raw_broker, {"op": "inspect_full", "urn": urn, "depth": "all"})
    sh_keys = set(r_sh["tree"].keys())
    all_keys = set(r_all["tree"].keys())
    assert sh_keys <= all_keys, f"shallow emitted keys not in all: {sh_keys - all_keys}"
    # Every shallow value is an attr (str), a symlink sentinel, or a
    # binary sentinel — never a nested dir.
    for key, val in r_sh["tree"].items():
        assert _is_leaf(val), f"shallow emitted non-leaf for {key!r}: {val!r}"
    # The extras in ``all`` (subdir entries) MUST be nested dicts,
    # not strings or sentinels.
    for extra in all_keys - sh_keys:
        val = r_all["tree"][extra]
        assert isinstance(val, dict), f"{extra!r} expected dict, got {type(val)}"
        assert not _is_sentinel(val), f"{extra!r} expected nested dir, got sentinel {val!r}"


def _is_sentinel(d: dict[str, Any]) -> bool:
    """True iff a dict node is one of the broker's two sentinels."""
    return d == {"_binary": True} or (set(d.keys()) == {"_symlink"})


def _is_leaf(val: Any) -> bool:
    """True iff a tree value is a leaf (string attr, symlink sentinel,
    or binary sentinel) — i.e. NOT a walked sub-directory."""
    if isinstance(val, str):
        return True
    if isinstance(val, dict) and _is_sentinel(val):
        return True
    return False


# ---------------------------------------------------------------------------
# Sentinel pins — symlinks and binary attributes use the documented shape.
# ---------------------------------------------------------------------------


def test_symlink_sentinel_shape(raw_broker: socket.socket) -> None:
    """Symlinks in the tree MUST render as ``{"_symlink": "<target>"}``
    and the target MUST be a non-empty string. The broker MUST NOT
    follow the symlink (would re-walk all of /sys via /sys/bus links)."""
    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": "inspect_full", "urn": urn})
    syms = [
        (k, v)
        for k, v in resp["tree"].items()
        if isinstance(v, dict) and set(v.keys()) == {"_symlink"}
    ]
    # Real sysfs device dirs always have at least one symlink
    # (driver, subsystem, ...) — if not, the device has been
    # unbound or the walker silently dropped the symlink.
    assert len(syms) > 0, "expected ≥1 symlink dirent in tree"
    for _, sym in syms:
        target = sym["_symlink"]
        assert isinstance(target, str)
        assert len(target) > 0


def test_no_symlink_target_is_swallowed_as_nested_dir(
    raw_broker: socket.socket,
) -> None:
    """If the broker were following symlinks, the tree would contain
    nested directories named ``driver``, ``subsystem``, etc. The
    contract is they remain SENTINELS — never bare nested dicts."""
    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": "inspect_full", "urn": urn})
    for key in ("driver", "subsystem"):
        if key in resp["tree"]:
            val = resp["tree"][key]
            assert isinstance(val, dict), f"{key!r} should be dict, got {type(val)}"
            assert set(val.keys()) == {"_symlink"}, (
                f"{key!r} should be a symlink sentinel, not a walked dir"
            )


# ---------------------------------------------------------------------------
# Error pins — bad URN, fake backend, bad depth value.
# ---------------------------------------------------------------------------


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": "inspect_full", "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 sysfs substrate — by design the op
    returns ``Unsupported`` rather than a fabricated tree. Honesty
    over convenience (G2)."""
    resp = rt(raw_broker, {"op": "inspect_full", "urn": "hw:/test/dev0"})
    assert resp.get("ok") is False
    assert resp.get("err") == "Unsupported"


def test_invalid_depth_is_bad_request(raw_broker: socket.socket) -> None:
    """Closed grammar: only ``"shallow"`` and ``"all"`` are valid."""
    urn = _first_real_urn(raw_broker, "hw:/test/**") or "hw:/test/dev0"
    resp = rt(raw_broker, {"op": "inspect_full", "urn": urn, "depth": "deep"})
    assert resp.get("ok") is False
    assert resp.get("err") == "BadRequest"


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


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": "inspect_full"})
    assert resp.get("ok") is False
    assert resp.get("err") == "BadRequest"


# ---------------------------------------------------------------------------
# SDK projection pin — the typed result mirrors the wire dict.
# ---------------------------------------------------------------------------


def test_sdk_projection_matches_wire(broker: Client) -> None:
    """``Client.inspect_full()`` returns a typed
    :class:`InspectFullResult` whose fields mirror the wire dict 1:1.
    Same broker, two paths (raw + SDK), same shape on the typed
    surface."""
    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.inspect_full(urns[0])
    assert result.urn == urns[0]
    assert result.fidelity == "live"
    assert result.captured_at_ms > 0
    assert isinstance(result.tree, dict)
    assert result.truncated is False
    # depth='shallow' projection — keys are a subset of all's.
    sh = broker.inspect_full(urns[0], depth="shallow")
    assert set(sh.tree.keys()) <= set(result.tree.keys())
