"""R1 / USB-02 Slice B closure pins — claim + raw against USB URNs.

The Zig USB worker (`hrb-code/src/linux_usb.zig`, lines
1055-1535) already implements the full Slice B surface:
`vAcquire` (CLAIMINTERFACE), `vAcquireDetach` (atomic
DISCONNECT_CLAIM), `vRelease`, `vRawChannel`, `vFdForToken`.
The errno → Err mapping is at lines 489 + 1273 + 1288:
EACCES/EPERM → Err.NotPermitted.

These pins prove the SDK ↔ broker ↔ USB-worker round-trip
works end-to-end for the error paths a non-root client hits
(the happy path lives in `tests/e2e/test_r1_usb_claim_happy.py`
gated by root + a willing USB device).

Same shape as G1's MODEL-01 conformance pin (commit `3a56a6a`):
the underlying Zig is shipped; what was missing is the
Python-side regression pin that proves the wire path holds.

Marker: `contract` — the wire-shape pins below don't need root
or a specific device, they exercise the error-path routing
that every non-root invocation of `Client.claim(usb_urn)` will
take.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import pytest

from mgf.hrb import ClaimMode
from mgf.hrb.exceptions import (
    NotFoundError,
    NotPermittedError,
)
from tests.contract.conftest import rt

if TYPE_CHECKING:
    import socket

    from mgf.hrb import Client

pytestmark = pytest.mark.contract


def test_claim_against_nonexistent_usb_urn_is_not_found(broker: Client) -> None:
    """R1 acceptance: an unrouted USB URN MUST surface as
    NotFoundError. Catches the path where the URN's bus / port
    chain references a device the kernel never enumerated."""
    with pytest.raises(NotFoundError):
        broker.claim(
            "hw:/usb/bus99/port-99/dev/if0",  # nonexistent bus
            ClaimMode.EXCLUSIVE,
        )


def test_claim_against_real_usb_urn_without_root_is_not_permitted(
    broker: Client,
) -> None:
    """R1 acceptance: a real USB URN claim from a non-root
    client surfaces as NotPermittedError (the kernel returns
    EACCES on USBDEVFS open; `linux_usb.zig:489` maps EACCES to
    `Err.NotPermitted`; the SDK's `from_wire` translates to
    `NotPermittedError`).

    This pins the FULL wire round-trip across the USB worker:
    broker accepts the cap_id, routes to the USB worker, hits
    EACCES on the USBDEVFS open, returns the typed wire error.
    Pre-R1 (today, but no pin) there was no test proving this
    chain held end-to-end against a USB URN — only against the
    `hw:/test/` synthetic worker."""
    usb_urns = broker.list("hw:/usb/**")
    if not usb_urns:
        pytest.skip("no USB devices on this host")

    # Pick the first USB URN. Non-root → expect NotPermittedError.
    # A container can expose USB entries in sysfs (so list() returns
    # URNs) WITHOUT the matching /dev/bus/usb device node, so the claim
    # routes to NotFound instead of reaching the USBDEVFS-open EACCES
    # path this pin targets. Skip honestly there — we genuinely can't
    # exercise the NotPermitted path without a claimable device — while
    # still failing loudly if a non-root claim unexpectedly SUCCEEDS.
    try:
        broker.claim(usb_urns[0], ClaimMode.EXCLUSIVE)
    except NotPermittedError:
        pass  # the EACCES → NotPermitted path this pin asserts
    except NotFoundError:
        pytest.skip(
            "listed USB URN has no claimable device node "
            "(sysfs visible without /dev/bus/usb, e.g. in a container)"
        )
    else:
        pytest.fail("non-root claim unexpectedly succeeded (expected NotPermitted)")


def test_claim_request_carries_cap_id_for_usb_urn(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    """Wire-level pin: the claim request against a USB URN MUST
    include the `cap_id` field per the post-G2 unification.
    Pre-G2 the wire field was `cap`; G2 unified to `cap_id`. R1's
    USB path follows the same shape — this pin catches if a
    future refactor accidentally reintroduces the legacy
    field-name on USB-specific code paths."""
    sock, cap_id = raw_broker_with_cap
    list_resp = rt(sock, {"op": "list", "scope": "hw:/usb/**"})
    urns = list_resp.get("resources", [])
    if not urns:
        pytest.skip("no USB devices on this host")
    # Send the canonical post-G2 request shape (with cap_id, not cap).
    resp = rt(
        sock,
        {
            "op": "claim",
            "urn": urns[0],
            "mode": "exclusive",
            "holder": 1,
            "cap_id": cap_id,  # canonical name post-G2
        },
    )
    # The point of this pin isn't the outcome — it's that the request
    # SHAPE is accepted (the cap_id field routes past the wire gate, no
    # BadRequest). With a valid cap the request clears the cap check and
    # reaches the device: NotPermitted (EACCES) on a host with a real
    # claimable USB device, NotFound where sysfs is visible but the
    # /dev/bus/usb node isn't (e.g. a container). Either proves cap_id
    # was accepted; only BadRequest (the field was rejected) fails.
    assert resp.get("ok") is False
    assert resp.get("err") in {"NotPermitted", "NotFound"}, (
        f"USB claim with cap_id should route past the wire gate "
        f"(NotPermitted or NotFound), not BadRequest; got: {resp!r}"
    )


def test_legacy_cap_field_no_longer_accepted_on_usb_claim(
    raw_broker_with_cap: tuple[socket.socket, int],
) -> None:
    """Negative pin for G2 — the legacy `cap` field is no longer
    accepted. A request using the pre-G2 name should fail with
    BadRequest (the gate rejects the missing required cap_id),
    not silently succeed against an unguarded path."""
    sock, cap_id = raw_broker_with_cap
    list_resp = rt(sock, {"op": "list", "scope": "hw:/usb/**"})
    urns = list_resp.get("resources", [])
    if not urns:
        pytest.skip("no USB devices on this host")
    # Send with the LEGACY `cap` field (not cap_id).
    resp = rt(
        sock,
        {
            "op": "claim",
            "urn": urns[0],
            "mode": "exclusive",
            "holder": 1,
            "cap": cap_id,  # legacy name — post-G2 this is unknown
        },
    )
    # cap_id defaults to 0; cap-checked claim against the kernel
    # fails NotPermitted (no cap presented). EITHER outcome
    # proves the legacy field isn't silently accepted (the
    # request neither errors with BadRequest nor succeeds).
    assert resp.get("ok") is False
    # Don't pin the exact err code — depends on whether the
    # broker treats missing cap_id as "no cap" (NotPermitted on
    # the cap check) or "invalid cap" (also NotPermitted). The
    # ASSERTION is: the legacy field is NOT consumed.
    assert resp.get("err") in {"NotPermitted", "BadRequest"}, (
        f"legacy `cap` field should not be silently accepted; got err={resp.get('err')!r}"
    )


def test_release_against_unknown_lease_on_usb_urn_is_idempotent(
    broker: Client,
) -> None:
    """Cross-pin: releasing against a URN that the USB worker
    ROUTES but doesn't hold a lease for is idempotent success
    (Part 6.3 lease-engine contract).

    Distinguished from J1's NotFound path (commit `8f48398`):
    J1 fires when NO worker prefix matches (e.g.,
    `hw:/never-existed/...`); this path tests "prefix matches
    USB worker, but no lease with this id exists." The two
    paths must behave differently — J1 NotFound for routing
    misses, idempotent success for lease misses within a known
    worker."""
    # hw:/usb/bus99/... routes to the USB worker (prefix match)
    # but no lease exists for that URN/lease_id pair.
    broker.release(
        "hw:/usb/bus99/port-99/dev/if0",
        lease=42,
    )  # no exception — idempotent success per Part 6.3
