import signal
import subprocess

from a2a_agent import worker_instance


def test_is_worker_command_requires_run_as_the_actual_subcommand() -> None:
    assert worker_instance.is_worker_command("a2a-agent run")
    assert worker_instance.is_worker_command("python -m a2a_agent.cli run")
    assert not worker_instance.is_worker_command("a2a-agent send worker-a run")
    assert not worker_instance.is_worker_command(
        "a2a-agent send worker-a 'please run a2a-agent run'"
    )


def test_worker_process_requires_home_evidence_to_avoid_commander_kill(tmp_path) -> None:
    assert not worker_instance.is_worker_command_for_home("a2a-agent run", tmp_path)
    assert worker_instance.is_worker_command_for_home(
        f"A2A_HOME={tmp_path} a2a-agent run",
        tmp_path,
    )
    assert worker_instance.is_worker_command_for_home(
        "C:\\Python\\Scripts\\a2a-agent.exe run",
        tmp_path,
        parent_command=f'cmd.exe /c "{tmp_path}/start-worker.cmd"',
    )


def test_claim_worker_instance_creates_pid_file_when_none_exists(tmp_path) -> None:
    claim = worker_instance.claim_worker_instance(tmp_path, pid=101)

    assert claim.enabled is True
    assert claim.warnings == ()
    assert worker_instance.read_worker_pid(tmp_path / "worker.pid") == 101


def test_claim_worker_instance_replaces_stale_pid_file(tmp_path) -> None:
    pid_file = tmp_path / "worker.pid"
    pid_file.write_text("999\n", encoding="ascii")

    claim = worker_instance.claim_worker_instance(
        tmp_path,
        pid=101,
        is_pid_running=lambda pid: False,
    )

    assert claim.enabled is True
    assert worker_instance.read_worker_pid(pid_file) == 101


def test_claim_worker_instance_stops_live_recorded_worker_before_replacing_pid(tmp_path) -> None:
    pid_file = tmp_path / "worker.pid"
    pid_file.write_text("202\n", encoding="ascii")
    stopped: list[int] = []

    def terminate(pid: int) -> worker_instance.ProcessTermination:
        stopped.append(pid)
        return worker_instance.ProcessTermination(pid=pid, stopped=True, method="SIGTERM")

    claim = worker_instance.claim_worker_instance(
        tmp_path,
        pid=101,
        is_pid_running=lambda pid: pid == 202,
        command_for_pid=lambda pid: f"{tmp_path}/venv/bin/a2a-agent run",
        terminate=terminate,
    )

    assert stopped == [202]
    assert claim.warnings == ()
    assert worker_instance.read_worker_pid(pid_file) == 101


def test_claim_worker_instance_continues_when_old_worker_cleanup_fails(tmp_path) -> None:
    pid_file = tmp_path / "worker.pid"
    pid_file.write_text("202\n", encoding="ascii")

    claim = worker_instance.claim_worker_instance(
        tmp_path,
        pid=101,
        is_pid_running=lambda pid: pid == 202,
        command_for_pid=lambda pid: f"{tmp_path}/venv/bin/a2a-agent run",
        terminate=lambda pid: worker_instance.ProcessTermination(
            pid=pid,
            stopped=False,
            method="SIGTERM",
            error="permission denied",
        ),
    )

    assert worker_instance.read_worker_pid(pid_file) == 101
    assert worker_instance.read_worker_pid(
        worker_instance.stale_worker_pid_path(tmp_path)
    ) == 202
    assert any("cleanup failed" in warning for warning in claim.warnings)


def test_claim_worker_instance_does_not_stop_unconfirmed_run_command(tmp_path) -> None:
    pid_file = tmp_path / "worker.pid"
    pid_file.write_text("202\n", encoding="ascii")

    claim = worker_instance.claim_worker_instance(
        tmp_path,
        pid=101,
        is_pid_running=lambda pid: pid == 202,
        command_for_pid=lambda pid: "python unrelated_service.py",
        terminate=lambda pid: (_ for _ in ()).throw(
            AssertionError("unconfirmed process must not be stopped")
        ),
    )

    assert worker_instance.read_worker_pid(pid_file) == 101
    assert any("unconfirmed" in warning for warning in claim.warnings)


def test_release_worker_instance_removes_only_its_own_pid_file(tmp_path) -> None:
    pid_file = tmp_path / "worker.pid"
    pid_file.write_text("101\n", encoding="ascii")
    claim = worker_instance.WorkerInstanceClaim(pid_file, pid=101, enabled=True)

    worker_instance.release_worker_instance(claim)
    assert not pid_file.exists()

    pid_file.write_text("202\n", encoding="ascii")
    worker_instance.release_worker_instance(claim)
    assert worker_instance.read_worker_pid(pid_file) == 202


def test_claim_worker_instance_skips_pid_file_when_disabled(tmp_path) -> None:
    claim = worker_instance.claim_worker_instance(
        tmp_path,
        pid=101,
        environ={"A2A_SINGLE_INSTANCE": "0"},
    )

    assert claim.enabled is False
    assert not (tmp_path / "worker.pid").exists()


