"""Orchestrator API route coverage."""

from __future__ import annotations

from datetime import UTC, datetime
from http import HTTPStatus
from types import SimpleNamespace
from typing import TYPE_CHECKING
from unittest.mock import AsyncMock, MagicMock
from uuid import uuid4

import pytest
import pytest_asyncio
from fastapi import FastAPI, HTTPException
from httpx import ASGITransport, AsyncClient
from roboco.api.deps import _ServiceHolder, set_orchestrator
from roboco.api.routes.orchestrator import _validated_agent_id
from roboco.api.routes.orchestrator import router as orch_router

if TYPE_CHECKING:
    from collections.abc import AsyncIterator


@pytest_asyncio.fixture
async def orch_client() -> AsyncIterator[tuple[AsyncClient, MagicMock]]:
    app = FastAPI()
    app.include_router(orch_router, prefix="/api/orchestrator")

    orchestrator = MagicMock()
    set_orchestrator(orchestrator)

    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as client:
        yield client, orchestrator
    _ServiceHolder.orchestrator = None
    app.dependency_overrides.clear()


_HDR = {"X-Agent-ID": str(uuid4()), "X-Agent-Role": "ceo"}


@pytest.mark.parametrize(
    "bad", ["..", ".", "../x", "a/b", "a\\b", "", "be-dev-1/../x", "x\x00y"]
)
def test_validated_agent_id_rejects_path_traversal(bad: str) -> None:
    # agent_id is a request path param that flows into per-agent filesystem
    # paths; a traversal vector must be rejected at the HTTP boundary with 422.
    with pytest.raises(HTTPException) as exc:
        _validated_agent_id(bad)
    assert exc.value.status_code == HTTPStatus.UNPROCESSABLE_ENTITY


def test_validated_agent_id_accepts_real_slugs() -> None:
    for slug in ("be-dev-1", "pr-reviewer-1", "main-pm", "intake", "secretary"):
        assert _validated_agent_id(slug) == slug


# ---------------------------------------------------------------------------
# /status
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_get_status(orch_client: tuple[AsyncClient, MagicMock]) -> None:
    client, orch = orch_client
    orch.get_status_summary = MagicMock(
        return_value={
            "total": 1,
            "by_state": {"running": 1},
            "waiting_count": 0,
            "agents": [
                {
                    "agent_id": "be-dev-1",
                    "state": "running",
                    "task_id": None,
                    "error_count": 0,
                    "started_at": datetime.now(UTC).isoformat(),
                }
            ],
        }
    )
    waiting_record = SimpleNamespace(
        agent_id="be-qa-1",
        task_id=None,
        waiting_for="qa",
        waiting_since=datetime.now(UTC),
        context={},
    )
    orch.get_waiting_agents = MagicMock(return_value={"be-dev-1": waiting_record})
    response = await client.get("/api/orchestrator/status", headers=_HDR)
    assert response.status_code == HTTPStatus.OK


@pytest.mark.asyncio
async def test_get_status_no_started_at(
    orch_client: tuple[AsyncClient, MagicMock],
) -> None:
    client, orch = orch_client
    orch.get_status_summary = MagicMock(
        return_value={
            "total": 1,
            "by_state": {"running": 1},
            "waiting_count": 0,
            "agents": [
                {
                    "agent_id": "be-dev-1",
                    "state": "running",
                    "task_id": None,
                    "error_count": 0,
                    "started_at": None,
                }
            ],
        }
    )
    orch.get_waiting_agents = MagicMock(return_value={})
    response = await client.get("/api/orchestrator/status", headers=_HDR)
    assert response.status_code == HTTPStatus.OK


# ---------------------------------------------------------------------------
# /agents/{agent_id}
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_get_agent_status_not_found(
    orch_client: tuple[AsyncClient, MagicMock],
) -> None:
    client, orch = orch_client
    orch.get_instance = MagicMock(return_value=None)
    response = await client.get("/api/orchestrator/agents/be-dev-1", headers=_HDR)
    assert response.status_code == HTTPStatus.NOT_FOUND


@pytest.mark.asyncio
async def test_get_agent_status_success(
    orch_client: tuple[AsyncClient, MagicMock],
) -> None:
    client, orch = orch_client
    instance = SimpleNamespace(
        agent_id="be-dev-1",
        state=SimpleNamespace(value="running"),
        current_task_id=None,
        error_count=0,
        started_at=datetime.now(UTC),
    )
    orch.get_instance = MagicMock(return_value=instance)
    waiting_record = SimpleNamespace(
        waiting_for="qa",
        agent_id="be-dev-1",
        task_id=None,
        waiting_since=datetime.now(UTC),
        context={},
    )
    orch.get_waiting_agents = MagicMock(return_value={"be-dev-1": waiting_record})
    response = await client.get("/api/orchestrator/agents/be-dev-1", headers=_HDR)
    assert response.status_code == HTTPStatus.OK


