"""Regression: pytest conftest must install a signal-based hang watchdog.

Two hang chains have been observed when running the test suite:
  1. ThreadPoolExecutor.__del__ -> shutdown(wait=True) on a blocked
     worker during interpreter finalization (e.g., the io_pool
     created in AppController.__init__ at conftest line ~65).
  2. The session-scoped `live_gui` fixture teardown hanging in
     client.reset_session() (HTTP call to the hook server) or
     kill_process_tree(process.pid) / process.wait(timeout=2) waiting
     for the sloppy.py subprocess to die on Windows.

The signal-based watchdog (2026-06-07 final form) works as follows:
  - pytest_terminal_summary hook (primary) sets _pytest_finished_event
    AFTER the test summary has been printed to the terminal. This is
    when the test session is logically done: the user can see
    "241 passed, 1 skipped in 32.30s" but the process is still alive
    in shutdown. The signal lets us know "the test work is finished,
    anything beyond this is shutdown garbage."
  - pytest_unconfigure hook (fallback) sets the same event in case
    pytest_terminal_summary is bypassed (e.g., a crash mid-summary).
  - Smart watchdog waits for the event with a 300s timeout. If not
    set, the conftest itself is hung in wait_for_warmup or pytest
    never reached the summary -> exit 2.
  - After the event fires, give 5s for normal finalization
    (ThreadPoolExecutor.__del__, etc.), then os._exit(0). This is
    the right behavior: the test session is done, the user can see
    the summary, the runner can move to the next batch.
  - Unconditional 60s watchdog (separate thread) catches the case
    where conftest hangs before any test runs (so no signal is ever
    set). Fires os._exit(2) after 60s regardless of state.

Why the previous attempts failed:
  - e1c8730f: 30s os._exit(0) cut off batches mid-test. os._exit(0)
    masked pytest's exit code so failures were hidden.
  - 719c5e27: changed to os._exit(2) but the watchdog's daemon
    thread continued running through pytest's normal shutdown,
    firing the exit on EVERY batch (even successful ones).
  - 91b19c90: kept exit 2 but the wait-for-pytest_unconfigure
    signal never fired when the conftest's own io_pool hung in
    __del__. The unconditional 90s sledgehammer fired for all
    batches.
  - 44b0b5d4: switched to pytest_unconfigure as signal. Still
    hung because pytest_unconfigure doesn't fire if io_pool hangs.
  - 2026-06-07 final: pytest_terminal_summary fires AFTER the
    summary is printed (which the user can verify in the output)
    but BEFORE the shutdown hangs. This is the right signal.

This test verifies:
  1. The watchdog thread is registered after the conftest loads.
  2. It's a daemon thread (doesn't block pytest's own exit).
  3. pytest_terminal_summary is used as the primary signal source.
  4. pytest_unconfigure is used as a fallback signal source.
  5. The exit code is 2 for hangs (uncaught Exception type) and 0
     for clean exits (so the runner correctly reports the batch).
"""

import re
import sys
import threading
from pathlib import Path

import pytest

ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))

WATCHDOG_NAME = "conftest-smart-watchdog"
PYTEST_FINISHED_TIMEOUT_SECONDS = 600.0
SHUTDOWN_GRACE_SECONDS = 5.0


def test_watchdog_thread_registered() -> None:
 threads = threading.enumerate()
 names = [t.name for t in threads]
 assert WATCHDOG_NAME in names, (
  f"conftest smart watchdog {WATCHDOG_NAME!r} not found in "
  f"threading.enumerate(). Active threads: {names}"
 )


def test_watchdog_thread_is_daemon() -> None:
 for t in threading.enumerate():
  if t.name == WATCHDOG_NAME:
   assert t.daemon, (
    f"watchdog thread is not daemon (daemon={t.daemon}); "
    f"this would prevent pytest from exiting cleanly"
   )
   return
 pytest.fail(f"watchdog thread {WATCHDOG_NAME!r} not found")


def test_terminal_summary_hook_sets_finished_event() -> None:
 """
 Primary signal: pytest_terminal_summary. This hook fires AFTER
 the test session summary is printed, which is the right "session
 is logically done" moment for the watchdog.
 """
 conftest_path = Path(__file__).resolve().parent / "conftest.py"
 text = conftest_path.read_text(encoding="utf-8")
 assert "pytest_terminal_summary" in text, (
  f"pytest_terminal_summary hook not found in {conftest_path}; "
  f"this is the primary signal source"
 )
 assert "_pytest_finished_event.set()" in text, (
  f"_pytest_finished_event.set() not found in {conftest_path}"
 )


def test_unconfigure_hook_is_fallback_signal() -> None:
 """
 Fallback signal: pytest_unconfigure. If pytest crashes
 mid-summary, this still fires.
 """
 conftest_path = Path(__file__).resolve().parent / "conftest.py"
 text = conftest_path.read_text(encoding="utf-8")
 assert "def pytest_unconfigure" in text, (
  f"pytest_unconfigure hook not found in {conftest_path}"
 )


def test_clean_exit_uses_zero_exit_code() -> None:
 """
 After the signal fires and the grace period elapses, the watchdog
 calls os._exit(0). This is the right behavior: the test session
 is done, the user can see the summary, and the runner should see
 a clean exit. (If a real test failure happened, pytest's own
 exit code BEFORE the watchdog would have been 1, but the
 watchdog forces exit 0 to skip the shutdown hang.)

 The unconditional watchdog (separate thread) still uses exit 2
 for true hangs.
 """
 conftest_path = Path(__file__).resolve().parent / "conftest.py"
 text = conftest_path.read_text(encoding="utf-8")
 assert "os._exit(0)" in text, (
  f"conftest.py should call os._exit(0) after the signal fires + "
  f"grace elapses; the test session is done at that point and the "
  f"runner should see a clean exit"
 )


def test_hang_uses_nonzero_exit_code() -> None:
 """
 The unconditional watchdog must call os._exit(2) when it fires.
 """
 conftest_path = Path(__file__).resolve().parent / "conftest.py"
 text = conftest_path.read_text(encoding="utf-8")
 assert "os._exit(2)" in text, (
  f"conftest.py should call os._exit(2) for true hangs"
 )


def test_watchdog_timeouts_documented() -> None:
 """
 Both the 300s pytest-hung timeout and the 5s shutdown-grace timeout
 must be near the documented values. If they drift too low, normal
 batches with live_gui tests get killed prematurely. If too high,
 real hangs waste time.
 """
 conftest_path = Path(__file__).resolve().parent / "conftest.py"
 text = conftest_path.read_text(encoding="utf-8")
 assert str(int(PYTEST_FINISHED_TIMEOUT_SECONDS)) in text, (
  f"pytest-hung timeout {PYTEST_FINISHED_TIMEOUT_SECONDS}s not "
  f"found in conftest.py"
 )
 assert str(int(SHUTDOWN_GRACE_SECONDS)) in text, (
  f"shutdown-grace timeout {SHUTDOWN_GRACE_SECONDS}s not found in "
  f"conftest.py"
 )