def test_terminate_process_uses_sigterm_then_sigkill_when_needed() -> None:
    signals: list[int] = []
    alive = {"value": True}

    def send_signal(pid: int, signum: int) -> None:
        signals.append(signum)
        if signum == signal.SIGKILL:
            alive["value"] = False

    result = worker_instance.terminate_process(
        202,
        platform_name="posix",
        is_pid_running=lambda pid: alive["value"],
        send_signal=send_signal,
        sleep=lambda seconds: None,
        grace_seconds=0,
    )

    assert signals == [signal.SIGTERM, signal.SIGKILL]
    assert result.stopped is True
    assert result.method == "SIGTERM+SIGKILL"


def test_terminate_process_uses_taskkill_on_windows() -> None:
    calls: list[list[str]] = []
    alive = {"value": True}

    def run_command(args: list[str]) -> subprocess.CompletedProcess[str]:
        calls.append(args)
        alive["value"] = False
        return subprocess.CompletedProcess(args=args, returncode=0, stdout="", stderr="")

    result = worker_instance.terminate_process(
        202,
        platform_name="nt",
        is_pid_running=lambda pid: alive["value"],
        run_command=run_command,
    )

    assert calls == [["taskkill", "/PID", "202", "/F"]]
    assert result.stopped is True
    assert result.method == "taskkill"


def test_cleanup_stale_workers_filters_self_and_non_worker_processes(tmp_path) -> None:
    initial = [
        worker_instance.WorkerProcess(101, "a2a-agent run"),
        worker_instance.WorkerProcess(
            202,
            f"A2A_HOME={tmp_path} python -m a2a_agent.cli run",
        ),
        worker_instance.WorkerProcess(303, "python service.py"),
        worker_instance.WorkerProcess(404, "A2A_HOME=/other a2a-agent run"),
    ]
    remaining = [initial[0]]
    snapshots = iter([initial, remaining])
    stopped: list[int] = []

    cleanup = worker_instance.cleanup_stale_workers(
        tmp_path,
        current_pid=101,
        list_processes=lambda: next(snapshots),
        terminate=lambda pid: (
            stopped.append(pid)
            or worker_instance.ProcessTermination(pid=pid, stopped=True, method="SIGTERM")
        ),
    )

    assert stopped == [202]
    assert cleanup.remaining_workers == 1
    assert cleanup.skipped_pids == (404,)
    assert "terminated pids: 202 (SIGTERM)" in cleanup.report()


def test_cleanup_stale_workers_non_owner_reports_without_stopping_pid_file_owner(tmp_path) -> None:
    (tmp_path / "worker.pid").write_text("202\n", encoding="ascii")
    stopped: list[int] = []

    cleanup = worker_instance.cleanup_stale_workers(
        tmp_path,
        current_pid=101,
        list_processes=lambda: None,
        command_for_pid=lambda pid: "python unrelated_service.py",
        terminate=lambda pid: (
            stopped.append(pid)
            or worker_instance.ProcessTermination(pid=pid, stopped=True, method="SIGTERM")
        ),
    )

    assert stopped == []
    assert cleanup.method == "pid-file-owner"
    assert cleanup.skipped_pids == ()
    assert cleanup.remaining_workers is None
    assert "worker.pid is owned by pid=202" in cleanup.report()


def test_cleanup_stale_workers_uses_recorded_previous_pid_when_listing_is_unavailable(
    tmp_path,
) -> None:
    (tmp_path / "worker.pid").write_text("101\n", encoding="ascii")
    stale_path = worker_instance.stale_worker_pid_path(tmp_path)
    stale_path.write_text("202\n", encoding="ascii")
    stopped: list[int] = []

    cleanup = worker_instance.cleanup_stale_workers(
        tmp_path,
        current_pid=101,
        list_processes=lambda: None,
        command_for_pid=lambda pid: "a2a-agent run",
        terminate=lambda pid: (
            stopped.append(pid)
            or worker_instance.ProcessTermination(pid=pid, stopped=True, method="taskkill")
        ),
    )

    assert stopped == [202]
    assert cleanup.method == "pid-file"
    assert cleanup.remaining_workers is None
    assert not stale_path.exists()


def test_cleanup_stale_workers_only_pid_owner_can_terminate_duplicates(tmp_path) -> None:
    (tmp_path / "worker.pid").write_text("202\n", encoding="ascii")
    stopped: list[int] = []
    processes = [
        worker_instance.WorkerProcess(101, "a2a-agent run"),
        worker_instance.WorkerProcess(202, "a2a-agent run"),
    ]

    cleanup = worker_instance.cleanup_stale_workers(
        tmp_path,
        current_pid=101,
        list_processes=lambda: processes,
        terminate=lambda pid: (
            stopped.append(pid)
            or worker_instance.ProcessTermination(pid=pid, stopped=True, method="SIGTERM")
        ),
    )

    assert stopped == []
    assert cleanup.method == "pid-file-owner"
    assert cleanup.remaining_workers == 2
    assert "worker.pid is owned by pid=202" in cleanup.report()
