"""pids-rescue decision logic over synthetic cgroup trees.

Nothing here creates load or kills a process: every cgroup is a directory in
tmp_path and every "pid" is this test's own, or one that never existed.
"""
import importlib.machinery
import importlib.util
import os
from pathlib import Path

import pytest

BIN = Path(__file__).resolve().parents[1] / "bin" / "pids-rescue"

spec = importlib.util.spec_from_loader(
    "pids_rescue", importlib.machinery.SourceFileLoader("pids_rescue", str(BIN))
)
pids_rescue = importlib.util.module_from_spec(spec)
spec.loader.exec_module(pids_rescue)

UID = pids_rescue.UID
SVC = f"/user.slice/user-{UID}.slice/user@{UID}.service"
POISONED = f"{SVC}/agent.slice/confine-agent-2253113-11147.scope"


# --- target validation: the recovery path may not become a new way to kill a session ---


def test_agent_scope_is_accepted():
    assert pids_rescue.is_killable(POISONED) is True


@pytest.mark.parametrize("rel", [
    f"{SVC}/human.slice",
    f"{SVC}/human.slice/human-session-tmux.service",
    f"{SVC}/app.slice/app-org.gnome.Terminal.slice/vte-spawn-a.scope",
    f"/user.slice/user-{UID}.slice/session-364.scope",
    f"{SVC}/agent.slice",
])
def test_human_and_slice_targets_are_refused(rel):
    assert pids_rescue.is_killable(rel) is False


def test_cli_exits_nonzero_on_a_human_target(monkeypatch, capsys):
    monkeypatch.setattr("sys.argv", ["pids-rescue", f"{SVC}/human.slice"])
    assert pids_rescue.main() == 3
    assert "refusing" in capsys.readouterr().err


@pytest.mark.parametrize("arg,expected_rel", [
    (f"/sys/fs/cgroup{POISONED}", POISONED),
    (POISONED, POISONED),
    (f"/sys/fs/cgroup{POISONED}/", POISONED),
])
def test_absolute_and_relative_paths_normalize_alike(arg, expected_rel):
    path, rel = pids_rescue.normalize(arg)
    assert rel == expected_rel
    assert path == f"/sys/fs/cgroup{expected_rel}"


def test_a_relative_argument_is_rejected():
    assert pids_rescue.normalize("agent.slice/x.scope") == (None, None)


# --- evacuation selection ---


def test_keep_selects_by_pid_and_by_comm(monkeypatch):
    monkeypatch.setattr(pids_rescue, "comm", lambda p: {10: "claude", 11: "sleep"}[p])
    assert pids_rescue.select_keep([10, 11], {11}, set()) == [11]
    assert pids_rescue.select_keep([10, 11], set(), {"claude"}) == [10]
    assert pids_rescue.select_keep([10, 11], set(), set()) == []


def test_wedged_reads_the_denied_fork_counter(tmp_path):
    (tmp_path / "pids.events.local").write_text("max 13772\n")
    assert pids_rescue.wedged(str(tmp_path)) is True
    (tmp_path / "pids.events.local").write_text("max 0\n")
    assert pids_rescue.wedged(str(tmp_path)) is False


def test_wedged_is_false_when_the_counter_is_absent(tmp_path):
    assert pids_rescue.wedged(str(tmp_path)) is False


# --- the confirmation that gates the kill ---


def test_move_is_confirmed_only_when_the_pid_left_the_source():
    live = os.getpid()
    assert pids_rescue.verify_move([live], source_pids=[], dest_pids=[live]) is True
    assert pids_rescue.verify_move([live], source_pids=[live], dest_pids=[live]) is False
    assert pids_rescue.verify_move([live], source_pids=[], dest_pids=[]) is False


def test_an_exited_pid_does_not_block_confirmation():
    dead = 4194303  # above pid_max; provably not running
    assert pids_rescue.verify_move([dead], source_pids=[dead], dest_pids=[]) is True


def test_move_pids_skips_a_pid_that_already_exited(tmp_path):
    procs = tmp_path / "cgroup.procs"
    procs.write_text("")
    procs.chmod(0o444)  # stands in for the kernel refusing the write
    dead = 4194303
    assert pids_rescue.move_pids(str(tmp_path), [dead]) == []


def test_read_pids_parses_one_pid_per_line(tmp_path):
    (tmp_path / "cgroup.procs").write_text("101\n102\n\n")
    assert pids_rescue.read_pids(str(tmp_path)) == [101, 102]
