"""R-T1-11A (survivability §3.6) — capability-filtered enumeration on `list`.

The S3 isolation gap: pre-R-T1-11A, `list` returned every URN matching
the requested scope regardless of the caller's cap. A tenant could
observe the existence of resources outside its grant.

Post-R-T1-11A, the broker accepts an optional ``cap_id`` on the list
request. When non-zero, the broker intersects the requested scope with
the cap's scope before emitting — URNs outside the cap are silently
filtered out (the caller can't even *observe the existence* of
resources beyond its grant).

These tests exercise the WIRE shape end-to-end. The SDK's
``Client.list()`` doesn't yet pass ``cap_id`` — that's a follow-up
(R-T1-11B). For now the contract pin proves the broker honors the
field on the wire, so SDK refactors that wire it through don't need
broker-side changes.
"""

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


def test_list_with_zero_cap_id_preserves_legacy_unfiltered_behavior(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    """Legacy clients (no cap_id on list) see the full result set —
    the back-compat guarantee. Pin: cap_id default 0 ⇒ no filter."""
    sock, _root_cap = raw_broker_with_cap
    # Cap layer IS active (the broker mints a root cap), but the
    # request omits cap_id. Expected: full result set.
    resp_omit = rt(sock, {"op": "list", "scope": "hw:/**"})
    assert resp_omit.get("ok") is True
    urns_omit = resp_omit.get("resources", [])

    # Same request with explicit cap_id=0 must produce identical
    # output (0 is the documented sentinel for "no cap presented").
    resp_zero = rt(sock, {"op": "list", "scope": "hw:/**", "cap_id": 0})
    assert resp_zero.get("ok") is True
    urns_zero = resp_zero.get("resources", [])
    assert urns_omit == urns_zero, (
        "omitted cap_id and cap_id=0 should be equivalent (the "
        "sentinel meaning is documented in request.zig::ListReq)"
    )


def test_list_with_root_cap_returns_full_set(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    """Root cap has scope `hw:/**` — listing under any sub-scope MUST
    return the full set (cap ∩ request = request). Pin: the filter
    is intersection, not replacement."""
    sock, root_cap = raw_broker_with_cap
    resp_no_cap = rt(sock, {"op": "list", "scope": "hw:/**"})
    resp_root_cap = rt(
        sock,
        {
            "op": "list",
            "scope": "hw:/**",
            "cap_id": root_cap,
        },
    )
    assert resp_no_cap.get("ok") is True
    assert resp_root_cap.get("ok") is True
    # Root cap doesn't narrow the result set (its scope is hw:/**).
    assert set(resp_no_cap.get("resources", [])) == set(resp_root_cap.get("resources", [])), (
        "root cap should NOT narrow the result set; the intersection is the request"
    )


def test_list_with_unknown_cap_id_returns_empty(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    """A cap_id that doesn't resolve to a real capability is a
    forgery probe. The filter rejects every URN ⇒ empty result.
    Pin: the broker doesn't leak the existence of URNs to a
    holder of an unknown cap_id, AND it doesn't crash / raise
    BadRequest just for not finding the cap (that would be a
    leak in the other direction: confirming "this id isn't a
    real cap")."""
    sock, _root_cap = raw_broker_with_cap
    resp = rt(
        sock,
        {
            "op": "list",
            "scope": "hw:/**",
            "cap_id": 0xDEAD_BEEF,  # not a minted id
        },
    )
    assert resp.get("ok") is True, (
        f"unknown cap_id should not error; the absence of results IS the response. Got: {resp!r}"
    )
    assert resp.get("resources") == [], (
        f"unknown cap_id should yield empty results (no URN leak); got: {resp.get('resources')!r}"
    )
