"""Pin the psutil-free process helpers.

psutil is an optional extra, so on a base install these are the only way
`ncp stop` / `nexus status` can see or stop a service. Windows previously had
no fallback at all: pid_is_ours returned False, kill_port returned False, and
the terminate branch was POSIX-only, so stop was a no-op there.

Probing must also stay side-effect free. That is easy to get wrong on Windows,
where os.kill(pid, sig) is TerminateProcess for every sig except 0 and the two
console-control events - os.kill(pid, 15) kills instead of asking.
"""
from __future__ import annotations

import subprocess
import sys
import time

import pytest

from synapse import proc_util


def _spawn():
    return subprocess.Popen(
        [sys.executable, "-c", "import time; time.sleep(30)"],
        stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
    )


def _wait_gone(proc, timeout=10.0) -> bool:
    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        if proc.poll() is not None:
            return True
        time.sleep(0.05)
    return False


@pytest.fixture
def victim():
    proc = _spawn()
    try:
        yield proc
    finally:
        if proc.poll() is None:
            proc.kill()
            proc.wait(timeout=10)


def test_pid_alive_does_not_kill_the_process(victim):
    """Probing must be side-effect free, however it is implemented."""
    for _ in range(5):
        assert proc_util.pid_alive(victim.pid) is True
    time.sleep(0.3)
    assert victim.poll() is None, "pid_alive() terminated the process it probed"


def test_pid_alive_is_false_for_a_dead_pid(victim):
    victim.kill()
    victim.wait(timeout=10)
    assert proc_util.pid_alive(victim.pid) is False


@pytest.mark.parametrize("pid", [None, 0, -1, "not-a-pid"])
def test_pid_alive_rejects_junk(pid):
    assert proc_util.pid_alive(pid) is False


def test_terminate_pid_actually_stops_it(victim):
    assert proc_util.terminate_pid(victim.pid) is True
    assert _wait_gone(victim), "terminate_pid() did not stop the process"
    assert proc_util.pid_alive(victim.pid) is False


def test_terminate_pid_is_false_when_already_gone(victim):
    victim.kill()
    victim.wait(timeout=10)
    assert proc_util.terminate_pid(victim.pid) is False


def test_pid_cmdline_identifies_the_process(victim):
    cmd = proc_util.pid_cmdline(victim.pid)
    if not cmd:
        pytest.skip("no command-line source on this host")
    assert "time.sleep" in cmd or "python" in cmd.lower()


def test_iter_processes_includes_this_interpreter():
    import os

    entries = proc_util.iter_processes()
    if not entries:
        pytest.skip("no process enumeration available on this host")
    assert os.getpid() in {pid for pid, _ in entries}
