"""SpawnAgent: the native replacement for the CLI's built-in Agent tool. The child is a
fresh session inheriting the parent's model/cwd/dashboard; sync waits and returns the last
assistant text, background returns immediately. The builtin stays blocked so the model only
ever sees the two-arg schema."""
import asyncio
import json
import subprocess
import sys
from typing import Dict, List

from pytest import MonkeyPatch, raises

from backend.apps.agents.agent_manager import agent_manager
from backend.apps.agents.core.models import AgentSession, Message


def seed_parent() -> AgentSession:
    parent = AgentSession(name="parent", model="opus-4-8", cwd="/tmp/pw", dashboard_id="dashX")
    agent_manager.sessions[parent.id] = parent
    return parent


def test_spawn_agent_sync_returns_child_answer(monkeypatch: MonkeyPatch) -> None:
    parent = seed_parent()

    async def fake_loop(session_id: str, prompt: str, **kwargs: object) -> None:
        s = agent_manager.sessions[session_id]
        s.messages.append(Message(role="assistant", content="child says done", branch_id=s.active_branch_id))
        s.status = "completed"

    monkeypatch.setattr(agent_manager, "run_agent_loop", fake_loop)
    result = asyncio.run(agent_manager.spawn_agent(prompt="do the thing", parent_session_id=parent.id))

    child = agent_manager.sessions[result["session_id"]]
    assert result["response"] == "child says done"
    assert child.mode == "sub-agent"
    assert child.parent_session_id == parent.id
    assert child.model == parent.model
    assert child.cwd == parent.cwd
    assert child.dashboard_id == "dashX"
    assert child.messages[0].role == "user" and child.messages[0].content == "do the thing"


def test_spawn_agent_background_returns_immediately(monkeypatch: MonkeyPatch) -> None:
    parent = seed_parent()
    started: List[str] = []

    async def slow_loop(session_id: str, prompt: str, **kwargs: object) -> None:
        started.append(session_id)
        await asyncio.sleep(30)

    monkeypatch.setattr(agent_manager, "run_agent_loop", slow_loop)

    async def run() -> Dict:
        result = await asyncio.wait_for(
            agent_manager.spawn_agent(prompt="long task", parent_session_id=parent.id, run_in_background=True),
            timeout=2.0,
        )
        await asyncio.sleep(0.05)
        agent_manager.tasks[result["session_id"]].cancel()
        return result

    result = asyncio.run(run())
    assert result["background"] is True
    assert started == [result["session_id"]]


def test_spawn_agent_unknown_parent_raises() -> None:
    with raises(ValueError):
        asyncio.run(agent_manager.spawn_agent(prompt="x", parent_session_id="nope-" + "0" * 28))


def test_builtin_subagent_tools_stay_blocked() -> None:
    # The CLI's built-in sub-agent tool (Task on 2.1.122, Agent on older builds) must not be offered: out of the catalog AND hard-blocked at the SDK layer.
    from backend.apps.agents.manager.prompt.tool_catalog import FULL_TOOLS
    from backend.apps.agents.manager.run.run_options_helpers import merge_hard_blocked_tools
    assert "Agent" not in FULL_TOOLS
    assert "Task" not in FULL_TOOLS
    blocked = merge_hard_blocked_tools([])
    assert "Agent" in blocked and "Task" in blocked and "mcp__claude_ai_*" in blocked


def test_effective_disallowed_reaches_the_sdk_deny_list() -> None:
    # Regression: a plain assignment once DISCARDED the computed denies (Cron*/Skill/web-swap/per-tool MCP), leaving the runtime gate as the only wall. The merge must keep them AND append the hard blocks, deduped.
    from backend.apps.agents.manager.run.run_options_helpers import merge_hard_blocked_tools
    computed = ["CronCreate", "CronList", "CronDelete", "Skill", "mcp__gmail__DeleteEmail", "Task"]
    merged = merge_hard_blocked_tools(computed)
    for t in computed:
        assert t in merged
    assert "Agent" in merged and "mcp__claude_ai_*" in merged
    assert merged.count("Task") == 1
    assert merged[:len(computed)] == computed


def test_spawn_server_schema_is_prompt_plus_background_only() -> None:
    from backend.apps.agents import spawn_agent_mcp_server as srv
    tool = srv.TOOLS[0]
    assert tool["name"] == "SpawnAgent"
    assert set(tool["inputSchema"]["properties"].keys()) == {"prompt", "run_in_background"}
    assert tool["inputSchema"]["required"] == ["prompt"]


def test_spawn_server_speaks_mcp_stdio() -> None:
    from backend.apps.agents import spawn_agent_mcp_server as srv
    proc = subprocess.Popen(
        [sys.executable, srv.__file__],
        stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True,
    )
    try:
        msgs = [
            {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}},
            {"jsonrpc": "2.0", "method": "notifications/initialized"},
            {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}},
        ]
        out, _ = proc.communicate("\n".join(json.dumps(m) for m in msgs) + "\n", timeout=15)
        lines = [json.loads(line) for line in out.strip().splitlines()]
        assert lines[0]["result"]["serverInfo"]["name"] == "openswarm-spawn-agent"
        assert lines[1]["result"]["tools"][0]["name"] == "SpawnAgent"
    finally:
        proc.kill()
