"""The timeout/kill path must not crash the run when a hung test holds
the pipes open after SIGKILL (F-TS-001).

Regression: previously the final `communicate(timeout=5.0)` after
`proc.kill()` was unwrapped — a test-spawned child surviving the kill
raised an unhandled TimeoutExpired that crashed the whole run, losing
the chunk's outcomes. Now pytest runs in its own session (group-kill)
and the final communicate is guarded.
"""

from __future__ import annotations

import signal
import subprocess
from typing import TYPE_CHECKING

from mgf.test_supervisor import supervisor as sup_mod
from mgf.test_supervisor.supervisor import RunConfig, Supervisor

if TYPE_CHECKING:
    from pathlib import Path

    import pytest


class _AlwaysTimeoutProc:
    """A Popen stand-in whose communicate() always times out — models a
    test-spawned child that keeps the stdout/stderr pipes open."""

    def __init__(self) -> None:
        self.pid = 2**31 - 1  # not a live pid → killpg falls back to send_signal
        self.returncode = -int(signal.SIGKILL)
        self.signals: list[int] = []

    def communicate(self, timeout: float | None = None) -> tuple[str, str]:
        raise subprocess.TimeoutExpired(cmd="pytest", timeout=timeout or 0)

    def send_signal(self, sig: int) -> None:
        self.signals.append(sig)

    def kill(self) -> None:
        self.signals.append(int(signal.SIGKILL))


def test_run_chunk_survives_unkillable_pipe_holder(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    cfg = RunConfig(
        repo_root=tmp_path,
        gates_dir=tmp_path / ".gates",
        test_ids=["t1"],
        test_markers={},
        run_id="killtest",
        pytest_cmd=["python", "-m", "pytest"],
    )
    sup = Supervisor(cfg)
    sup.last_run_dir.mkdir(parents=True, exist_ok=True)

    fake = _AlwaysTimeoutProc()
    monkeypatch.setattr(sup_mod.subprocess, "Popen", lambda *a, **k: fake)

    # Must NOT raise, even though every communicate() times out.
    result = sup._run_chunk(0, ["t1"])

    assert result.hit_timeout is True
    # Escalation reached SIGTERM then SIGKILL (via the send_signal fallback,
    # since the fake pid isn't a real process group).
    assert int(signal.SIGTERM) in fake.signals
    assert int(signal.SIGKILL) in fake.signals
