"""Operator cancel (C/M1, testctl cancel): a run can be stopped from any origin.

Cancel routes through the warden like every other intent. The load-bearing invariant
is that **single-flight is never broken by a cancel**: cancelling a running lease
signals its child but does NOT drop the lease or advance the queue until that child
actually exits — so no queued run starts while the cancelled one is still alive. A
queued request is simply dropped (no child). The integration test proves the daemon
delivers a real SIGTERM that kills a real child; the CLI/web tests prove cancel is
fail-closed when the warden is down and honest when the run is already gone.
"""

from __future__ import annotations

import os
import subprocess
import sys
import threading
import time
from pathlib import Path

import pytest
from fastapi.testclient import TestClient
from mgf.fedres.canonical import dumpb

from mgf.test_web.catalog import build_catalog, catalog_path
from mgf.test_web.testctl import main
from mgf.test_web.warden import RunRequest, Warden, WardenClient, WardenDaemon
from mgf.test_web.web import create_app


def _gate_runner(gate: threading.Event, *, pid: int = 4242):
    def runner(request: RunRequest, emit) -> str:  # type: ignore[no-untyped-def]
        emit({"kind": "test_start", "pid": pid})  # the warden lifts pid onto the lease
        gate.wait(timeout=5)
        return "pass"

    return runner


# --------------------------------------------------------------------------- #
# the broker — cancel preserves single-flight                                  #
# --------------------------------------------------------------------------- #
@pytest.mark.concurrency
def test_cancel_running_holds_the_lease_until_exit_then_advances() -> None:
    gate = threading.Event()
    warden = Warden("proj", _gate_runner(gate))
    warden.submit(RunRequest("r1", "T-1"))
    warden.submit(RunRequest("r2", "T-2"))  # different test → queues behind r1

    # wait until the warden has lifted the child's pid onto the running lease
    for _ in range(300):
        leases = list(warden.registry.running.values())
        if leases and leases[0].pid == 4242:
            break
        time.sleep(0.01)

    out = warden.cancel("r1")
    assert out.found and out.state == "running" and out.pid == 4242
    # single-flight holds: the lease is NOT dropped and the queued run has NOT started
    assert "proj" in warden.registry.running
    assert any(q.run_id == "r2" for q in warden.registry.queue)

    # the cancelled child exits → the lease releases as "cancelled", the queue advances
    gate.set()
    assert warden.wait_idle(timeout=5)
    assert warden.result("r1") == "cancelled"  # authoritative intent, not the exit code
    assert warden.result("r2") == "pass"  # the queued run started only after release
    assert "cancelled" in [a["event"] for a in warden.status()["recent"]]


@pytest.mark.concurrency
def test_cancel_queued_request_dequeues_before_start() -> None:
    gate = threading.Event()
    warden = Warden("proj", _gate_runner(gate))
    warden.submit(RunRequest("r1", "T-1"))
    for _ in range(300):
        if warden.registry.running:
            break
        time.sleep(0.01)
    warden.submit(RunRequest("r2", "T-2"))  # queues behind the running r1

    out = warden.cancel("r2")
    assert out.found and out.state == "queued" and out.pid is None
    assert not any(q.run_id == "r2" for q in warden.registry.queue)

    gate.set()
    assert warden.wait_idle(timeout=5)
    assert warden.result("r2") is None  # it never ran


def test_cancel_of_an_unknown_run_is_honest_not_an_error() -> None:
    out = Warden("proj", lambda r, e: "pass").cancel("nope")  # type: ignore[arg-type,return-value]
    assert out.found is False and out.state == "absent" and out.pid is None


# --------------------------------------------------------------------------- #
# the daemon — a real cancel signals + kills a real child                      #
# --------------------------------------------------------------------------- #
def _process_gone(pid: int, *, timeout: float = 5.0) -> bool:
    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        try:
            os.kill(pid, 0)
        except ProcessLookupError:
            return True
        time.sleep(0.02)
    return False


@pytest.mark.concurrency
def test_cancel_kills_the_real_child_over_the_socket(tmp_path: Path) -> None:
    started = threading.Event()
    holder: dict[str, int] = {}

    def runner(request: RunRequest, emit) -> str:  # type: ignore[no-untyped-def]
        proc = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(30)"])
        holder["pid"] = proc.pid
        emit({"kind": "test_start", "pid": proc.pid})
        started.set()
        proc.wait()  # released only when the child exits (the cancel's SIGTERM)
        return "pass"

    sock = tmp_path / "warden.sock"
    warden = Warden("proj", runner)
    daemon = WardenDaemon(warden, sock)
    daemon.start_background()
    client = WardenClient(sock)
    try:
        for _ in range(200):
            if client.ping():
                break
            time.sleep(0.01)
        client.run(RunRequest("r1", "T-1"))
        assert started.wait(timeout=5)
        child_pid = holder["pid"]

        resp = client.cancel("r1")
        assert resp["found"] and resp["state"] == "running" and resp["pid"] == child_pid

        assert warden.wait_idle(timeout=10)
        assert warden.result("r1") == "cancelled"
        assert _process_gone(child_pid), "the daemon's SIGTERM should have killed the child"
    finally:
        daemon.shutdown()


@pytest.mark.concurrency
def test_cancel_unknown_over_the_socket_reports_not_found(tmp_path: Path) -> None:
    sock = tmp_path / "warden.sock"
    daemon = WardenDaemon(Warden("proj", lambda r, e: "pass"), sock)  # type: ignore[arg-type,return-value]
    daemon.start_background()
    client = WardenClient(sock)
    try:
        for _ in range(200):
            if client.ping():
                break
            time.sleep(0.01)
        resp = client.cancel("ghost")
        assert resp["ok"] and resp["found"] is False and resp["state"] == "absent"
    finally:
        daemon.shutdown()


# --------------------------------------------------------------------------- #
# the CLI + the console — fail-closed, honest                                  #
# --------------------------------------------------------------------------- #
def test_testctl_cancel_refuses_when_the_warden_is_down(
    tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
    rc = main(["-C", str(tmp_path), "cancel", "run_x"])
    assert rc == 1  # fail-closed: nonzero, nothing signalled
    assert "REFUSED" in capsys.readouterr().out


def test_web_cancel_when_warden_down_is_fail_closed(tmp_path: Path) -> None:
    # a discoverable project (catalog on disk) but no warden running
    path = catalog_path(tmp_path)
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_bytes(dumpb(build_catalog(["tests/test_demo.py::test_one"], tmp_path.name)))

    client = TestClient(create_app(roots=[tmp_path]))
    resp = client.post(f"/p/{tmp_path.name}/cancel", data={"run_id": "run_x"})
    assert resp.status_code == 200  # never a 500
    assert "fail-closed" in resp.text  # the refused note renders honestly
