"""Use case: a GUI's diagnostics ("doctor") pane.

Reproduces the hrb-app Phase-1.1 chunk D scenario end-to-end. Cites:
FEEDBACK TOOLS-02 (the paper that motivated `Client.doctor`).

The scenario in words:

  1. Operator opens hrb-app, sees the doctor pane.
  2. The pane calls `Client.doctor()` to populate.
  3. Each finding renders as a row with color-by-severity, a stable
     code, the operator-facing prose, and (when relevant) the
     copy-pastable remediation.
  4. Operator follows the remediation for a FAIL row.

The test walks step 2+3 against the real broker and asserts every
property the GUI would render against.
"""

from __future__ import annotations

import pytest

from mgf.hrb import Client, DoctorFinding, DoctorSeverity

pytestmark = [pytest.mark.use_case, pytest.mark.timeout(60)]


def test_doctor_pane_returns_stable_code_set(broker: Client) -> None:
    """Pin: the engine emits a STABLE set of codes the GUI can
    pattern-match on. Adding a code is additive (allowed); removing
    or renaming one is a breaking change — caught here.

    R3 (2026-05-27) — TOOLS-01 substrate rows. USBFS_DEVNODE_PRESENT
    and USBFS_DEVNODE_OPENABLE were added with the USB worker's
    claim-path closure (R1); the substrate rows distinguish "sysfs
    walk works" (USB_SYSFS_READABLE) from "USBDEVFS opens for claim"
    (the two new rows). Pre-R3 there were 7 codes; with R3 there are 9.
    """
    findings = broker.doctor()
    codes = {f.code for f in findings}
    # The nine codes the engine ships today (FEEDBACK TOOLS-02 + R3).
    # GUIs may safely match on these literal strings.
    expected = {
        "SOCKET_REACHABLE",
        "PCI_SYSFS_READABLE",
        "USB_SYSFS_READABLE",
        "USBFS_DEVNODE_PRESENT",
        "USBFS_DEVNODE_OPENABLE",
        "VFIO_DEVICE_PRESENT",
        "IOMMU_PRESENT",
        "UDEV_RULE_INSTALLED",
        "BACKEND_CAPABILITIES",
    }
    assert codes == expected, f"diagnostics code set drifted; was {expected}, now {codes}"


def test_doctor_pane_rows_have_complete_prose(broker: Client) -> None:
    """Every finding the GUI renders MUST have at minimum: a
    non-empty summary (the row title), a non-empty detail (the
    body), and a severity for the color. Pin so a future engine
    change can't ship a half-empty finding."""
    findings = broker.doctor()
    for f in findings:
        assert isinstance(f, DoctorFinding)
        assert f.code, f"empty code on {f}"
        assert f.summary, f"empty summary on {f.code}"
        assert f.detail, f"empty detail on {f.code}"
        # severity ALWAYS one of the four — guards a future engine
        # against silently introducing a 5th value the GUI doesn't
        # know how to color.
        assert f.severity in (
            DoctorSeverity.OK,
            DoctorSeverity.WARN,
            DoctorSeverity.FAIL,
            DoctorSeverity.DEFERRED,
        )


def test_doctor_pane_socket_check_is_ok_when_broker_up(broker: Client) -> None:
    """We're already connected via the fixture — the SOCKET_REACHABLE
    check must reflect that. Pin: the engine doesn't lie (G2)."""
    findings = broker.doctor()
    sock = next(f for f in findings if f.code == "SOCKET_REACHABLE")
    assert sock.severity is DoctorSeverity.OK, (
        f"socket reachable check wrong when we're connected: {sock}"
    )


def test_doctor_pane_backend_capabilities_runs_against_live_broker(
    broker: Client,
) -> None:
    """When the broker is reachable, BACKEND_CAPABILITIES runs the
    real `capabilities()` wire op rather than returning DEFERRED.
    Pin so a future refactor can't silently skip the live check
    in the "broker reachable" path."""
    findings = broker.doctor()
    cap_check = next(f for f in findings if f.code == "BACKEND_CAPABILITIES")
    # When the broker is up, the check ran — it's NOT deferred.
    assert cap_check.severity is not DoctorSeverity.DEFERRED, (
        "backend capabilities deferred even though broker is reachable"
    )


def test_doctor_findings_carry_remediation_when_actionable(
    broker: Client,
) -> None:
    """Every FAIL-severity finding MUST carry a non-empty
    `remediation` string — that's the copy-pastable line the
    operator follows. Pin: a FAIL without remediation is a UX
    failure (the operator knows something's wrong but not what
    to do)."""
    findings = broker.doctor()
    failing_without_remediation = [
        f for f in findings if f.severity is DoctorSeverity.FAIL and not f.remediation
    ]
    assert not failing_without_remediation, (
        f"FAIL findings missing remediation: {[f.code for f in failing_without_remediation]}"
    )


def test_doctor_findings_have_stable_order(broker: Client) -> None:
    """The GUI renders rows top-to-bottom; the engine emits in a
    stable order that matches the substrate/hardware/broker
    grouping. Pin: calling doctor() twice in sequence gives the
    same code order — no hash-set surprises."""
    a = [f.code for f in broker.doctor()]
    b = [f.code for f in broker.doctor()]
    assert a == b, f"doctor() order not stable: {a} vs {b}"
