"""Tests for the GUI-updater hand-off backend reap (_handoff_reapable_backend_pids).

Field incident (2026-08-20, Teknium's Windows box): a Desktop update hand-off
(`hermes update --yes --gateway --force`) left a *swarm* of per-profile `serve`
backends (mr-tester, probe-inherit, turqoise, clippy, maroon, …) holding
`cryptography\\_rust.pyd`. Some still had a live parent (the tearing-down
Electron process, or the venv launcher→worker two-hop chain mid-exit), so the
strict orphan-only reap (_orphaned_desktop_backend_pids) disqualified the whole
set and the update dead-ended — a 12-minute hang, then a force-close that
stranded bot sessions.

_handoff_reapable_backend_pids is the additional rung that ONLY runs in the
hand-off context (caller gates on args.gateway + the update-incomplete marker +
no live hermes.exe shim). There, any surviving Hermes `serve`/`dashboard`
backend from this venv is a leak — live parent or not — and safe to reap.
A non-backend holder still disqualifies the whole set.

Runs on any host via a fake psutil module (same approach as
test_update_orphan_backend_reap.py).
"""

from __future__ import annotations

import sys
import types
from unittest.mock import MagicMock, patch

from hermes_cli import main as cli_main


class _FakeNoSuchProcess(Exception):
    pass


def _fake_psutil(procs: dict[int, MagicMock]):
    def _process(pid: int):
        if pid not in procs:
            raise _FakeNoSuchProcess(pid)
        return procs[pid]

    return types.SimpleNamespace(Process=_process, NoSuchProcess=_FakeNoSuchProcess)


def _proc(pid: int, cmdline: list[str]):
    proc = MagicMock()
    proc.pid = pid
    proc.cmdline.return_value = cmdline
    return proc


def _serve_argv(profile: str = "mr-tester") -> list[str]:
    return [
        "C:\\hermes\\venv\\Scripts\\python.exe",
        "-m",
        "hermes_cli.main",
        "--profile",
        profile,
        "serve",
        "--host",
        "127.0.0.1",
        "--port",
        "0",
    ]


def _holder(pid: int, cmdline: str):
    return (pid, "python.exe", cmdline)


def test_live_parent_backend_reaped_in_handoff():
    # The exact case the orphan-only path REFUSES: a serve backend that still
    # has a live parent. In the hand-off context it must still be reaped.
    backend = _proc(200, _serve_argv("mr-tester"))
    fake = _fake_psutil({200: backend})
    with patch.dict(sys.modules, {"psutil": fake}):
        holders = [_holder(200, "python.exe -m hermes_cli.main --profile mr-tester serve")]
        assert cli_main._handoff_reapable_backend_pids(holders) == [200]


def test_swarm_of_profile_backends_all_reaped():
    profiles = ["mr-tester", "probe-inherit", "turqoise", "clippy", "maroon"]
    procs = {200 + i: _proc(200 + i, _serve_argv(p)) for i, p in enumerate(profiles)}
    fake = _fake_psutil(procs)
    with patch.dict(sys.modules, {"psutil": fake}):
        holders = [
            _holder(200 + i, f"python.exe -m hermes_cli.main --profile {p} serve")
            for i, p in enumerate(profiles)
        ]
        assert sorted(cli_main._handoff_reapable_backend_pids(holders)) == sorted(procs)


def test_dashboard_backend_reaped():
    backend = _proc(200, ["python.exe", "-m", "hermes_cli.main", "dashboard"])
    fake = _fake_psutil({200: backend})
    with patch.dict(sys.modules, {"psutil": fake}):
        holders = [_holder(200, "python.exe -m hermes_cli.main dashboard")]
        assert cli_main._handoff_reapable_backend_pids(holders) == [200]


def test_non_backend_holder_disqualifies_whole_set():
    # An operator REPL / stray script during a hand-off is unexpected — refuse
    # the whole set rather than reap something we can't justify.
    backend = _proc(200, _serve_argv("mr-tester"))
    repl = _proc(300, ["python.exe", "-m", "hermes_cli.main", "chat"])
    fake = _fake_psutil({200: backend, 300: repl})
    with patch.dict(sys.modules, {"psutil": fake}):
        holders = [
            _holder(200, "python.exe -m hermes_cli.main --profile mr-tester serve"),
            _holder(300, "python.exe -m hermes_cli.main chat"),
        ]
        assert cli_main._handoff_reapable_backend_pids(holders) is None


def test_exited_holder_skipped_not_fatal():
    # A holder that vanished between scan and classification is skipped, and the
    # remaining real backend still qualifies.
    backend = _proc(200, _serve_argv("mr-tester"))
    fake = _fake_psutil({200: backend})  # 300 absent → NoSuchProcess
    with patch.dict(sys.modules, {"psutil": fake}):
        holders = [
            _holder(300, "python.exe -m hermes_cli.main --profile gone serve"),
            _holder(200, "python.exe -m hermes_cli.main --profile mr-tester serve"),
        ]
        assert cli_main._handoff_reapable_backend_pids(holders) == [200]


def test_no_holders_returns_none():
    fake = _fake_psutil({})
    with patch.dict(sys.modules, {"psutil": fake}):
        assert cli_main._handoff_reapable_backend_pids([]) is None


def test_psutil_unavailable_returns_none():
    # Can't re-read argv to classify → refuse (leave the decision to the
    # caller's existing rungs / the dead-end).
    import builtins

    real_import = builtins.__import__

    def _no_psutil(name, *a, **k):
        if name == "psutil":
            raise ImportError("no psutil")
        return real_import(name, *a, **k)

    with patch.dict(sys.modules, {}, clear=False):
        sys.modules.pop("psutil", None)
        with patch("builtins.__import__", _no_psutil):
            holders = [_holder(200, "python.exe -m hermes_cli.main --profile x serve")]
            assert cli_main._handoff_reapable_backend_pids(holders) is None
