"""Focused compatibility tests for ACE's generic MCP server surface."""

from __future__ import annotations

import json
import re
from unittest.mock import MagicMock, patch

import pytest

from ace.integrations.mcp.adapters import _MCP_INSTALL_HINT as ADAPTERS_INSTALL_HINT
from ace.integrations.mcp.adapters import _mcp_schema, register_tools
from ace.integrations.mcp.config import MCPServerConfig
from ace.integrations.mcp.errors import (
    ForbiddenInSafeModeError,
    SessionNotFoundError,
    ValidationError as ACEValidationError,
    map_error_to_mcp,
)
from ace.integrations.mcp.handlers import MCPHandlers
from ace.integrations.mcp.models import AskRequest, LearnSampleRequest
from ace.integrations.mcp.registry import SessionRegistry
from ace.integrations.mcp.server import _MCP_INSTALL_HINT as SERVER_INSTALL_HINT

_CLIENT_PATTERN = re.compile(
    r"(vs\s*code|vscode|visual\s*studio\s*code|cursor|windsurf)",
    re.IGNORECASE,
)


def _require_mcp():
    pytest.importorskip("mcp.server")
    pytest.importorskip("mcp.types")

    from mcp.server import Server
    from mcp.types import CallToolRequest, ListToolsRequest

    return Server, CallToolRequest, ListToolsRequest


def test_ask_request_schema_is_inlined():
    schema = _mcp_schema(AskRequest)
    schema_str = json.dumps(schema)

    assert "$ref" not in schema_str
    assert "$defs" not in schema_str
    assert "session_id" in schema.get("properties", {})
    assert "question" in schema.get("properties", {})


def test_nested_schema_is_inlined():
    schema = _mcp_schema(LearnSampleRequest)
    schema_str = json.dumps(schema)

    assert "$ref" not in schema_str
    assert "$defs" not in schema_str
    assert "samples" in schema.get("properties", {})


def test_install_hints_are_client_agnostic():
    assert not _CLIENT_PATTERN.search(SERVER_INSTALL_HINT)
    assert not _CLIENT_PATTERN.search(ADAPTERS_INSTALL_HINT)


def test_error_messages_are_client_agnostic():
    for err in (
        SessionNotFoundError("session-1"),
        ForbiddenInSafeModeError("ace.learn.sample"),
        ACEValidationError("prompt too long", details={"field": "question"}),
        RuntimeError("boom"),
    ):
        mapped = map_error_to_mcp(err)
        assert not _CLIENT_PATTERN.search(mapped["message"])


@pytest.fixture
def wired_server():
    Server, _, _ = _require_mcp()

    config = MCPServerConfig(safe_mode=False)
    registry = SessionRegistry(config)
    handlers = MCPHandlers(registry, config)
    server = Server("ace-mcp-server")
    register_tools(server, handlers)
    return server, registry


@pytest.mark.asyncio
async def test_published_tool_schemas_are_inlined(wired_server):
    server, _ = wired_server
    _, _, ListToolsRequest = _require_mcp()

    handler = server.request_handlers.get(ListToolsRequest)
    assert handler is not None

    result = await handler(MagicMock())
    for tool in result.root.tools:
        schema_str = json.dumps(tool.inputSchema)
        assert "$ref" not in schema_str
        assert "$defs" not in schema_str
        assert not _CLIENT_PATTERN.search(tool.description or "")


@pytest.mark.asyncio
async def test_call_tool_ace_ask_returns_json_payload(wired_server):
    server, _ = wired_server
    _, CallToolRequest, _ = _require_mcp()

    with patch("ace.integrations.mcp.registry.ACELiteLLM") as mock_runner_cls:
        runner = MagicMock()
        runner.ask.return_value = "The answer is 42."
        runner.skillbook.skills.return_value = []
        mock_runner_cls.from_model.return_value = runner

        handler = server.request_handlers.get(CallToolRequest)
        assert handler is not None

        req = MagicMock()
        req.params.name = "ace.ask"
        req.params.arguments = {
            "session_id": "generic-client-1",
            "question": "What is the meaning of life?",
        }

        result = await handler(req)
        assert not result.root.isError

        payload = json.loads(result.root.content[0].text)
        assert payload["answer"] == "The answer is 42."
        assert payload["session_id"] == "generic-client-1"


@pytest.mark.asyncio
async def test_call_tool_unknown_tool_returns_structured_error(wired_server):
    server, _ = wired_server
    _, CallToolRequest, _ = _require_mcp()

    handler = server.request_handlers.get(CallToolRequest)
    assert handler is not None

    req = MagicMock()
    req.params.name = "nonexistent.tool"
    req.params.arguments = {}

    result = await handler(req)
    assert result.root.isError

    payload = json.loads(result.root.content[0].text)
    assert payload["code"] == "ACE_MCP_INTERNAL_ERROR"
    assert "Unknown tool" in payload["message"]


@pytest.mark.asyncio
async def test_session_ids_are_opaque_strings():
    config = MCPServerConfig()
    registry = SessionRegistry(config)

    with patch("ace.integrations.mcp.registry.ACELiteLLM") as mock_runner_cls:
        mock_runner_cls.from_model.side_effect = lambda *a, **kw: MagicMock()

        ids = [
            "simple-id",
            "uuid-550e8400-e29b-41d4-a716-446655440000",
            "cursor/project/session-1",
            "claude-code:workspace:12345",
        ]

        sessions = [await registry.get_or_create(session_id) for session_id in ids]

        assert [session.session_id for session in sessions] == ids
        assert len({id(session.runner) for session in sessions}) == len(ids)
