"""Worker exit classification must not depend on POSIX-only ``os.WIF*`` helpers.

``_classify_worker_exit`` feeds the dead-worker reclaim: a ``rate_limited``
verdict requeues the card without counting a failure, ``unknown`` counts as a
crash. Windows has neither ``os.WIFEXITED`` nor ``waitpid(-1)``, so both the
decode and the reaper's exit capture have a Windows-independent path.
"""

from __future__ import annotations

import os
import subprocess
import sys

import pytest

from hermes_cli import kanban_db as kb
from hermes_cli import kanban_db_dispatch as kbd


def _spawn_exit(code: int) -> subprocess.Popen:
    proc = subprocess.Popen([sys.executable, "-c", f"raise SystemExit({code})"])  # noqa: S603
    proc.wait()
    return proc


def test_windows_reaper_classifies_parked_popen_exit(monkeypatch):
    """With the Windows flag on, the reaper learns exit codes from the parked
    ``Popen`` handles (no ``waitpid``) and the decode needs no ``os.WIF*``:
    the rate-limit sentinel is ``rate_limited``, a crash ``nonzero_exit``,
    and a still-running worker is left alone."""
    monkeypatch.setattr(kb, "_IS_WINDOWS", True)
    for name in ("WIFEXITED", "WEXITSTATUS", "WIFSIGNALED", "WTERMSIG"):
        monkeypatch.delattr(os, name, raising=False)
    monkeypatch.setattr(kbd, "_live_worker_procs", {})
    monkeypatch.setattr(kbd, "_recent_worker_exits", {})

    limited = _spawn_exit(kb.KANBAN_RATE_LIMIT_EXIT_CODE)
    crashed = _spawn_exit(3)
    alive = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(30)"])  # noqa: S603
    try:
        for proc in (limited, crashed, alive):
            kbd._live_worker_procs[proc.pid] = proc

        reaped = kbd.reap_worker_zombies()

        assert sorted(reaped) == sorted([limited.pid, crashed.pid])
        assert kbd._classify_worker_exit(limited.pid) == ("rate_limited", kb.KANBAN_RATE_LIMIT_EXIT_CODE)
        assert kbd._classify_worker_exit(crashed.pid) == ("nonzero_exit", 3)
        assert kbd._classify_worker_exit(alive.pid) == ("unknown", None)
        assert set(kbd._live_worker_procs) == {alive.pid}
    finally:
        alive.kill()
        alive.wait()


@pytest.mark.windows_only
def test_native_windows_reaper_and_decode(monkeypatch):
    """Native Windows, nothing patched: ``_IS_WINDOWS`` selects the Popen-poll
    reaper and the decode runs where ``os.WIFEXITED`` does not exist, so the
    rate-limit sentinel exit is a requeue, not a crash."""
    monkeypatch.setattr(kbd, "_live_worker_procs", {})
    monkeypatch.setattr(kbd, "_recent_worker_exits", {})
    assert not hasattr(os, "WIFEXITED")
    proc = _spawn_exit(kb.KANBAN_RATE_LIMIT_EXIT_CODE)
    kbd._live_worker_procs[proc.pid] = proc
    assert kbd.reap_worker_zombies() == [proc.pid]
    assert kbd._classify_worker_exit(proc.pid) == ("rate_limited", kb.KANBAN_RATE_LIMIT_EXIT_CODE)
