"""MODEL-01 conformance pin for USB-02 Slice A.

The broker's USB worker (`hrb-code/src/linux_usb.zig`) and the
SDK's `mgf.hrb.sysfs.snapshot_from_sysfs` (FEEDBACK SDK-07) both
read the same sysfs files on the same host. If they disagree
about what a USB device looks like, the federation's MODEL-01
identity-continuity story breaks — a consumer comparing a
broker-side `Resource` against an SDK-side `Snapshot` would
get spurious `DIFFERENT` verdicts when the two sides should
score `SAME`.

This test enforces the contract: for every real USB device on
the test host, the broker's `inspect` and the SDK's
`snapshot_from_sysfs` produce field-equivalent records that
the comparator scores `SAME` (or `LIKELY_SAME` for devices
that don't expose a serial).

USB-02 Slice A acceptance criterion #3.

Marked ``conformance`` (release-gated) because it depends on
real sysfs + a live broker; skipped honestly when no USB
devices are present (CI build hosts).
"""

from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING

import pytest

from mgf.hrb.domain import IdentityClass
from mgf.hrb.identity import Confidence, Snapshot, same_likely
from mgf.hrb.sysfs import snapshot_from_sysfs

if TYPE_CHECKING:
    from mgf.hrb import Client

pytestmark = pytest.mark.conformance


def _urn_to_sysfs_path(urn: str) -> Path | None:
    """Decode a USB URN into its sysfs device directory.

    URN shape: ``hw:/usb/bus<N>/port-<a>[/port-<b>...]/dev/if<I>``.

    sysfs shape:
      - root hub: ``/sys/bus/usb/devices/usb<N>``
      - non-root device: ``/sys/bus/usb/devices/<N>-<a.b.c>``

    Returns None if the URN is malformed (not the expected
    shape) — the conformance test skips those rather than
    failing the whole run on a single odd URN.
    """
    # split("/") on "hw:/usb/bus3/port-9/dev/if1" produces
    # ['hw:', 'usb', 'bus3', 'port-9', 'dev', 'if1'] — single
    # slash in `hw:/` leaves no empty slot.
    parts = urn.split("/")
    if len(parts) < 4 or parts[0] != "hw:" or parts[1] != "usb":
        return None
    try:
        bus_segment = parts[2]
        assert bus_segment.startswith("bus")
        bus = int(bus_segment[3:])
    except (AssertionError, ValueError):
        return None

    # Collect the port-N segments between bus and `dev`.
    port_segments: list[int] = []
    for seg in parts[3:]:
        if seg == "dev":
            break
        if seg.startswith("port-"):
            try:
                port_segments.append(int(seg[5:]))
            except ValueError:
                return None
    if not port_segments:
        return None

    if port_segments == [0]:
        # Root hub.
        return Path(f"/sys/bus/usb/devices/usb{bus}")
    chain = ".".join(str(p) for p in port_segments)
    return Path(f"/sys/bus/usb/devices/{bus}-{chain}")


def _broker_inspect_to_snapshot(
    urn: str, attrs: dict[str, str], captured_at_ms: int = 1_000_000
) -> Snapshot:
    """Lift the broker's wire-level attrs dict into a Snapshot.

    Mirrors what `snapshot_from_sysfs` does for the SDK side —
    same field interpretations so the two sides round-trip.
    """
    vid = int(attrs.get("idVendor", "0"), 16)
    pid = int(attrs.get("idProduct", "0"), 16)
    bus = int(attrs.get("busnum", "0"))
    serial = attrs.get("serial") or None
    if serial == "":
        serial = None

    # The broker doesn't surface port_chain explicitly in attrs;
    # derive it from the URN so the comparator can use it for the
    # locality signal. split("/") on "hw:/usb/..." gives
    # ['hw:', 'usb', 'busN', 'port-X', 'dev', 'ifK'].
    port_chain: tuple[int, ...] = ()
    parts = urn.split("/")
    for seg in parts:
        if seg.startswith("port-"):
            try:
                port_chain += (int(seg[5:]),)
            except ValueError:
                pass

    identity_class = IdentityClass.INTRINSIC if serial else IdentityClass.TOPOLOGICAL
    return Snapshot(
        urn=urn,
        identity_class=identity_class,
        vid=vid,
        pid=pid,
        serial_number=serial,
        bus=bus,
        port_chain=port_chain or (0,),
        captured_at_ms=captured_at_ms,
    )


