"""E2E: USB-02 Slice B happy path (R1 closure).

Root-gated + hardware-gated. Skips honestly when:
  * Not running as root (USBDEVFS open returns EACCES).
  * No USB devices on the host.
  * Every USB device has a driver bound + the test isn't
    invoked with the explicit `--detach-test-device` env flag.

The error paths (NotPermitted on EACCES, NotFound on unrouted
URN, BadRequest on legacy field names) are pinned in
`tests/contract/test_r1_usb_claim_paths.py` — those don't need
root. This file covers the part of R1 that CAN'T be tested
without privileges: a real USBDEVFS open + the SCM_RIGHTS fd
hand-off via `lease.raw()`.

The Zig backing (`hrb-code/src/linux_usb.zig` lines 1055-1535)
implements Phases U3a end-to-end: `vAcquire` does the
USBDEVFS_CLAIMINTERFACE; `vRawChannel` returns the fd;
`vFdForToken` plumbs the fd through SCM_RIGHTS; `vRelease`
issues USBDEVFS_RELEASEINTERFACE + close.

If you DO run as root + have a driver-free test device, this
test exercises the full claim → raw → release lifecycle.
"""

from __future__ import annotations

import os
from typing import TYPE_CHECKING

import pytest

from mgf.hrb import ClaimMode
from mgf.hrb.exceptions import (
    ConflictError,
    NotPermittedError,
)

if TYPE_CHECKING:
    from mgf.hrb import Client

pytestmark = pytest.mark.e2e


def _find_driver_free_usb_interface(broker: Client) -> str | None:
    """Pick a USB interface URN whose kernel driver is unbound.
    A claim against a driver-bound interface returns EBUSY →
    `ConflictError` — that's tested separately. The happy path
    needs an interface where the kernel hasn't already opened it."""
    from pathlib import Path

    for urn in broker.list("hw:/usb/**"):
        # URN: hw:/usb/bus<N>/port-<...>/dev/if<K>
        # sysfs interface dir: /sys/bus/usb/devices/<bus>-<chain>:<config>.<iface>
        parts = urn.split("/")
        if len(parts) < 6:
            continue
        try:
            bus = int(parts[2][3:])
            chain_parts = [int(p[5:]) for p in parts if p.startswith("port-")]
            iface = int(parts[-1][2:])
        except (ValueError, IndexError):
            continue
        if not chain_parts or chain_parts == [0]:
            continue  # root hub, skip
        chain = ".".join(str(p) for p in chain_parts)
        # Try config 1, iface K.
        iface_dir = Path(f"/sys/bus/usb/devices/{bus}-{chain}:1.{iface}")
        if not iface_dir.exists():
            continue
        driver_link = iface_dir / "driver"
        if not driver_link.exists():
            return urn  # driver-free → claimable without detach
    return None


def test_claim_then_release_against_driver_free_usb_device(
    broker: Client,
) -> None:
    """The R1 happy path: a real USB interface with no driver
    bound is claimable; release tears down cleanly."""
    if os.geteuid() != 0:
        pytest.skip("USB claim requires root (USBDEVFS open)")
    urn = _find_driver_free_usb_interface(broker)
    if urn is None:
        pytest.skip(
            "no driver-free USB interface on this host; "
            "claim against a driver-bound interface needs "
            "detach=True which would steal a kernel device — "
            "test author refuses to do that automatically"
        )
    # Use a context-managed lease so release runs in the
    # `__exit__` even on assertion failure.
    with broker.claim(urn, ClaimMode.EXCLUSIVE) as lease:
        assert lease.lease_id > 0, f"claim returned bogus lease: {lease}"
        # The raw channel: USBDEVFS fd via SCM_RIGHTS.
        ch = lease.raw()
        assert "fd" in ch, f"lease.raw() should carry fd; got {ch!r}"
        assert isinstance(ch["fd"], int), (
            f"fd should be int (SCM_RIGHTS-passed); got {type(ch['fd'])}"
        )
        assert ch["fd"] >= 0, f"invalid fd: {ch['fd']}"
        # Close the fd ourselves — the SDK transfers ownership.
        os.close(ch["fd"])
    # The `with` block's exit released the lease; subsequent
    # raw() against the now-stale lease would fail. Don't pin
    # that here (lease lifecycle has its own pins); this test's
    # job is "happy path round-trips end-to-end."


def test_claim_against_driver_bound_usb_is_conflict_without_detach(
    broker: Client,
) -> None:
    """A claim against a driver-bound USB interface MUST fail
    with ConflictError (the broker maps USBDEVFS_CLAIMINTERFACE's
    EBUSY → Err.Conflict). The opt-in `detach=True` path would
    succeed by atomically detaching the driver first; this test
    pins the default-safe behavior — no surprise driver theft."""
    if os.geteuid() != 0:
        pytest.skip("USB claim requires root")
    # Find an interface that DOES have a driver bound.
    from pathlib import Path

    bound_urn = None
    for urn in broker.list("hw:/usb/**"):
        parts = urn.split("/")
        if len(parts) < 6:
            continue
        try:
            bus = int(parts[2][3:])
            chain_parts = [int(p[5:]) for p in parts if p.startswith("port-")]
            iface = int(parts[-1][2:])
        except (ValueError, IndexError):
            continue
        if not chain_parts or chain_parts == [0]:
            continue
        chain = ".".join(str(p) for p in chain_parts)
        iface_dir = Path(f"/sys/bus/usb/devices/{bus}-{chain}:1.{iface}")
        if (iface_dir / "driver").exists():
            bound_urn = urn
            break
    if bound_urn is None:
        pytest.skip("no driver-bound USB interface on this host")
    with pytest.raises((ConflictError, NotPermittedError)):
        broker.claim(bound_urn, ClaimMode.EXCLUSIVE)
