#!/usr/bin/env python3
"""Run each offline integration-test module in an isolated process group."""

from __future__ import annotations

import argparse
import os
import signal
import subprocess
import sys
import time
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "libexec"))

from mmo_util import filtered_environment, terminate_process_group  # noqa: E402

DEFAULT_MODULES = (
    "tests.test_catalog_profiles",
    "tests.test_runtime",
    "tests.test_runtime_advanced",
    "tests.test_tui_metadata",
    "tests.test_install_eval",
    "tests.test_cli_ux",
    "tests.test_release",
)
_CURRENT_PROCESS: subprocess.Popen[str] | None = None


def _terminate_group(process: subprocess.Popen[str]) -> None:
    terminate_process_group(process.pid, grace_seconds=3.0)
    try:
        process.wait(timeout=2)
    except subprocess.TimeoutExpired:
        pass


def _handle_termination(_signum: int, _frame: object) -> None:
    if _CURRENT_PROCESS is not None:
        _terminate_group(_CURRENT_PROCESS)
    raise KeyboardInterrupt


def run_module(module: str, timeout: float) -> tuple[bool, float]:
    global _CURRENT_PROCESS
    environment = filtered_environment(
        extra={
            "PYTHONDONTWRITEBYTECODE": "1",
            "PYTHONWARNINGS": "error::ResourceWarning",
            "PYTHONPATH": os.pathsep.join((str(ROOT / "libexec"), str(ROOT / "tests"))),
        }
    )
    command = [sys.executable, "-m", "unittest", "-v", module]
    print(f"\n=== {module} ===", flush=True)
    started = time.monotonic()
    process = subprocess.Popen(
        command,
        cwd=ROOT,
        env=environment,
        text=True,
        start_new_session=True,
    )
    _CURRENT_PROCESS = process
    try:
        try:
            return_code = process.wait(timeout=timeout)
        except subprocess.TimeoutExpired:
            print(
                f"ERROR: {module} exceeded {timeout:g} seconds; terminating its process group",
                file=sys.stderr,
                flush=True,
            )
            return False, time.monotonic() - started
    finally:
        # A test process can exit while leaving a child in its isolated group.
        # Retire that group on success, timeout, interruption, and wait errors.
        _terminate_group(process)
        _CURRENT_PROCESS = None
    return return_code == 0, time.monotonic() - started


def main() -> int:
    parser = argparse.ArgumentParser(
        description="Run Codex MMO offline tests with per-module process isolation"
    )
    parser.add_argument("modules", nargs="*", default=list(DEFAULT_MODULES))
    parser.add_argument("--timeout", type=float, default=300.0)
    args = parser.parse_args()
    if args.timeout <= 0:
        parser.error("--timeout must be positive")

    signal.signal(signal.SIGTERM, _handle_termination)
    signal.signal(signal.SIGINT, _handle_termination)

    failures: list[str] = []
    durations: dict[str, float] = {}
    for module in args.modules:
        passed, duration = run_module(module, args.timeout)
        durations[module] = duration
        if not passed:
            failures.append(module)

    print("\n=== isolated suite summary ===")
    for module, duration in durations.items():
        status = "PASS" if module not in failures else "FAIL"
        print(f"{status:4} {duration:8.2f}s  {module}")
    if failures:
        print(
            "failed modules: " + ", ".join(failures),
            file=sys.stderr,
        )
        return 1
    print(f"all {len(durations)} modules passed")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
