"""R-T1-11B (survivability §3.6, Tier-1 #11) — capability-filtered
enumeration on ``list_leases`` / ``dump_all`` / ``inspect_full``.

R-T1-11A closed the S3 isolation gap for the ``list`` op. This is the
follow-up that wires the SAME mechanism (``Facade.capPermitsNaming`` +
the per-op collector filter) into the remaining enumeration-shaped ops:

  * ``dump_all`` — scope-shaped; URNs outside the cap's scope are dropped
    during pass-1 enumeration, so they're never inspected nor emitted.
  * ``list_leases`` — scope-shaped; lease records whose URN is outside
    the cap's scope are dropped before emission.
  * ``inspect_full`` — SINGLE root URN; when the root is outside the
    cap's scope the broker returns ``NotFound`` so the holder of an
    attenuated cap can't distinguish an out-of-scope URN from a
    nonexistent one (existence itself is not observable beyond the
    grant).

These tests exercise the WIRE shape end-to-end. As with R-T1-11A the
SDK doesn't yet pass ``cap_id`` on these ops — that's R-T1-11C. The
contract pin proves the broker honors the field on the wire so the SDK
refactor doesn't need broker-side changes.

The scope-narrowing filter itself (in-scope kept / out-of-scope dropped)
is pinned at the facade layer by R-T1-11A's ``facade_test.zig`` suite
(``capPermitsNaming`` in/out tests) — the mechanism is shared. What
these wire tests add is per-op proof that (a) the parser accepts the
field, (b) the collector wiring drops out-of-scope URNs, and (c)
``inspect_full``'s distinctive NotFound-on-out-of-scope shape holds.

Limitation pinned honestly: ``list_leases`` filtering is not
wire-observable at broker startup because there are zero live leases
(nothing has been claimed). Its field-acceptance + back-compat are
pinned here; the narrowing behavior rides on the shared
``capPermitsNaming`` Zig pin + the ``LeaseJsonWriter.push`` filter.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import pytest

from tests.contract.conftest import rt

if TYPE_CHECKING:
    import socket

pytestmark = pytest.mark.contract


# ---------------------------------------------------------------------------
# dump_all — scope-shaped, real resources exist (PCI), so the filter is
# directly wire-observable: unknown cap_id ⇒ zero items even though the
# host has live resources.
# ---------------------------------------------------------------------------


def test_dump_all_with_zero_cap_id_preserves_legacy_unfiltered_behavior(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    """Omitted cap_id and explicit cap_id=0 must be equivalent — the
    documented sentinel for 'no cap presented' (back-compat guarantee)."""
    sock, _root_cap = raw_broker_with_cap
    resp_omit = rt(sock, {"op": "dump_all", "scope": "hw:/**"})
    resp_zero = rt(sock, {"op": "dump_all", "scope": "hw:/**", "cap_id": 0})
    assert resp_omit.get("ok") is True
    assert resp_zero.get("ok") is True
    assert resp_omit.get("count") == resp_zero.get("count"), (
        "omitted cap_id and cap_id=0 should be equivalent "
        "(sentinel documented in request.zig::DumpAllReq)"
    )


def test_dump_all_with_root_cap_returns_full_set(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    """Root cap scope is hw:/** — the intersection with any sub-scope is
    the request itself, so dump_all under it must NOT narrow."""
    sock, root_cap = raw_broker_with_cap
    resp_no_cap = rt(sock, {"op": "dump_all", "scope": "hw:/**"})
    resp_root = rt(sock, {"op": "dump_all", "scope": "hw:/**", "cap_id": root_cap})
    assert resp_no_cap.get("ok") is True
    assert resp_root.get("ok") is True
    no_cap_urns = {item["urn"] for item in resp_no_cap.get("items", [])}
    root_urns = {item["urn"] for item in resp_root.get("items", [])}
    assert no_cap_urns == root_urns, (
        "root cap should NOT narrow the dump; the intersection is the request"
    )


def test_dump_all_with_unknown_cap_id_returns_empty(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    """The S3 isolation pin: an unknown cap_id resolves to no scope, so
    capPermitsNaming rejects every URN ⇒ zero items — EVEN THOUGH the
    host has live resources (dump under the root cap is non-empty). The
    holder of an unforgeable-but-unknown id can't observe the existence
    of any resource. No BadRequest either (that would itself leak that
    the id isn't a real cap)."""
    sock, root_cap = raw_broker_with_cap
    baseline = rt(sock, {"op": "dump_all", "scope": "hw:/**", "cap_id": root_cap})
    assert baseline.get("count", 0) > 0, (
        "precondition: the host must expose at least one resource for this "
        "pin to be meaningful; root-cap dump came back empty"
    )
    resp = rt(sock, {"op": "dump_all", "scope": "hw:/**", "cap_id": 0xDEAD_BEEF})
    assert resp.get("ok") is True, f"unknown cap_id should not error; got {resp!r}"
    assert resp.get("count") == 0, (
        f"unknown cap_id should yield zero items (no existence leak) even "
        f"though {baseline.get('count')} resources exist; got {resp.get('count')!r}"
    )
    assert resp.get("items") == []


# ---------------------------------------------------------------------------
# inspect_full — single root URN. The S3 check is on the root: out-of-scope
# ⇒ NotFound (indistinguishable from nonexistent).
# ---------------------------------------------------------------------------


def _first_urn(sock: socket.socket) -> str:
    resp = rt(sock, {"op": "list", "scope": "hw:/**"})
    urns = resp.get("resources", [])
    if not urns:
        pytest.skip("host exposes no resources; inspect_full pin needs a real URN")
    return urns[0]


def test_inspect_full_with_zero_cap_id_preserves_legacy_behavior(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    """Omitted cap_id and explicit cap_id=0 are equivalent — both run the
    legacy unfiltered path (the root is always inspectable)."""
    sock, _root_cap = raw_broker_with_cap
    urn = _first_urn(sock)
    resp_omit = rt(sock, {"op": "inspect_full", "urn": urn})
    resp_zero = rt(sock, {"op": "inspect_full", "urn": urn, "cap_id": 0})
    assert resp_omit.get("ok") is True
    assert resp_zero.get("ok") is True


def test_inspect_full_with_root_cap_succeeds(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    """Root cap scope hw:/** contains every URN, so inspect_full of a real
    URN under it succeeds (the cap filter applies but permits the root)."""
    sock, root_cap = raw_broker_with_cap
    urn = _first_urn(sock)
    resp = rt(sock, {"op": "inspect_full", "urn": urn, "cap_id": root_cap})
    assert resp.get("ok") is True, f"root cap should permit naming {urn}; got {resp!r}"
    assert "tree" in resp


def test_inspect_full_with_unknown_cap_id_is_not_found(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    """The distinctive single-URN S3 pin: an unknown cap_id can't name the
    root URN, so the broker returns NotFound — the holder can't even tell
    the URN exists. Note this differs from the scope-shaped ops (which
    return an empty set): for a single-target op the only honest 'you
    can't see this' answer is the same one a nonexistent URN gets."""
    sock, _root_cap = raw_broker_with_cap
    urn = _first_urn(sock)
    # Sanity: the URN really does exist (resolves with no cap filter).
    assert rt(sock, {"op": "inspect_full", "urn": urn}).get("ok") is True
    resp = rt(sock, {"op": "inspect_full", "urn": urn, "cap_id": 0xDEAD_BEEF})
    assert resp.get("ok") is False, (
        f"unknown cap_id naming a real URN must not succeed; got {resp!r}"
    )
    assert resp.get("err") == "NotFound", (
        f"out-of-scope inspect_full must be indistinguishable from a "
        f"nonexistent URN (NotFound), not a NotPermitted leak; got {resp!r}"
    )


# ---------------------------------------------------------------------------
# list_leases — scope-shaped, but zero live leases at startup, so the
# filter isn't wire-observable here. Pin field-acceptance + back-compat;
# the narrowing rides on the shared capPermitsNaming Zig pin.
# ---------------------------------------------------------------------------


def test_list_leases_accepts_cap_id_field_and_is_backcompat(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    """The parser accepts cap_id on list_leases and the field is
    back-compatible: omitted, cap_id=0, and a real root cap all return a
    well-formed ok response. (Filter narrowing needs a live lease to be
    wire-observable — covered by the shared capPermitsNaming pin.)"""
    sock, root_cap = raw_broker_with_cap
    resp_omit = rt(sock, {"op": "list_leases", "scope": "hw:/**"})
    resp_zero = rt(sock, {"op": "list_leases", "scope": "hw:/**", "cap_id": 0})
    resp_root = rt(sock, {"op": "list_leases", "scope": "hw:/**", "cap_id": root_cap})
    for resp in (resp_omit, resp_zero, resp_root):
        assert resp.get("ok") is True, f"list_leases should accept cap_id; got {resp!r}"
        assert isinstance(resp.get("leases"), list)
    assert resp_omit.get("leases") == resp_zero.get("leases"), (
        "omitted cap_id and cap_id=0 must be equivalent on list_leases"
    )


def test_list_leases_with_unknown_cap_id_does_not_error(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    """An unknown cap_id on list_leases yields an empty (filtered) set
    without erroring — same no-BadRequest-leak discipline as list /
    dump_all. (Empty here is also the startup baseline; the meaningful
    narrowing is the shared filter, pinned in facade_test.zig.)"""
    sock, _root_cap = raw_broker_with_cap
    resp = rt(sock, {"op": "list_leases", "scope": "hw:/**", "cap_id": 0xDEAD_BEEF})
    assert resp.get("ok") is True, f"unknown cap_id should not error; got {resp!r}"
    assert resp.get("leases") == []
