"""List refresh observes the child, not its descendants' capture-pipe lifetime."""

import ctypes
import json
import os
from pathlib import Path
import shlex
import signal
import subprocess
import sys
import time
from types import SimpleNamespace
from typing import Any, cast

import pytest


@pytest.mark.platforms("posix")
def test_list_leaves_live_reader_as_completion_owner():
    from tools.process_registry import ProcessRegistry, ProcessSession

    registry = ProcessRegistry()
    session = ProcessSession(
        id="proc_owned_reader",
        command="owned-reader",
        task_id="owner-task",
        owner_task_id="owner-owner",
        session_key="owner-session",
        started_at=time.time(),
        notify_on_complete=True,
    )
    session.process = cast(Any, SimpleNamespace(
        poll=lambda: 0, stdout=None, stderr=None, stdin=None,
    ))
    session._reader_selectable = True
    session._reader_thread = cast(Any, SimpleNamespace(is_alive=lambda: True))
    registry._running[session.id] = session

    listed = registry.list_sessions(session_key="owner-session")

    assert listed[0]["status"] == "exited"
    assert session._reader_finish_requested.is_set()
    assert session.id in registry._running
    assert registry.completion_queue.empty()

    session.append_output("owner-output")
    registry._move_to_finished(session)
    event = registry.completion_queue.get_nowait()
    assert (event["owner_task_id"], event["output"]) == (
        "owner-owner", "owner-output",
    )


@pytest.mark.platforms("linux")
def test_list_reconciles_real_exit_without_consuming_owned_result(tmp_path):
    # A disposable subreaper owns even the orphaned writer; no global pytest
    # process state is changed, and every fixture child is reaped on failure.
    result = subprocess.run(
        [sys.executable, str(Path(__file__).resolve()), "probe", str(tmp_path)],
        cwd=Path(__file__).resolve().parents[2],
        env={**os.environ, "PYTHONPATH": str(Path(__file__).resolve().parents[2])},
        stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=30,
    )
    assert result.returncode == 0, result.stdout + result.stderr


def _probe(root):
    import tools.process_registry as module

    assert ctypes.CDLL(None).prctl(36, 1, 0, 0, 0) == 0  # PR_SET_CHILD_SUBREAPER
    module._SYSTEMD_SCOPE_AVAILABLE = False  # Own the test process tree, not a host service.
    registry = module.ProcessRegistry()
    sessions = []
    command = f"exec {shlex.quote(sys.executable)} {shlex.quote(__file__)}"
    try:
        for name in ("owner", "sibling"):
            session = registry.spawn_local(
                f"{command} child {shlex.quote(str(root / name))}",
                cwd=str(root), task_id=name + "-task", owner_task_id=name + "-owner",
                session_key=name + "-session",
            )
            sessions.append(session)
            session.notify_on_complete = True
        owner, sibling = sessions
        deadline = time.monotonic() + 5
        while not all(name + "-output" in s.output_buffer for name, s in zip(("owner", "sibling"), sessions)):
            assert time.monotonic() < deadline, "writers did not become ready"
            time.sleep(0.01)
        assert all(s.process.poll() is None for s in sessions)
        (root / "owner-exit").touch()
        assert owner.process.wait(timeout=5) == 0
        assert owner._reader_thread.is_alive()  # Writer is still producing output.
        started = time.monotonic()
        listed = registry.list_sessions(session_key="owner-session")
        elapsed = time.monotonic() - started
        print(json.dumps({"direct_child": owner.process.returncode, "listed": listed,
                          "elapsed": elapsed, "reader_alive": owner._reader_thread.is_alive()}), flush=True)
        assert elapsed < 2, "list waited for the descendant's pipe lifetime"
        assert [entry["session_id"] for entry in listed] == [owner.id]
        assert listed[0]["status"] == "exited"
        assert listed[0]["exit_code"] == 0
        event = registry.completion_queue.get(timeout=2)
        assert (event["session_id"], event["session_key"], event["task_id"], event["owner_task_id"]) == (
            owner.id, "owner-session", "owner-task", "owner-owner")
        assert event["exit_code"] == 0 and "owner-output" in event["output"]
        assert registry.unread_completions_owned_by("owner-owner") == [owner]
        assert registry.unread_completions_owned_by("sibling-owner") == []
        for _ in range(3):
            assert registry.list_sessions(session_key="owner-session")[0]["status"] == "exited"
            foreign = registry.list_sessions(session_key="sibling-session")
            assert [(row["session_id"], row["status"]) for row in foreign] == [(sibling.id, "running")]
        assert sibling.process.poll() is None
        (root / "owner-stop").touch()
        owner._reader_thread.join(timeout=5)
        assert not owner._reader_thread.is_alive()
        assert registry.completion_queue.empty(), "reader and list emitted duplicate completions"
        assert not registry.is_completion_consumed(owner.id)
        assert "owner-output" in registry.read_log(owner.id)["output"]
        assert registry.is_completion_consumed(owner.id)
        print("PASS: list-only exit; one exact-owner event; running sibling isolated; unread output retained", flush=True)
    finally:
        for name in ("owner", "sibling"):
            (root / (name + "-stop")).touch()
            (root / (name + "-exit")).touch()
        for session in sessions:
            try:
                os.killpg(session.pid, signal.SIGKILL)
            except ProcessLookupError:
                pass
            session.process.wait(timeout=5)
            session._reader_thread.join(timeout=5)
        # This subprocess contains only our two child trees.
        while True:
            try:
                os.waitpid(-1, 0)
            except ChildProcessError:
                break


def _child(gate):
    subprocess.Popen(
        [sys.executable, __file__, "writer", str(gate)], stdin=subprocess.DEVNULL,
    )
    deadline = time.monotonic() + 15
    while not Path(str(gate) + "-exit").exists() and time.monotonic() < deadline:
        time.sleep(0.01)


def _writer(gate):
    deadline = time.monotonic() + 15
    while not Path(str(gate) + "-stop").exists() and time.monotonic() < deadline:
        print(gate.name + "-output", flush=True)
        time.sleep(0.02)


if __name__ == "__main__":
    {"probe": _probe, "child": _child, "writer": _writer}[sys.argv[1]](Path(sys.argv[2]))
