"""Smoke tests for the doctor subcommand + the shared diagnostics
engine (FEEDBACK TOOLS-02).

The doctor's checks are mostly filesystem-shaped (presence of
``/sys/bus/pci``, ``/dev/vfio``, IOMMU group dir) so they can be
exercised without a live broker. The "broker socket missing" path
is exercised by pointing at a path that doesn't exist — the same
state an operator sees on a fresh host before starting the broker.

The check functions live in :mod:`mgf.hrb.diagnostics` (the shared
engine consumed by both the CLI here and ``Client.doctor()``); the
CLI module exports ``doctor_command`` only.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import pytest

from mgf.hrb.cli._doctor import doctor_command
from mgf.hrb.diagnostics import (
    Category,
    Severity,
    _check_path_readable,
    check_iommu_groups,
    check_pci_sysfs,
    check_socket,
    check_udev_rule,
    check_usb_sysfs,
    check_usbfs_devnode,
    check_usbfs_openable,
    check_vfio_device,
)

if TYPE_CHECKING:
    from pathlib import Path


def test_socket_missing_returns_fail(tmp_path: Path) -> None:
    """No socket file at the path → FAIL with a copy-pastable fix."""
    result = check_socket(str(tmp_path / "nonexistent.sock"))
    assert result.severity is Severity.FAIL
    assert result.code == "SOCKET_REACHABLE"
    assert "mgf-hrb-server" in (result.remediation or "")


def test_pci_sysfs_check_returns_status() -> None:
    """The check returns one of the four severities (the actual answer
    depends on the host; both OK and WARN are valid for a CI environment)."""
    result = check_pci_sysfs()
    assert result.severity in {Severity.OK, Severity.WARN}
    assert result.code == "PCI_SYSFS_READABLE"
    assert "PCI sysfs" in result.summary


def test_usb_sysfs_check_returns_status() -> None:
    result = check_usb_sysfs()
    assert result.severity in {Severity.OK, Severity.WARN}
    assert result.code == "USB_SYSFS_READABLE"


# === R3 — TOOLS-01 USB substrate rows ====================================
#
# Two new checks distinguish "sysfs walk works" (enumeration surface,
# tested above) from "USBDEVFS opens for claim" (claim surface, tested
# below). Pre-R3 the doctor surfaced an EACCES on USBDEVFS only after
# an operator hit it via `mgf-hrb claim`; with R3 the doctor surfaces
# it at bring-up time and points at the udev-rule remediation.


def test_usbfs_devnode_check_returns_status() -> None:
    """Whether /dev/bus/usb is present on the host is environment-
    dependent (CI sandboxes may not mount devtmpfs); both OK + WARN
    are valid outcomes. The code MUST be stable."""
    result = check_usbfs_devnode()
    assert result.severity in {Severity.OK, Severity.WARN}
    assert result.code == "USBFS_DEVNODE_PRESENT"
    assert result.category is Category.SUBSTRATE
    assert "USB device nodes" in result.summary


def test_usbfs_openable_check_returns_valid_severity() -> None:
    """The open() probe can return OK (root or udev-rule installed),
    WARN (EACCES — the udev-rule case), or DEFERRED (no devices to
    test against). Pin: never raise; always one of the four."""
    result = check_usbfs_openable()
    assert result.severity in {
        Severity.OK,
        Severity.WARN,
        Severity.DEFERRED,
    }
    assert result.code == "USBFS_DEVNODE_OPENABLE"
    assert result.category is Category.SUBSTRATE


def test_usbfs_openable_eacces_points_at_udev_rule() -> None:
    """When the open probe returns EACCES, the remediation MUST
    name the udev rule (the operator's actual fix). Skip if we
    happen to land in a state where the probe doesn't return WARN
    (e.g. running as root, no devices)."""
    result = check_usbfs_openable()
    if result.severity is not Severity.WARN:
        pytest.skip(f"open probe returned {result.severity}, not WARN")
    # Only the EACCES branch carries a remediation; other WARN
    # branches (enumerate failure, non-EACCES OSError) don't.
    if "EACCES" in (result.detail or ""):
        assert "udev" in (result.remediation or "").lower(), (
            f"EACCES finding should point at udev rule; got {result!r}"
        )


def test_vfio_device_check_returns_warn_or_ok() -> None:
    """When /dev/vfio is absent (common in CI), the check is WARN
    (honest, G2) — acquire returns Unsupported, not a fake success."""
    result = check_vfio_device()
    assert result.severity in {Severity.OK, Severity.WARN}
    assert result.code == "VFIO_DEVICE_PRESENT"


def test_iommu_groups_check_returns_warn_or_ok() -> None:
    """LINUX-01: IOMMU absence is WARN, not FAIL — the broker can still
    do enumeration without it, just not DMA-capable passthrough."""
    result = check_iommu_groups()
    assert result.severity in {Severity.OK, Severity.WARN, Severity.FAIL}
    assert result.code == "IOMMU_PRESENT"


def test_udev_rule_check_returns_status() -> None:
    """R-USB-PKG (2026-05-27) — was DEFERRED; now actually checks for
    the rule file at canonical udev paths. Both OK and WARN are valid
    outcomes depending on whether the host has the rule installed."""
    result = check_udev_rule()
    assert result.code == "UDEV_RULE_INSTALLED"
    assert result.severity in {Severity.OK, Severity.WARN}
    # WARN branch MUST carry the install remediation.
    if result.severity is Severity.WARN:
        assert "install" in (result.remediation or "").lower()
        assert "70-mgf-hrb.rules" in (result.remediation or "")


def test_path_readable_helper_handles_missing(tmp_path: Path) -> None:
    """Internal helper: a missing path → WARN, not FAIL (the substrate
    might just be absent on this host shape)."""
    result = _check_path_readable(
        code="TEST_PATH",
        summary="test",
        path=str(tmp_path / "missing"),
        category=Category.SUBSTRATE,
        why="test fixture",
    )
    assert result.severity is Severity.WARN
    assert result.code == "TEST_PATH"


def test_doctor_command_runs_end_to_end(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
    """End-to-end smoke: doctor runs without raising, returns an integer
    exit code, prints a summary line. Socket missing → at least 1 fail."""
    exit_code = doctor_command(socket_path=str(tmp_path / "nonexistent.sock"))
    assert isinstance(exit_code, int)
    # Socket-missing is a FAIL, so exit code should be 1.
    assert exit_code == 1
    captured = capsys.readouterr()
    # The polished report has a Summary section and an ACTION REQUIRED block
    # for FAILed checks.
    assert "Summary" in captured.out
    assert "fail" in captured.out
    assert "ACTION REQUIRED" in captured.out


def test_doctor_renders_grouped_categories(
    tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
    """The polished report groups checks by category (Substrate / Hardware
    / Broker). Verify each header is rendered."""
    doctor_command(socket_path=str(tmp_path / "nonexistent.sock"))
    out = capsys.readouterr().out
    assert "Substrate" in out
    assert "Hardware policy" in out
    assert "Broker" in out


def test_doctor_renders_broker_health_pane_with_live_broker(
    tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
    """When the broker is reachable + the doctor is the FIRST
    connection (so cap-binding succeeds), the new broker-health
    pane renders with uptime, op count, and audit-ring fill."""
    import subprocess
    from pathlib import Path as Path_

    from tests import _broker

    repo = Path_(__file__).resolve().parents[4]
    server = repo / "hrb-code" / "zig-out" / "bin" / "mgf-hrb-server"
    if not server.exists():
        pytest.skip(f"broker binary not built at {server}")
    sock_path = "/tmp/mgf-hrb.sock"
    if Path_(sock_path).exists():
        Path_(sock_path).unlink()
    proc = subprocess.Popen(  # noqa: S603
        [str(server)],
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
    )
    try:
        # Wait until the broker ACCEPTS connections (connect-probe) — a Unix
        # socket file appears at bind() but refuses connects until listen(), so
        # an existence check (or an immediate connect after it) races under
        # contention. The shared helper both waits AND is the accept sanity.
        try:
            _broker.wait_for_socket(sock_path, 5.0)
        except TimeoutError:
            proc.kill()
            pytest.fail("broker never started accepting connections")

        # NOTE: the doctor's Client.connect is the FIRST cap-binding
        # of this test process to the broker — owns the root cap.
        capsys.readouterr()  # drain anything prior
        exit_code = doctor_command(socket_path=sock_path)
        out = capsys.readouterr().out
        # Exit 0 = all OK; exit 78 (EX_CONFIG) = at least one WARN,
        # no FAIL. Both are acceptable here — the test's intent is
        # "broker reachable + health pane renders", not "host is
        # warning-free." R3 (2026-05-27) added the USBDEVFS open
        # probe which honestly returns WARN on non-root hosts
        # without the udev rule installed; flipping the assertion
        # to allow 78 doesn't weaken the test (it still catches
        # exit-1 / FAIL regressions, and pins the pane content).
        assert exit_code in (0, 78), out
        # The pane MUST render with its key fields.
        assert "Broker health" in out
        assert "broker 0.1.0" in out
        assert "audit ring:" in out
        assert "cap table:" in out
        assert "active:" in out
    finally:
        proc.terminate()
        try:
            proc.wait(timeout=2)
        except subprocess.TimeoutExpired:
            proc.kill()
        if proc.stdout is not None:
            proc.stdout.close()
        if Path_(sock_path).exists():
            Path_(sock_path).unlink()