@pytest.mark.asyncio
async def test_get_agent_status_not_waiting(
    orch_client: tuple[AsyncClient, MagicMock],
) -> None:
    client, orch = orch_client
    instance = SimpleNamespace(
        agent_id="be-dev-1",
        state=SimpleNamespace(value="running"),
        current_task_id=None,
        error_count=0,
        started_at=datetime.now(UTC),
    )
    orch.get_instance = MagicMock(return_value=instance)
    orch.get_waiting_agents = MagicMock(return_value={})
    response = await client.get("/api/orchestrator/agents/be-dev-1", headers=_HDR)
    assert response.status_code == HTTPStatus.OK


# ---------------------------------------------------------------------------
# /waiting
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_get_waiting_agents(
    orch_client: tuple[AsyncClient, MagicMock],
) -> None:
    client, orch = orch_client
    record = SimpleNamespace(
        agent_id="be-dev-1",
        task_id="t1",
        waiting_for="qa",
        waiting_since=datetime.now(UTC),
        context={},
    )
    orch.get_waiting_agents = MagicMock(return_value={"be-dev-1": record})
    response = await client.get("/api/orchestrator/waiting", headers=_HDR)
    assert response.status_code == HTTPStatus.OK


# ---------------------------------------------------------------------------
# /agents/{agent_id}/spawn
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_spawn_agent_not_found(
    orch_client: tuple[AsyncClient, MagicMock],
) -> None:
    client, orch = orch_client
    orch.spawn_agent = AsyncMock(side_effect=FileNotFoundError("missing"))
    response = await client.post(
        "/api/orchestrator/agents/be-dev-1/spawn", headers=_HDR
    )
    assert response.status_code == HTTPStatus.NOT_FOUND


@pytest.mark.asyncio
async def test_spawn_agent_internal_error(
    orch_client: tuple[AsyncClient, MagicMock],
) -> None:
    client, orch = orch_client
    orch.spawn_agent = AsyncMock(side_effect=RuntimeError("boom"))
    response = await client.post(
        "/api/orchestrator/agents/be-dev-1/spawn",
        headers=_HDR,
    )
    assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR


@pytest.mark.asyncio
async def test_spawn_agent_success(
    orch_client: tuple[AsyncClient, MagicMock],
) -> None:
    client, orch = orch_client
    instance = SimpleNamespace(
        agent_id="be-dev-1",
        state=SimpleNamespace(value="starting"),
        current_task_id="t1",
        error_count=0,
        started_at=datetime.now(UTC),
    )
    orch.spawn_agent = AsyncMock(return_value=instance)
    response = await client.post(
        "/api/orchestrator/agents/be-dev-1/spawn",
        json={
            "agent_id": "be-dev-1",
            "initial_prompt": "go",
            "task_id": "t1",
            "model": "claude-opus",
        },
        headers=_HDR,
    )
    assert response.status_code == HTTPStatus.CREATED


# ---------------------------------------------------------------------------
# /agents/{agent_id}/stop
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_stop_agent(
    orch_client: tuple[AsyncClient, MagicMock],
) -> None:
    client, orch = orch_client
    orch.stop_agent = AsyncMock(return_value=None)
    response = await client.post("/api/orchestrator/agents/be-dev-1/stop", headers=_HDR)
    assert response.status_code == HTTPStatus.NO_CONTENT


# ---------------------------------------------------------------------------
# /agents/{agent_id}/resolve-wait
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_resolve_wait_not_found(
    orch_client: tuple[AsyncClient, MagicMock],
) -> None:
    client, orch = orch_client
    orch.resolve_wait = AsyncMock(return_value=None)
    response = await client.post(
        "/api/orchestrator/agents/be-dev-1/resolve-wait",
        json={"resolution": {"action": "fixed"}},
        headers=_HDR,
    )
    assert response.status_code == HTTPStatus.NOT_FOUND


@pytest.mark.asyncio
async def test_resolve_wait_success(
    orch_client: tuple[AsyncClient, MagicMock],
) -> None:
    client, orch = orch_client
    instance = SimpleNamespace(
        agent_id="be-dev-1",
        state=SimpleNamespace(value="running"),
        current_task_id=None,
        error_count=0,
        started_at=datetime.now(UTC),
    )
    orch.resolve_wait = AsyncMock(return_value=instance)
    response = await client.post(
        "/api/orchestrator/agents/be-dev-1/resolve-wait",
        json={"resolution": {"action": "ok"}},
        headers=_HDR,
    )
    assert response.status_code == HTTPStatus.OK


# ---------------------------------------------------------------------------
# /agents/{agent_id}/mark-waiting
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_mark_waiting(
    orch_client: tuple[AsyncClient, MagicMock],
) -> None:
    client, orch = orch_client
    orch.mark_waiting_long = AsyncMock(return_value=None)
    response = await client.post(
        "/api/orchestrator/agents/be-dev-1/mark-waiting?waiting_for=qa&task_id=t1",
        headers=_HDR,
    )
    assert response.status_code == HTTPStatus.NO_CONTENT


@pytest.mark.asyncio
async def test_orchestrator_not_initialized() -> None:
    """When orchestrator isn't set, get_orchestrator raises 503."""
    app = FastAPI()
    app.include_router(orch_router, prefix="/api/orchestrator")
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as client:
        response = await client.get("/api/orchestrator/status", headers=_HDR)
    assert response.status_code == HTTPStatus.SERVICE_UNAVAILABLE
