"""Unit tests for ``mgf.hrb.identity`` — pins the Python comparator
against the same iFig scores ``hrb-code/src/identity_test.zig`` pins for
the Zig side. Drift between the two sides is exactly what D-035 says
must be impossible; this test catches it.

The exhaustive contract (boundary cases, fatal short-circuits, debounce
windows, all four ComparatorOpts toggles, symmetry) lives in the Zig
suite; here we keep a focused subset that pins the *spec numbers* (11 /
13 / 15) and the obvious mismatch cases — enough to catch drift, not so
much that maintaining two suites doubles the cost.
"""

from __future__ import annotations

from mgf.hrb import (
    ComparatorOpts,
    Confidence,
    IdentityClass,
    Snapshot,
    same_likely,
    same_likely_explained,
)

IFIG_VID = 0x05AC
RECOVERY_PID = 0x1281
DFU_PID = 0x1227
NORMAL_PID = 0x12AB


def _recovery(t_ms: int = 1000) -> Snapshot:
    return Snapshot(
        urn="hw:/usb/bus1/port-2.3/dev/if0",
        identity_class=IdentityClass.INTRINSIC,
        vid=IFIG_VID,
        pid=RECOVERY_PID,
        serial_number="F4GS9PWWJCM7",
        bus=1,
        port_chain=(2, 3),
        captured_at_ms=t_ms,
    )


def _dfu(t_ms: int = 1500) -> Snapshot:
    return Snapshot(
        urn="hw:/usb/bus1/port-2.3/dev/if0",
        identity_class=IdentityClass.TOPOLOGICAL,
        vid=IFIG_VID,
        pid=DFU_PID,
        serial_number=None,
        bus=1,
        port_chain=(2, 3),
        captured_at_ms=t_ms,
    )


def _normal(t_ms: int = 1000) -> Snapshot:
    return Snapshot(
        urn="hw:/usb/bus1/port-2.3/dev/if0",
        identity_class=IdentityClass.INTRINSIC,
        vid=IFIG_VID,
        pid=NORMAL_PID,
        serial_number="F4GS9PWWJCM7",
        bus=1,
        port_chain=(2, 3),
        captured_at_ms=t_ms,
    )


# === Canonical iFig scores from hrb-design/mgf-hrb-usb-worker.md §4.3 ======


def test_recovery_to_dfu_scores_11_likely_same() -> None:
    r = same_likely_explained(_recovery(), _dfu())
    assert r.score == 11
    assert r.confidence is Confidence.LIKELY_SAME


def test_normal_to_recovery_serial_intact_scores_15_same() -> None:
    r = same_likely_explained(_normal(), _recovery(t_ms=1500))
    assert r.score == 15
    assert r.confidence is Confidence.SAME


def test_identical_snapshots_score_13_same() -> None:
    r = same_likely_explained(_recovery(1000), _recovery(1100))
    assert r.score == 13
    assert r.confidence is Confidence.SAME


# === Fatal short-circuits ==================================================


def test_vid_mismatch_short_circuits_different() -> None:
    other = Snapshot(
        urn="hw:/usb/bus1/port-2.3/dev/if0",
        identity_class=IdentityClass.TOPOLOGICAL,
        vid=0x1234,
        pid=RECOVERY_PID,
        serial_number="F4GS9PWWJCM7",  # irrelevant once vid disagrees
        bus=1,
        port_chain=(2, 3),
        captured_at_ms=1100,
    )
    assert same_likely(_recovery(), other) is Confidence.DIFFERENT


def test_same_vid_two_serials_short_circuits_different() -> None:
    other = Snapshot(
        urn="hw:/usb/bus1/port-2.3/dev/if0",
        identity_class=IdentityClass.TOPOLOGICAL,
        vid=IFIG_VID,
        pid=RECOVERY_PID,
        serial_number="DIFFERENT_SERIAL",
        bus=1,
        port_chain=(2, 3),
        captured_at_ms=1100,
    )
    assert same_likely(_recovery(), other) is Confidence.DIFFERENT


# === ComparatorOpts toggles ================================================


def test_strict_serial_demotes_dfu_below_likely_same() -> None:
    r = same_likely_explained(_recovery(), _dfu(), ComparatorOpts(tolerate_missing_serial=False))
    # vid +3, serial_asymmetric_strict -4, port_match +4, mode_change +2 -> 5
    assert r.score == 5
    assert r.confidence is Confidence.AMBIGUOUS


def test_debounce_window_excludes_late_mode_change() -> None:
    r = same_likely_explained(_recovery(1000), _dfu(t_ms=6000))
    # +3 vid, +2 serial-asymmetric, +4 port → 9; no mode_change_explained
    assert r.score == 9
    assert r.confidence is Confidence.LIKELY_SAME


# === Symmetry ==============================================================


def test_symmetric_over_inputs() -> None:
    a, b = _recovery(), _dfu()
    assert same_likely(a, b) is same_likely(b, a)
