"""Spawn-path coverage for the pi transport.

``PiRpcProcess.spawn`` must launch the *resolved* executable that
``shutil.which`` returns — not the bare ``PI_BINARY`` name. On Windows
the bare name ``pi`` has no ``.exe``; npm installs the launcher as
``pi.cmd``, so ``create_subprocess_exec("pi", ...)`` fails with WinError 2
even though the existence guard found ``pi.cmd``. Launching the resolved
path is what makes agent nodes start on Windows, and it closes the
guard/launch disagreement on every platform.

CI runs on Linux (where the bare name happens to resolve), so this
regression asserts at the argv level rather than via a live spawn.
"""

from __future__ import annotations

from pathlib import Path

import pytest

from attractor.agent import pi_rpc
from attractor.agent.errors import AgentError


class _FakeStderr:
    """A stderr stream that is immediately at EOF."""

    async def read(self, _n: int) -> bytes:
        return b""


class _FakeProc:
    """Just enough of ``asyncio.subprocess.Process`` for ``__init__``/``aclose``."""

    def __init__(self) -> None:
        self.stderr = _FakeStderr()
        self.stdin = None
        self.stdout = None
        self.returncode = 0


@pytest.mark.asyncio
# [unit->REQ-LLM-SURFACE]
async def test_spawn_launches_resolved_path_not_bare_name(
    monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
    """spawn() execs the path which() resolved, not PI_BINARY itself."""
    resolved = str(tmp_path / "pi.cmd")

    def _fake_which(_name: str) -> str | None:
        return resolved

    captured: dict[str, object] = {}

    async def _fake_exec(program: str, *args: str, **kwargs: object) -> _FakeProc:
        captured["program"] = program
        captured["args"] = args
        return _FakeProc()

    monkeypatch.setattr(pi_rpc.shutil, "which", _fake_which)
    monkeypatch.setattr(pi_rpc.asyncio, "create_subprocess_exec", _fake_exec)

    proc = await pi_rpc.PiRpcProcess.spawn(["--mode", "rpc"], cwd=tmp_path)
    try:
        assert captured["program"] == resolved
        # The bare name is exactly what CreateProcess fails to find.
        assert captured["program"] != pi_rpc.PI_BINARY
        # Args pass through untouched.
        assert captured["args"] == ("--mode", "rpc")
    finally:
        await proc.aclose()


@pytest.mark.asyncio
# [unit->REQ-LLM-SURFACE]
async def test_spawn_raises_agenterror_when_pi_absent(
    monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
    """A missing pi is a setup failure surfaced as AgentError, not a crash."""

    def _fake_which(_name: str) -> str | None:
        return None

    monkeypatch.setattr(pi_rpc.shutil, "which", _fake_which)
    with pytest.raises(AgentError, match="not found on PATH"):
        await pi_rpc.PiRpcProcess.spawn(["--mode", "rpc"], cwd=tmp_path)
