import os
import subprocess
import threading
import time

import pytest

if os.name != "posix":
    pytest.skip("terminal process cleanup tests require POSIX terminal support", allow_module_level=True)

import api.terminal as terminal


class _DummyThread:
    def __init__(self, *args, **kwargs):
        self.args = args
        self.kwargs = kwargs
        self.started = False

    def start(self):
        self.started = True


class _FakeProc:
    pid = 999_999_999

    def __init__(self):
        self.wait_calls = []

    def poll(self):
        return None

    def wait(self, timeout=None):
        self.wait_calls.append(timeout)
        return 0


def test_terminal_shell_does_not_use_pdeathsig_preexec(monkeypatch, tmp_path):
    """Regression for #2853.

    The previous implementation passed a ``preexec_fn`` that called
    ``prctl(PR_SET_PDEATHSIG, SIGTERM)``.  Because that signal is *per-thread*
    and WebUI's ``ThreadingHTTPServer`` spawns a new thread for every HTTP
    request, the PTY shell registered the request-handler thread as its
    parent and was killed within ~10 ms of being created on Linux.

    The fix is to spawn the shell without ``preexec_fn`` at all.  Graceful
    shutdown remains covered by ``atexit.register(close_all_terminals)`` and
    the explicit ``close_terminal`` paths.
    """
    captured = {}
    proc = _FakeProc()

    def fake_popen(*args, **kwargs):
        captured["args"] = args
        captured["kwargs"] = kwargs
        return proc

    monkeypatch.setattr(terminal.subprocess, "Popen", fake_popen)
    monkeypatch.setattr(terminal.threading, "Thread", _DummyThread)
    monkeypatch.setattr(terminal, "_set_size", lambda *args, **kwargs: None)

    term = terminal.start_terminal("term-no-preexec", tmp_path)

    try:
        assert term.proc is proc
        assert "preexec_fn" not in captured["kwargs"], (
            "preexec_fn must not be set — the PR_SET_PDEATHSIG implementation "
            "killed every Linux user's terminal (#2853). See module-level note."
        )
        assert captured["kwargs"]["start_new_session"] is True
        assert captured["kwargs"]["stdin"] == captured["kwargs"]["stdout"] == captured["kwargs"]["stderr"]
    finally:
        terminal.close_terminal("term-no-preexec")


@pytest.mark.skipif(
    not hasattr(os, "openpty") or os.name != "posix",
    reason="PTY-spawn test requires a POSIX host",
)
def test_pty_shell_survives_when_spawning_thread_exits(tmp_path):
    """End-to-end regression for #2853.

    Spawn a real PTY shell via ``start_terminal`` from inside a worker thread
    that then exits.  The shell must remain alive after the spawning thread
    joins, otherwise we've regressed back to the PR_SET_PDEATHSIG behaviour
    that killed every Linux user's embedded terminal.
    """
    sid = "term-thread-survival"
    holder: dict = {}

    def worker():
        try:
            holder["term"] = terminal.start_terminal(sid, tmp_path)
        except Exception as exc:  # pragma: no cover - surface in assertion
            holder["error"] = exc

    t = threading.Thread(target=worker)
    t.start()
    t.join(timeout=5)
    assert not t.is_alive(), "spawn worker thread should have exited"
    assert "error" not in holder, holder.get("error")
    term = holder["term"]

    try:
        # Give the kernel a beat — if PR_SET_PDEATHSIG were re-introduced the
        # shell would receive SIGTERM right about now.
        time.sleep(0.5)
        assert term.proc.poll() is None, (
            "PTY shell exited after the spawning thread joined — likely a "
            "PR_SET_PDEATHSIG regression (#2853). "
            f"exit_code={term.proc.poll()!r}"
        )
    finally:
        terminal.close_terminal(sid)


def test_close_terminal_waits_again_after_sigkill(monkeypatch):
    class TimeoutThenReapedProc(_FakeProc):
        def wait(self, timeout=None):
            self.wait_calls.append(timeout)
            if len(self.wait_calls) == 1:
                raise subprocess.TimeoutExpired(cmd="shell", timeout=timeout)
            return -9

    proc = TimeoutThenReapedProc()
    term = terminal.TerminalSession(
        session_id="term-timeout",
        workspace="/tmp",
        proc=proc,
        master_fd=12345,
    )
    terminal._TERMINALS["term-timeout"] = term
    kills = []
    monkeypatch.setattr(terminal.os, "killpg", lambda pid, sig: kills.append((pid, sig)))
    monkeypatch.setattr(terminal.os, "close", lambda fd: None)

    assert terminal.close_terminal("term-timeout") is True

    assert proc.wait_calls == [1.5, 1.0]
    assert kills == [(proc.pid, terminal.signal.SIGHUP), (proc.pid, terminal.signal.SIGKILL)]


def test_close_all_terminals_closes_snapshot(monkeypatch):
    terminal._TERMINALS.clear()
    terminal._TERMINALS.update({"a": object(), "b": object()})
    closed = []

    def fake_close(session_id):
        closed.append(session_id)
        terminal._TERMINALS.pop(session_id, None)
        return True

    monkeypatch.setattr(terminal, "close_terminal", fake_close)

    terminal.close_all_terminals()

    assert closed == ["a", "b"]
    assert terminal._TERMINALS == {}


def test_terminal_module_registers_graceful_shutdown_reaper():
    """atexit is still the reap path; pdeathsig must NOT be re-introduced."""
    src = terminal.Path(terminal.__file__).read_text()

    assert "atexit.register(close_all_terminals)" in src
    # The PR_SET_PDEATHSIG implementation broke every Linux user (#2853);
    # guard against accidentally bringing it back.
    assert "preexec_fn=_terminal_shell_preexec_fn" not in src
    assert "libc.prctl(1, signal.SIGTERM)" not in src
