"""Smoke tests for the CLI parser and the HrbSettings → CLI wiring.

Exercises the parser shape and the no-broker fallback paths. The
parser tests confirm the CF-08 injection point holds — tests construct
``HrbSettings`` with explicit values and assert the parser uses them,
so the test never depends on the host's env vars or ``$MGF_HRB_*``.
The no-broker tests exercise each subcommand against a non-existent
socket — the state operators see on a fresh host before starting the
broker — to cover the FileNotFoundError → exit-2 fallback.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import pytest

from mgf.hrb import HrbSettings
from mgf.hrb.cli import build_parser, main
from mgf.hrb.cli._info import info_command
from mgf.hrb.cli._scan import scan_command

if TYPE_CHECKING:
    from pathlib import Path


def test_socket_default_comes_from_injected_settings() -> None:
    """CF-08 / CF-10 — tests inject settings rather than mutating env."""
    s = HrbSettings(socket_path="/tmp/explicit-for-test.sock")
    parser = build_parser(s)
    args = parser.parse_args(["scan"])
    assert args.socket == "/tmp/explicit-for-test.sock"
    assert args.scope == "hw:/**"


def test_cli_flag_overrides_setting_default() -> None:
    """CF-02 precedence: CLI > env > defaults; the --socket flag wins."""
    s = HrbSettings(socket_path="/tmp/should-be-overridden.sock")
    parser = build_parser(s)
    args = parser.parse_args(["--socket", "/tmp/from-cli.sock", "scan"])
    assert args.socket == "/tmp/from-cli.sock"


def test_subcommands_registered() -> None:
    s = HrbSettings()
    parser = build_parser(s)
    # Each subcommand parses without raising.
    assert parser.parse_args(["scan"]).command == "scan"
    assert parser.parse_args(["scan", "hw:/pci/**"]).command == "scan"
    assert parser.parse_args(["info", "hw:/pci/x"]).command == "info"
    assert parser.parse_args(["doctor"]).command == "doctor"


def test_missing_subcommand_errors() -> None:
    s = HrbSettings()
    parser = build_parser(s)
    with pytest.raises(SystemExit):
        parser.parse_args([])


def test_scan_no_broker_returns_exit_2(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
    """No broker at the socket → scan exits 2 with a copy-pastable hint.
    This is the path an operator sees on a fresh host before the broker
    starts."""
    socket_path = str(tmp_path / "nonexistent.sock")
    exit_code = scan_command(socket_path=socket_path, scope="hw:/**")
    assert exit_code == 2
    captured = capsys.readouterr()
    assert "no broker is listening" in captured.err
    assert socket_path in captured.err


def test_info_no_broker_returns_exit_2(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
    """Same fallback shape as scan — info raises FileNotFoundError before
    even reaching the inspect call."""
    socket_path = str(tmp_path / "nonexistent.sock")
    exit_code = info_command(socket_path=socket_path, urn="hw:/pci/x")
    assert exit_code == 2
    captured = capsys.readouterr()
    assert "no broker is listening" in captured.err


def test_scan_render_handles_empty_and_sorted_input(
    capsys: pytest.CaptureFixture[str],
) -> None:
    """The internal renderer formats whatever the broker returns. Direct
    test so we don't need a live socket to cover the formatting path."""
    from mgf.hrb.cli._scan import _render

    _render([], socket_path="/tmp/test.sock", scope="hw:/**")
    out = capsys.readouterr().out
    assert "(no resources matched the scope)" in out

    _render(
        ["hw:/pci/0000:01:00.0/fn0", "hw:/pci/0000:00:1f.2/fn0"],
        socket_path="/tmp/test.sock",
        scope="hw:/**",
    )
    out = capsys.readouterr().out
    # Output groups by family + summary footer; both URNs appear.
    assert "hw:/pci/0000:00:1f.2/fn0" in out
    assert "hw:/pci/0000:01:00.0/fn0" in out
    # Sorted within the PCI bucket — 00:1f.2 comes before 01:00.0
    assert out.index("0000:00:1f.2") < out.index("0000:01:00.0")
    # Header + footer rendered.
    assert "mgf-hrb scan" in out
    assert "2 resource(s) total" in out


def test_scan_render_groups_by_family(capsys: pytest.CaptureFixture[str]) -> None:
    """The polished renderer puts PCI and USB into separate buckets so a
    composite stays visually together."""
    from mgf.hrb.cli._scan import _render

    _render(
        [
            "hw:/usb/bus1/port-0/dev/if0",
            "hw:/pci/0000:00:00.0/fn0",
            "hw:/usb/bus3/port-7/dev/if0",
        ],
        socket_path="/tmp/test.sock",
        scope="hw:/**",
    )
    out = capsys.readouterr().out
    # PCI section comes before USB section (per _FAMILY_LABELS order).
    pci_idx = out.index("PCI functions")
    usb_idx = out.index("USB interfaces")
    assert pci_idx < usb_idx
    # Each section gets its count.
    assert "PCI functions  (1):" in out
    assert "USB interfaces  (2):" in out