def test_broker_inspect_matches_sysfs_snapshot(broker: Client) -> None:
    """For every real USB URN the broker enumerates, building a
    Snapshot from the broker's `inspect` attrs vs reading the
    same device via `snapshot_from_sysfs` produces records the
    comparator scores `SAME` (or `LIKELY_SAME` for serial-less
    devices). MODEL-01 round-trip pin."""
    usb_urns = broker.list("hw:/usb/**")
    if not usb_urns:
        pytest.skip("no USB devices on this host")

    checked = 0
    skipped: list[str] = []
    for urn in usb_urns:
        syspath = _urn_to_sysfs_path(urn)
        if syspath is None:
            skipped.append(f"{urn}: malformed URN")
            continue
        if not syspath.exists():
            # Device may have been unplugged between list + inspect;
            # tolerable in a hot-plug environment.
            skipped.append(f"{urn}: sysfs path absent ({syspath})")
            continue

        detail = broker.inspect(urn)
        attrs = dict(detail.attrs)

        # Required fields the broker MUST surface for a usb_interface
        # — if any are missing, that's a Slice A regression worth
        # naming explicitly.
        for required in ("idVendor", "idProduct", "busnum"):
            assert required in attrs, (
                f"broker inspect of {urn} missing required attr {required!r}; got {attrs!r}"
            )

        broker_snap = _broker_inspect_to_snapshot(urn, attrs)
        sysfs_snap = snapshot_from_sysfs(syspath, urn=urn, captured_at_ms=1_001_000)

        # Field-by-field equivalence first — surfaces the SPECIFIC
        # disagreement when the comparator's verdict is wrong.
        assert broker_snap.vid == sysfs_snap.vid, (
            f"{urn}: vid disagreement broker=0x{broker_snap.vid:04x} "
            f"vs sysfs=0x{sysfs_snap.vid:04x}"
        )
        assert broker_snap.pid == sysfs_snap.pid, (
            f"{urn}: pid disagreement broker=0x{broker_snap.pid:04x} "
            f"vs sysfs=0x{sysfs_snap.pid:04x}"
        )
        assert broker_snap.bus == sysfs_snap.bus, (
            f"{urn}: bus disagreement broker={broker_snap.bus} vs sysfs={sysfs_snap.bus}"
        )
        assert broker_snap.serial_number == sysfs_snap.serial_number, (
            f"{urn}: serial disagreement broker={broker_snap.serial_number!r} "
            f"vs sysfs={sysfs_snap.serial_number!r}"
        )

        # Comparator round-trip — the high-level invariant.
        verdict = same_likely(broker_snap, sysfs_snap)
        assert verdict >= Confidence.LIKELY_SAME, (
            f"{urn}: broker + sysfs snapshots scored {verdict.name}; "
            f"MODEL-01 conformance breaks here. "
            f"broker={broker_snap} sysfs={sysfs_snap}"
        )
        # With INTRINSIC identity (serial present), score MUST be SAME.
        if broker_snap.identity_class == IdentityClass.INTRINSIC:
            assert verdict == Confidence.SAME, (
                f"{urn}: INTRINSIC pair scored {verdict.name} not SAME; "
                f"comparator drift between broker + SDK sides"
            )
        checked += 1

    assert checked > 0, f"no USB URNs cross-checkable on this host. skipped: {skipped}"


def test_check_usb_sysfs_doctor_row_present(broker: Client) -> None:
    """USB-02 Slice A acceptance #4: doctor surfaces the
    `check_usb_sysfs` row when the broker runs."""
    findings = broker.doctor()
    codes = {f.code for f in findings}
    assert "USB_SYSFS_READABLE" in codes, (
        f"USB_SYSFS_READABLE finding missing from doctor output; got codes: {sorted(codes)}"
    )


def test_broker_enumerates_usb_devices_when_present(broker: Client) -> None:
    """USB-02 Slice A acceptance #1: `Client.list('hw:/usb/**')`
    returns ≥1 URN when at least one USB device is on the host.

    Skips honestly on hosts with no USB devices at all (rare for
    a real workstation; common for stripped CI images)."""
    import shutil
    import subprocess

    usb_urns = broker.list("hw:/usb/**")
    lsusb_bin = shutil.which("lsusb")
    if lsusb_bin is None:
        if not usb_urns:
            pytest.skip("no USB devices and no lsusb to confirm host state")
        return

    lsusb_out = subprocess.run(  # noqa: S603 — controlled binary
        [lsusb_bin],
        capture_output=True,
        text=True,
        timeout=5,
    )
    lsusb_device_count = len(
        [line for line in lsusb_out.stdout.splitlines() if line.strip()],
    )

    if lsusb_device_count == 0:
        # Host has no USB at all (very unusual). Both sides agree.
        assert not usb_urns
        pytest.skip("host has no USB devices per lsusb")

    # The broker emits one URN per interface; lsusb counts devices.
    # A composite device shows multiple interfaces, so the broker
    # count is always ≥ lsusb count.
    assert len(usb_urns) >= lsusb_device_count, (
        f"broker emitted {len(usb_urns)} URNs but lsusb sees "
        f"{lsusb_device_count} devices; per-interface enumeration "
        f"shouldn't UNDER-count vs per-device tally."
    )
