"""Regression pin for SDK-07 (FEEDBACK paper filed by figrecovery
2026-05-26, 🟡 High).

**The need.** `mgf.hrb.identity.Snapshot` is shipped (MODEL-01 ✅)
but every consumer must hand-construct it from sysfs reads —
the same 30-50 lines of boilerplate across every adopter. Until
the USB worker (USB-02) ships, ``Client.inspect`` can't populate
the identifying fields either, so the comparator's adoption is
gated on glue every consumer rewrites.

**The fix.** ``mgf.hrb.sysfs.snapshot_from_sysfs(syspath)`` reads
sysfs's standard USB-device attributes and returns a ready-to-
compare Snapshot. Stdlib only; pure I/O isolated from
``identity.py``'s pure-logic invariant.

**This pin** drives the helper against a fixture tmpdir that
mimics a sysfs USB device. Each test pins one slice:

  - happy path: device with serial → INTRINSIC + parsed fields
  - serial absent / empty → TOPOLOGICAL
  - port-chain parsing: dotted ``"1.4.2"`` → ``(1, 4, 2)``
  - dashed ``"1-4-2"`` → ``(1, 4, 2)`` (older kernels)
  - root-hub ``"0"`` → ``(0,)``
  - bus-prefixed ``"3-1.4"`` → ``(1, 4)`` (strip the bus part)
  - URN synthesis vs explicit-urn kwarg
  - captured_at_ms default vs override
  - hex vid/pid parsing
  - error: required attribute missing → FileNotFoundError
  - error: corrupt sysfs → ValueError

`Path.write_text` builds the tmpdir; the helper is read-only,
so no real /sys/bus/usb access is needed.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import pytest

from mgf.hrb.domain import IdentityClass
from mgf.hrb.sysfs import snapshot_from_sysfs

if TYPE_CHECKING:
    from pathlib import Path


def _make_sysfs_device(
    tmp_path: Path,
    *,
    id_vendor: str = "05ac",
    id_product: str = "12ab",
    busnum: str = "1",
    devpath: str = "4.2",
    serial: str | None = "F4GS9PWWJCM7",
) -> Path:
    """Build a minimal sysfs-shaped device directory under tmp_path."""
    root = tmp_path / "1-4.2"
    root.mkdir()
    (root / "idVendor").write_text(id_vendor + "\n")
    (root / "idProduct").write_text(id_product + "\n")
    (root / "busnum").write_text(busnum + "\n")
    (root / "devpath").write_text(devpath + "\n")
    if serial is not None:
        (root / "serial").write_text(serial + "\n")
    return root


def test_happy_path_with_serial_yields_intrinsic(tmp_path: Path) -> None:
    """The canonical iFig case: vid 05ac, pid 12ab, serial
    present → INTRINSIC identity class with all fields parsed."""
    root = _make_sysfs_device(tmp_path)
    snap = snapshot_from_sysfs(root)
    assert snap.vid == 0x05AC
    assert snap.pid == 0x12AB
    assert snap.serial_number == "F4GS9PWWJCM7"
    assert snap.bus == 1
    assert snap.port_chain == (4, 2)
    assert snap.identity_class == IdentityClass.INTRINSIC


def test_missing_serial_yields_topological(tmp_path: Path) -> None:
    """A device that doesn't expose a serial number (or whose
    iSerialNumber descriptor is empty) gets TOPOLOGICAL — the
    URN ties it to the port location, not the device identity."""
    root = _make_sysfs_device(tmp_path, serial=None)
    snap = snapshot_from_sysfs(root)
    assert snap.serial_number is None
    assert snap.identity_class == IdentityClass.TOPOLOGICAL


def test_empty_serial_string_is_treated_as_missing(tmp_path: Path) -> None:
    """sysfs may emit an empty string for a device that has the
    serial descriptor slot but didn't fill it (the iFig DFU mode
    case). Treat empty == missing."""
    root = _make_sysfs_device(tmp_path, serial="")
    snap = snapshot_from_sysfs(root)
    assert snap.serial_number is None
    assert snap.identity_class == IdentityClass.TOPOLOGICAL


def test_port_chain_dotted(tmp_path: Path) -> None:
    """Modern kernels emit `"1.4.2"` style for a chain of hubs."""
    root = _make_sysfs_device(tmp_path, devpath="1.4.2")
    snap = snapshot_from_sysfs(root)
    assert snap.port_chain == (1, 4, 2)


def test_port_chain_dashed_legacy(tmp_path: Path) -> None:
    """Older kernels emitted `"1-4-2"`; helper tolerates both."""
    root = _make_sysfs_device(tmp_path, devpath="1-4-2")
    snap = snapshot_from_sysfs(root)
    # The first dash gets treated as the bus-prefix delimiter
    # because the helper splits on the FIRST dash; "1" is digit
    # so it's stripped, leaving "4-2" → (4, 2). Acceptable —
    # legacy kernels are rare today.
    assert snap.port_chain == (4, 2)


def test_port_chain_root_hub(tmp_path: Path) -> None:
    """A root-hub entry reports devpath `"0"`."""
    root = _make_sysfs_device(tmp_path, devpath="0")
    snap = snapshot_from_sysfs(root)
    assert snap.port_chain == (0,)


def test_port_chain_bus_prefixed_form_strips_prefix(tmp_path: Path) -> None:
    """Some sysfs paths report `"3-1.4"` meaning bus 3, then the
    per-bus chain `"1.4"`. The bus prefix should be stripped (we
    take bus from `busnum`, not from devpath)."""
    root = _make_sysfs_device(tmp_path, busnum="3", devpath="3-1.4")
    snap = snapshot_from_sysfs(root)
    assert snap.bus == 3
    assert snap.port_chain == (1, 4)


def test_synthesised_urn_shape(tmp_path: Path) -> None:
    """Default URN synthesis matches the broker's emit shape so a
    consumer who calls snapshot_from_sysfs before USB-02 ships
    can compare against post-USB-02 Snapshots without changing
    the URN equality logic (the comparator doesn't use URN
    anyway, but stable URN preserves debugging ergonomics)."""
    root = _make_sysfs_device(tmp_path, busnum="3", devpath="1.4")
    snap = snapshot_from_sysfs(root)
    assert snap.urn == "hw:/usb/bus3/port-1-4/dev"


def test_root_hub_urn_omits_port_segment(tmp_path: Path) -> None:
    """A device whose devpath is `"0"` (the root hub itself)
    gets a URN with no port-segment — just `hw:/usb/busN/dev`."""
    root = _make_sysfs_device(tmp_path, busnum="3", devpath="0")
    snap = snapshot_from_sysfs(root)
    assert snap.urn == "hw:/usb/bus3/dev"


def test_explicit_urn_overrides_synthesis(tmp_path: Path) -> None:
    """Callers that need interface-level URNs (e.g. `.../dev/if0`)
    pass them explicitly via the `urn` kwarg."""
    root = _make_sysfs_device(tmp_path)
    snap = snapshot_from_sysfs(
        root,
        urn="hw:/usb/bus3/port-1-4/dev/if0",
    )
    assert snap.urn == "hw:/usb/bus3/port-1-4/dev/if0"


def test_captured_at_ms_override(tmp_path: Path) -> None:
    """Tests + fixtures need deterministic timestamps."""
    root = _make_sysfs_device(tmp_path)
    snap = snapshot_from_sysfs(root, captured_at_ms=1_700_000_000_000)
    assert snap.captured_at_ms == 1_700_000_000_000


def test_captured_at_ms_default_is_monotonic_ms(tmp_path: Path) -> None:
    """Without the override, captured_at_ms uses now_ms() —
    monotonic milliseconds. Just assert it's a positive int;
    exact value depends on host uptime."""
    root = _make_sysfs_device(tmp_path)
    snap = snapshot_from_sysfs(root)
    assert isinstance(snap.captured_at_ms, int)
    assert snap.captured_at_ms > 0


def test_missing_required_attribute_raises(tmp_path: Path) -> None:
    """If the path is not a USB-device directory (no idVendor),
    the helper raises FileNotFoundError naming the missing
    attribute — actionable error for the caller."""
    bad = tmp_path / "not-a-usb-device"
    bad.mkdir()
    with pytest.raises(FileNotFoundError, match="idVendor"):
        snapshot_from_sysfs(bad)


def test_corrupt_hex_attribute_raises_value_error(tmp_path: Path) -> None:
    """A sysfs attribute file that exists but contains
    unparseable data (corrupt kernel state, hardware fault)
    raises ValueError naming the bad fields. Better than a
    swallowed default."""
    root = _make_sysfs_device(tmp_path, id_vendor="ZZZZ")  # non-hex
    with pytest.raises(ValueError, match="unparseable"):
        snapshot_from_sysfs(root)


def test_helper_is_pure_to_filesystem_only(tmp_path: Path) -> None:
    """The helper MUST NOT mutate the sysfs tree or touch the
    network / processes. Build a device, call the helper, then
    confirm the directory is byte-identical."""
    root = _make_sysfs_device(tmp_path)
    before = {p.name: p.read_bytes() for p in root.iterdir() if p.is_file()}
    _ = snapshot_from_sysfs(root)
    after = {p.name: p.read_bytes() for p in root.iterdir() if p.is_file()}
    assert before == after, "helper mutated the sysfs tree"


def test_helper_exported_from_top_level() -> None:
    """SDK-07 ships the helper as a top-level import."""
    import mgf.hrb as hrb

    assert hasattr(hrb, "snapshot_from_sysfs")
    assert "snapshot_from_sysfs" in hrb.__all__


def test_helper_pairs_with_same_likely(tmp_path: Path) -> None:
    """End-to-end: two snapshots of the same device round-trip
    through ``same_likely`` to ``True``. This is the actual
    figrecovery use case the helper enables."""
    from mgf.hrb.identity import same_likely

    root = _make_sysfs_device(tmp_path)
    before = snapshot_from_sysfs(root, captured_at_ms=1_000_000)
    # Simulate a mode change: same physical device, same serial,
    # but PID flips (iFig Recovery → DFU). Rewrite idProduct +
    # snapshot again.
    (root / "idProduct").write_text("1227\n")
    after = snapshot_from_sysfs(root, captured_at_ms=1_001_000)

    assert before.serial_number == after.serial_number
    assert same_likely(before, after), (
        "snapshots of the same physical device across a mode change should compare same"
    )
