"""Unit tests for --mcp wiring from CLI to agent creation.

Spec source: vectl step wire-cli-params.wire-mcp-servers.
"""

from __future__ import annotations

import asyncio
import json
import os
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

import pytest
from returns.result import Success

from anima.core.schemas import AgentResult, AgentState, CapabilityToken, PersonaSpec
from tests.helpers.mcp_fakes import FakeMCPServer


def test_parse_mcp_server_maps_url_and_command_types() -> None:
    """HTTP URLs map to MCPServerSSE; commands map to MCPServerStdio."""
    pytest.importorskip("pydantic_ai.mcp")

    from anima.shell.mcp_servers import parse_mcp_server

    http_server = parse_mcp_server("https://example.com/mcp")
    stdio_server = parse_mcp_server("npx @modelcontextprotocol/server-filesystem /tmp")

    assert http_server.__class__.__name__ == "MCPServerSSE"
    assert stdio_server.__class__.__name__ == "MCPServerStdio"


def test_agent_runner_main_parses_and_forwards_mcp_servers(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Detached runner parses --mcp values and forwards parsed objects."""
    from anima.shell import agent_runner as runner_module

    parsed_servers = [object(), object()]
    captured: dict[str, Any] = {}

    def fake_parse_mcp_servers(
        values: list[str] | None,
        capability_token: Any | None = None,
    ) -> list[object] | None:
        captured["parse_values"] = values
        captured["capability_token"] = capability_token
        return parsed_servers

    def fake_run_agent_sync(
        instance_id: str,
        task: str,
        persona: PersonaSpec | None = None,
        job: Any | None = None,
        completion_policy: Any | None = None,
        max_turns: int | None = None,
        model_override: str | None = None,
        base_url: str | None = None,
        mcp_servers: list[Any] | None = None,
        capability_token: Any | None = None,
        sandbox_policy: Any | None = None,
        auto_rm: bool = False,
    ) -> int:
        captured["instance_id"] = instance_id
        captured["task"] = task
        captured["base_url"] = base_url
        captured["mcp_servers"] = mcp_servers
        return 0

    monkeypatch.setattr(
        "anima.shell.mcp_servers.parse_mcp_servers", fake_parse_mcp_servers
    )
    monkeypatch.setattr("anima.shell.agent_sync.run_agent_sync", fake_run_agent_sync)
    monkeypatch.setattr(
        runner_module.sys,
        "argv",
        [
            "anima-agent-runner",
            "inst_test",
            "Do work",
            "--llm-base-url",
            "http://localhost:11434/v1",
            "--mcp",
            "https://example.com/mcp",
            "--mcp",
            "npx @modelcontextprotocol/server-filesystem /tmp",
        ],
    )

    exit_code = runner_module.main()

    assert exit_code == 0
    assert captured["parse_values"] == [
        "https://example.com/mcp",
        "npx @modelcontextprotocol/server-filesystem /tmp",
    ]
    assert captured["instance_id"] == "inst_test"
    assert captured["task"] == "Do work"
    assert captured["base_url"] == "http://localhost:11434/v1"
    assert captured["mcp_servers"] is parsed_servers
    # Token not loaded in this test (--token-file not provided)
    assert captured["capability_token"] is None


def test_agent_runner_main_reports_invalid_mcp_value_cleanly(
    monkeypatch: pytest.MonkeyPatch,
    capsys: pytest.CaptureFixture[str],
) -> None:
    """Detached runner returns error code 1 for malformed --mcp value."""
    from anima.shell import agent_runner as runner_module

    def fake_parse_mcp_servers(
        _values: list[str] | None,
        capability_token: Any | None = None,
    ) -> None:
        raise ValueError("bad mcp")

    monkeypatch.setattr(
        "anima.shell.mcp_servers.parse_mcp_servers", fake_parse_mcp_servers
    )
    monkeypatch.setattr(
        runner_module.sys,
        "argv",
        ["anima-agent-runner", "inst_test", "Do work", "--mcp", "unterminated 'quote"],
    )

    exit_code = runner_module.main()
    stderr = capsys.readouterr().err

    assert exit_code == 1
    assert "Invalid --mcp value" in stderr


def test_agent_runner_main_rejects_invalid_llm_base_url(
    monkeypatch: pytest.MonkeyPatch,
    capsys: pytest.CaptureFixture[str],
) -> None:
    """Detached runner validates --llm-base-url before launching agent loop."""
    from anima.shell import agent_runner as runner_module

    monkeypatch.setattr(
        runner_module.sys,
        "argv",
        [
            "anima-agent-runner",
            "inst_test",
            "Do work",
            "--llm-base-url",
            "not-a-url",
        ],
    )

    exit_code = runner_module.main()
    stderr = capsys.readouterr().err

    assert exit_code == 1
    assert "Invalid --llm-base-url" in stderr


def test_agent_runner_main_requires_agent_and_task(
    monkeypatch: pytest.MonkeyPatch,
    capsys: pytest.CaptureFixture[str],
) -> None:
    """Detached runner requires positional ``instance_id`` and ``task`` arguments.

    Spec source: usage contract in ``src/anima/shell/agent_runner.py``.
    """
    from anima.shell import agent_runner as runner_module

    monkeypatch.setattr(runner_module.sys, "argv", ["anima-agent-runner"])

    exit_code = runner_module.main()
    stderr = capsys.readouterr().err

    assert exit_code == 1
    assert "Usage: anima-agent-runner" in stderr


def test_agent_runner_main_loads_persona_job_and_token_files(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    """Runner loads JSON files and forwards typed specs to agent loop.

    Spec source: detached runner file loading in ``agent_runner.main``.
    """
    from anima.core.schemas import CapabilityToken, JobSpec, PersonaSpec
    from anima.shell import agent_runner as runner_module

    persona_file = tmp_path / "persona.json"
    persona_file.write_text(
        json.dumps(
            {
                "name": "worker",
                "prompt": "You are a worker.",
                "model": "gpt-4",
                "spec_version": "0.1.0",
            }
        )
    )

    job_file = tmp_path / "job.json"
    job_file.write_text(
        json.dumps(
            {
                "persona_ref": "worker@digest",
                "instance_id": "inst_123",
                "job_version": "0.1.0",
            }
        )
    )

    token_file = tmp_path / "token.json"
    token_file.write_text(
        json.dumps(
            {
                "token_id": "tok_test123",
                "tools_profile": "default",
                "issued_at": "2026-03-01T10:00:00+00:00",
                "expires_at": "2026-03-01T11:00:00+00:00",
                "signature": "sig_test123",
            }
        )
    )

    captured: dict[str, Any] = {}

    def fake_run_agent_sync(
        instance_id: str,
        task: str,
        persona: PersonaSpec | None = None,
        job: Any | None = None,
        completion_policy: Any | None = None,
        max_turns: int | None = None,
        model_override: str | None = None,
        base_url: str | None = None,
        mcp_servers: list[Any] | None = None,
        capability_token: Any | None = None,
        sandbox_policy: Any | None = None,
        auto_rm: bool = False,
    ) -> int:
        captured["instance_id"] = instance_id
        captured["task"] = task
        captured["persona"] = persona
        captured["job"] = job
        captured["capability_token"] = capability_token
        return 0

    monkeypatch.setattr("anima.shell.agent_sync.run_agent_sync", fake_run_agent_sync)
    monkeypatch.chdir(tmp_path)
    monkeypatch.setattr(
        runner_module.sys,
        "argv",
        [
            "anima-agent-runner",
            "inst_files",
            "Do work",
            "--persona-file",
            "persona.json",
            "--job-file",
            "job.json",
            "--token-file",
            "token.json",
            "--ignored",
        ],
    )

    exit_code = runner_module.main()

    assert exit_code == 0
    assert captured["instance_id"] == "inst_files"
    assert captured["task"] == "Do work"
    assert isinstance(captured["persona"], PersonaSpec)
    assert captured["persona"].id == "worker"
    assert isinstance(captured["job"], JobSpec)
    assert captured["job"].instance_id == "inst_123"
    assert isinstance(captured["capability_token"], CapabilityToken)
    assert captured["capability_token"].token_id == "tok_test123"


def test_agent_runner_main_builds_bootstrap_persona_from_spec_id(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Detached runner converts --persona-spec-id into bootstrap persona in child."""
    from anima.shell import agent_runner as runner_module

    captured: dict[str, Any] = {}

    def fake_run_agent_sync(
        instance_id: str,
        task: str,
        persona: PersonaSpec | None = None,
        job: Any | None = None,
        completion_policy: Any | None = None,
        max_turns: int | None = None,
        model_override: str | None = None,
        base_url: str | None = None,
        mcp_servers: list[Any] | None = None,
        capability_token: Any | None = None,
        sandbox_policy: Any | None = None,
        auto_rm: bool = False,
    ) -> int:
        captured["instance_id"] = instance_id
        captured["task"] = task
        captured["persona"] = persona
        _ = (
            job,
            completion_policy,
            max_turns,
            model_override,
            base_url,
            mcp_servers,
            capability_token,
            sandbox_policy,
            auto_rm,
        )
        return 0

    monkeypatch.setattr("anima.shell.agent_sync.run_agent_sync", fake_run_agent_sync)
    monkeypatch.setattr(
        runner_module.sys,
        "argv",
        [
            "anima-agent-runner",
            "inst_spec_id",
            "Do work",
            "--persona-spec-id",
            "python-senior",
            "--model",
            "test-model",
        ],
    )

    exit_code = runner_module.main()

    assert exit_code == 0
    assert isinstance(captured["persona"], PersonaSpec)
    assert captured["persona"].id == "bootstrap-python-senior"


def test_agent_runner_main_reports_persona_load_errors(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    """Runner returns error code 1 on invalid persona JSON file."""
    from anima.shell import agent_runner as runner_module

    broken_persona = tmp_path / "persona.json"
    broken_persona.write_text("{not valid json")

    monkeypatch.setattr(
        runner_module.sys,
        "argv",
        [
            "anima-agent-runner",
            "inst_persona_fail",
            "Do work",
            "--persona-file",
            str(broken_persona),
        ],
    )

    exit_code = runner_module.main()
    stderr = capsys.readouterr().err

    assert exit_code == 1
    assert "Error loading persona" in stderr


def test_agent_runner_main_reports_job_load_errors(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    """Runner returns error code 1 on invalid job JSON payload."""
    from anima.shell import agent_runner as runner_module

    broken_job = tmp_path / "job.json"
    broken_job.write_text(json.dumps({"instance_id": "inst_missing_persona_ref"}))

    monkeypatch.setattr(
        runner_module.sys,
        "argv",
        [
            "anima-agent-runner",
            "inst_job_fail",
            "Do work",
            "--job-file",
            str(broken_job),
        ],
    )

    exit_code = runner_module.main()
    stderr = capsys.readouterr().err

    assert exit_code == 1
    assert "Error loading job" in stderr


def test_agent_runner_main_reports_token_load_errors(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    """Runner returns error code 1 on invalid capability token payload."""
    from anima.shell import agent_runner as runner_module

    broken_token = tmp_path / "token.json"
    broken_token.write_text(
        json.dumps(
            {
                "token_id": "not_prefixed",
                "tools_profile": "default",
                "issued_at": "2026-03-01T10:00:00+00:00",
                "expires_at": "2026-03-01T11:00:00+00:00",
                "signature": "sig_test123",
            }
        )
    )

    monkeypatch.setattr(
        runner_module.sys,
        "argv",
        [
            "anima-agent-runner",
            "inst_token_fail",
            "Do work",
            "--token-file",
            str(broken_token),
        ],
    )

    exit_code = runner_module.main()
    stderr = capsys.readouterr().err

    assert exit_code == 1
    assert "Error loading token" in stderr


def test_agent_runner_run_forwards_mcp_servers_to_create_agent(
    monkeypatch: pytest.MonkeyPatch,
    sample_agent_state: Any,
    tmp_state_dir: Any,
) -> None:
    """AgentRunner passes parsed MCP servers into create_agent."""
    from anima.shell import agent as agent_module

    _ = tmp_state_dir

    runner_mcp_servers = [object(), object()]
    runner = agent_module.AgentRunner(
        instance_id="inst_test",
        task="test task",
        persona=PersonaSpec(
            id="test",
            description="A test persona.",
            prompt="You are a test agent.",
            model="test-model",
        ),
        mcp_servers=runner_mcp_servers,
    )
    monkeypatch.setattr(runner, "_setup_signal_handlers", lambda: None)

    state = sample_agent_state.model_copy(
        update={
            "instance_id": "inst_mcp_wiring",
            "trace_id": "tr_mcp_wiring",
        }
    )
    monkeypatch.setattr(agent_module, "read_state", lambda _: Success(state))
    monkeypatch.setattr(agent_module, "write_agent_pid", lambda _a, _b: Success(None))
    monkeypatch.setattr(
        agent_module,
        "transition_state",
        lambda _a, _b, error=None, result_ref=None: Success(state),
    )
    monkeypatch.setattr(
        agent_module, "update_agent_state", lambda _a, _b: Success(state)
    )

    captured: dict[str, Any] = {}

    async def fake_create_agent(
        persona: PersonaSpec,
        mcp_servers: list[Any] | None = None,
        agent_runner: agent_module.AgentRunner | None = None,
        side_effect_policy: Any | None = None,
        model_override: str | None = None,
        model_instance: Any | None = None,
    ) -> Success[Any]:
        captured["persona"] = persona
        captured["mcp_servers"] = mcp_servers
        captured["agent_runner"] = agent_runner
        captured["side_effect_policy"] = side_effect_policy
        captured["model_override"] = model_override
        _ = model_instance  # ignored in test
        return Success(object())

    async def fake_run_agent_loop(
        self: agent_module.AgentRunner,
        _agent: Any,
        _state: Any,
    ) -> Success[AgentResult]:
        return Success(AgentResult(final_state=AgentState.completed))

    monkeypatch.setattr(agent_module, "create_agent", fake_create_agent)
    monkeypatch.setattr(agent_module, "AgentMCPServer", FakeMCPServer)
    monkeypatch.setattr(
        agent_module.AgentRunner, "_run_agent_loop", fake_run_agent_loop
    )

    result = asyncio.run(runner.run())

    assert isinstance(result, Success)
    assert captured["persona"].id == "test"
    # MCP servers are wrapped by _ToolNameSanitizedToolset, so check length matches
    assert len(captured["mcp_servers"]) == len(runner_mcp_servers)
    assert captured["agent_runner"] is runner


def test_agent_runner_exposes_capability_token_in_agent_context(
    monkeypatch: pytest.MonkeyPatch,
    sample_agent_state: Any,
    tmp_state_dir: Any,
) -> None:
    """AgentRunner exports token JSON to ANIMA_CAPABILITY_TOKEN_JSON."""
    from anima.shell import agent as agent_module

    _ = tmp_state_dir

    token = CapabilityToken(
        token_id="tok_context",
        tools_profile="default",
        issued_at=datetime(2026, 3, 1, 10, 0, 0, tzinfo=timezone.utc),
        expires_at=datetime(2026, 3, 1, 11, 0, 0, tzinfo=timezone.utc),
        signature="sig_context",
    )

    runner = agent_module.AgentRunner(
        instance_id="inst_test",
        task="test task",
        persona=PersonaSpec(
            id="test",
            description="A test persona.",
            prompt="You are a test agent.",
            model="test-model",
        ),
        capability_token=token,
    )
    monkeypatch.setattr(runner, "_setup_signal_handlers", lambda: None)

    state = sample_agent_state.model_copy(
        update={"instance_id": "inst_token_context", "trace_id": "tr_token_context"}
    )
    monkeypatch.setattr(agent_module, "read_state", lambda _: Success(state))
    monkeypatch.setattr(agent_module, "write_agent_pid", lambda _a, _b: Success(None))
    monkeypatch.setattr(
        agent_module,
        "transition_state",
        lambda _a, _b, error=None, result_ref=None: Success(state),
    )
    monkeypatch.setattr(
        agent_module, "update_agent_state", lambda _a, _b: Success(state)
    )

    async def fake_create_agent(
        persona: PersonaSpec,
        mcp_servers: list[Any] | None = None,
        agent_runner: agent_module.AgentRunner | None = None,
        side_effect_policy: Any | None = None,
        model_override: str | None = None,
        model_instance: Any | None = None,
    ) -> Success[Any]:
        assert agent_runner is not None
        assert agent_runner.capability_token is not None
        assert agent_runner.capability_token.token_id == "tok_context"
        _ = model_override
        _ = model_instance  # ignored in test
        return Success(object())

    async def fake_run_agent_loop(
        self: agent_module.AgentRunner,
        _agent: Any,
        _state: Any,
    ) -> Success[AgentResult]:
        return Success(AgentResult(final_state=AgentState.completed))

    monkeypatch.setattr(agent_module, "create_agent", fake_create_agent)
    monkeypatch.setattr(agent_module, "AgentMCPServer", FakeMCPServer)
    monkeypatch.setattr(
        agent_module.AgentRunner, "_run_agent_loop", fake_run_agent_loop
    )

    previous = os.environ.get("ANIMA_CAPABILITY_TOKEN_JSON")
    try:
        result = asyncio.run(runner.run())
        assert isinstance(result, Success)
        token_json = os.environ.get("ANIMA_CAPABILITY_TOKEN_JSON")
        assert token_json is not None
        parsed = json.loads(token_json)
        assert parsed["token_id"] == "tok_context"
        assert parsed["tools_profile"] == "default"
    finally:
        if previous is None:
            os.environ.pop("ANIMA_CAPABILITY_TOKEN_JSON", None)
        else:
            os.environ["ANIMA_CAPABILITY_TOKEN_JSON"] = previous
