import json
from types import SimpleNamespace
from typing import Any

import anyio
import pytest
from mcp.server.fastmcp import FastMCP
from mcp.shared.memory import create_connected_server_and_client_session

from cog_mcp.resources import FILTER_DOCS
from cog_mcp.tools import register_tools

EXPECTED_TOOL_NAMES = {
    "list_views",
    "get_view_schema",
    "get_filter_docs",
    "get_query_docs",
    "list_instances",
    "search_instances",
    "aggregate_instances",
    "query_instances",
    "ask_documents_question",
    "summarize_document",
    "retrieve_instances",
}

VIEW_ARGS = {"view_space": "my-space", "view_external_id": "MyView", "view_version": "v1"}


@pytest.fixture
def mcp_server(cognite_client_mock, sample_config) -> FastMCP:
    server = FastMCP("contract-test-server")
    register_tools(server, cognite_client_mock, sample_config)
    return server


def _call_tool(server: FastMCP, name: str, arguments: dict | None = None) -> Any:
    async def _run():
        async with create_connected_server_and_client_session(server) as session:
            return await session.call_tool(name, arguments or {})

    return anyio.run(_run)


def _text_content(tool_result: Any) -> str:
    assert tool_result.content, "Expected at least one content block from tool result"
    first_block = tool_result.content[0]
    text = getattr(first_block, "text", None)
    assert isinstance(text, str), f"Expected text content block, got {type(first_block).__name__}"
    return text


def test_list_tools_contract_includes_expected_tool_names(mcp_server) -> None:
    async def _run() -> set[str]:
        async with create_connected_server_and_client_session(mcp_server) as session:
            tool_list = await session.list_tools()
            return {tool.name for tool in tool_list.tools}

    assert EXPECTED_TOOL_NAMES.issubset(anyio.run(_run))


def test_get_filter_docs_contract_returns_expected_reference_text(mcp_server) -> None:
    result = _call_tool(mcp_server, "get_filter_docs")

    assert result.isError is False
    assert _text_content(result) == FILTER_DOCS


def test_list_instances_contract_round_trips_json_arguments_and_payload(
    mcp_server, cognite_client_mock, dumpable_factory
) -> None:
    cognite_client_mock.data_modeling.instances.list.return_value = [
        dumpable_factory({"externalId": "n1"})
    ]
    cognite_client_mock.data_modeling.instances.aggregate.return_value = SimpleNamespace(value=1)

    arguments = {
        **VIEW_ARGS,
        "filter": '{"equals": {"property": ["node", "space"], "value": "space-a"}}',
        "limit": 1,
        "sort": '[{"property": ["my-space", "MyView/v1", "name"], "direction": "descending"}]',
    }
    result = _call_tool(mcp_server, "list_instances", arguments)
    payload = json.loads(_text_content(result))

    assert result.isError is False
    assert payload["count"] == 1
    assert payload["total"] == 1
    assert payload["items"][0]["externalId"] == "n1"

    kwargs = cognite_client_mock.data_modeling.instances.list.call_args.kwargs
    assert kwargs["filter"]["equals"]["value"] == "space-a"
    assert kwargs["sort"][0].direction == "descending"


def test_ask_documents_question_contract_returns_validation_error(mcp_server) -> None:
    result = _call_tool(
        mcp_server,
        "ask_documents_question",
        {"question": "what is this?", "file_instance_ids": "[]"},
    )

    assert result.isError is True
    assert "non-empty list" in _text_content(result)