def test_scan_broker_error_returns_exit_1(
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """When the broker is reachable but returns an MgfHrbError, scan exits
    1 with the error text on stderr."""
    from mgf.hrb.exceptions import NotPermittedError

    class _FakeClient:
        @classmethod
        def connect(cls, _path: str) -> _FakeClient:
            return cls()

        def list(self, _scope: str) -> list[str]:
            raise NotPermittedError("simulated cap check fail")

        def __enter__(self) -> _FakeClient:
            return self

        def __exit__(self, *_exc: object) -> None:
            return None

    monkeypatch.setattr("mgf.hrb.cli._scan.Client", _FakeClient)
    exit_code = scan_command(socket_path=str(tmp_path / "irrelevant.sock"), scope="hw:/**")
    assert exit_code == 1
    assert "simulated cap check fail" in capsys.readouterr().err


def test_main_dispatches_doctor_and_returns_exit_code(
    tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
    """End-to-end through main(): bootstrap wires logging + the dispatch,
    doctor runs against the absent socket and returns 1 (FAIL on socket
    check). Covers the EH-04 bootstrap wrap + the dispatch table.

    Suppresses the migration DeprecationWarning that B8 added to
    ``main`` — the warning IS part of the public surface (operators
    must see it on every invocation) but it's tested separately by
    ``test_main_emits_deprecation_warning`` below; here we just
    verify dispatch still works.
    """
    import warnings

    socket_path = str(tmp_path / "nonexistent.sock")
    with warnings.catch_warnings():
        warnings.simplefilter("ignore", DeprecationWarning)
        exit_code = main(["--socket", socket_path, "doctor"])
    assert exit_code == 1
    captured = capsys.readouterr()
    assert "broker socket" in captured.out


def test_main_emits_deprecation_warning(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
    """B8 — every `mgf-hrb` invocation MUST emit a DeprecationWarning
    pointing at hrb-app, AND print the migration banner to stderr so
    operators see it even if their logging config swallows the
    warning. Pinned here so the message text doesn't silently drift
    (or get removed) before 0.1.x removal."""
    import warnings

    socket_path = str(tmp_path / "nonexistent.sock")
    with warnings.catch_warnings(record=True) as caught:
        warnings.simplefilter("always", DeprecationWarning)
        # Use `explain` which doesn't need a broker (no network calls);
        # the deprecation banner fires regardless of subcommand.
        main(["--socket", socket_path, "explain"])
    dep_warnings = [w for w in caught if issubclass(w.category, DeprecationWarning)]
    assert len(dep_warnings) == 1, (
        f"expected exactly one DeprecationWarning; got {len(dep_warnings)}"
    )
    msg = str(dep_warnings[0].message)
    assert "hrb-app" in msg, f"warning must point at hrb-app; got {msg!r}"
    captured = capsys.readouterr()
    assert "DEPRECATION NOTICE" in captured.err
    assert "hrb-app cli" in captured.err


def test_info_renders_usb_descriptor(
    tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
) -> None:
    """USB-kind inspect response is decoded with the class label and
    /dev/bus/usb path constructed from busnum/devnum."""

    class _FakeClient:
        @classmethod
        def connect(cls, _path: str) -> _FakeClient:
            return cls()

        def __enter__(self) -> _FakeClient:
            return self

        def __exit__(self, *_exc: object) -> None:
            return None

        def inspect_raw(self, _urn: str) -> dict[str, object]:
            return {
                "urn": "hw:/usb/bus3/port-9/dev/if0",
                "family": "claimable",
                "kind": "usb_interface",
                "fidelity": "live",
                "attrs": {
                    "idVendor": "5986",
                    "idProduct": "2178",
                    "serial": "01.00.00",
                    "bInterfaceClass": "0e",
                    "bInterfaceSubClass": "01",
                    "bInterfaceProtocol": "01",
                    "busnum": "3",
                    "devnum": "3",
                },
            }

    monkeypatch.setattr("mgf.hrb.Client", _FakeClient, raising=False)
    exit_code = info_command(
        socket_path=str(tmp_path / "irrelevant.sock"),
        urn="hw:/usb/bus3/port-9/dev/if0",
    )
    assert exit_code == 0
    out = capsys.readouterr().out
    assert "USB descriptor" in out
    assert "5986:2178" in out
    assert "Video" in out  # class 0x0e decoded
    assert "/dev/bus/usb/003/003" in out  # padded busnum/devnum


def test_info_renders_pci_descriptor(
    tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
) -> None:
    """PCI-kind inspect response renders BDF + vendor/device/class."""

    class _FakeClient:
        @classmethod
        def connect(cls, _path: str) -> _FakeClient:
            return cls()

        def __enter__(self) -> _FakeClient:
            return self

        def __exit__(self, *_exc: object) -> None:
            return None

        def inspect_raw(self, _urn: str) -> dict[str, object]:
            return {
                "urn": "hw:/pci/0000:01:00.0/fn0",
                "family": "claimable",
                "kind": "pcie_function",
                "fidelity": "live",
                "attrs": {
                    "vendor": "0x10de",
                    "device": "0x2d39",
                    "class": "0x030000",
                    "bdf": "0000:01:00.0",
                },
            }

    monkeypatch.setattr("mgf.hrb.Client", _FakeClient, raising=False)
    exit_code = info_command(
        socket_path=str(tmp_path / "irrelevant.sock"),
        urn="hw:/pci/0000:01:00.0/fn0",
    )
    assert exit_code == 0
    out = capsys.readouterr().out
    assert "PCI descriptor" in out
    assert "0000:01:00.0" in out
    assert "0x10de" in out
