"""Tests validating MCP server documentation accuracy.

Ensures docs/mcp-server.md stays in sync with the actual MCP server
tool definitions, security settings, and configuration.
"""
import re
from pathlib import Path

import pytest

PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
DOCS_PATH = PROJECT_ROOT / "docs" / "mcp-server.md"
SERVER_PATH = PROJECT_ROOT / "plugin" / "src" / "greymatter_plugin" / "server.py"
HTTP_SERVER_PATH = PROJECT_ROOT / "plugin" / "src" / "greymatter_plugin" / "http_server.py"
SOLO_SERVER_PATH = PROJECT_ROOT / "solo" / "mcp_server.py"
PYPROJECT_PATH = PROJECT_ROOT / "pyproject.toml"


@pytest.fixture
def doc_content() -> str:
    """Load the MCP server documentation."""
    assert DOCS_PATH.exists(), f"Documentation file missing: {DOCS_PATH}"
    return DOCS_PATH.read_text()


@pytest.fixture
def server_source() -> str:
    """Load the MCP server source code."""
    assert SERVER_PATH.exists(), f"Server source missing: {SERVER_PATH}"
    return SERVER_PATH.read_text()


@pytest.fixture
def http_server_source() -> str:
    """Load the HTTP server source code."""
    assert HTTP_SERVER_PATH.exists(), f"HTTP server source missing: {HTTP_SERVER_PATH}"
    return HTTP_SERVER_PATH.read_text()


class TestDocumentedToolsCoverAllServerTools:
    """Every @mcp.tool() in server.py must appear in docs/mcp-server.md."""

    def test_all_tools_documented(self, doc_content: str, server_source: str):
        """Each gm_* tool defined in server.py must have a ### heading in the docs."""
        tool_pattern = re.compile(r"def (gm_\w+)\(")
        defined_tools = set(tool_pattern.findall(server_source))
        assert len(defined_tools) > 0, "No tools found in server.py"

        heading_pattern = re.compile(r"^### (gm_\w+)", re.MULTILINE)
        documented_tools = set(heading_pattern.findall(doc_content))

        missing = defined_tools - documented_tools
        assert missing == set(), (
            f"Tools defined in server.py but not documented: {sorted(missing)}"
        )

    def test_no_phantom_docs(self, doc_content: str, server_source: str):
        """Docs should not reference tools that don't exist in server.py."""
        tool_pattern = re.compile(r"def (gm_\w+)\(")
        defined_tools = set(tool_pattern.findall(server_source))

        heading_pattern = re.compile(r"^### (gm_\w+)", re.MULTILINE)
        documented_tools = set(heading_pattern.findall(doc_content))

        phantom = documented_tools - defined_tools
        assert phantom == set(), (
            f"Tools documented but not defined in server.py: {sorted(phantom)}"
        )


class TestDocumentedActions:
    """Action lists in docs must match what the server actually handles."""

    @pytest.fixture
    def action_map(self, server_source: str) -> dict[str, set[str]]:
        """Extract action -> handler mappings from server.py.

        Looks for patterns like: action == "create" within each tool function.
        """
        tool_blocks = re.split(r"# --- (gm_\w+) ---", server_source)
        result: dict[str, set[str]] = {}

        for i in range(1, len(tool_blocks), 2):
            tool_name = tool_blocks[i]
            body = tool_blocks[i + 1] if i + 1 < len(tool_blocks) else ""
            actions = set(re.findall(r'action\s*==\s*"(\w+)"', body))
            if actions:
                result[tool_name] = actions
        return result

    @pytest.fixture
    def doc_action_map(self, doc_content: str) -> dict[str, set[str]]:
        """Extract documented actions from the table rows in docs.

        Looks for | `action_name` | patterns in tool reference tables.
        """
        result: dict[str, set[str]] = {}
        current_tool = None
        for line in doc_content.splitlines():
            heading = re.match(r"^### (gm_\w+)", line)
            if heading:
                current_tool = heading.group(1)
                result[current_tool] = set()
                continue
            if current_tool:
                action_match = re.match(r"\|\s*`(\w+)`\s*\|", line)
                if action_match:
                    result[current_tool].add(action_match.group(1))
        return result

    def test_pattern_actions_match(self, action_map, doc_action_map):
        """gm_pattern documented actions must match server actions."""
        if "gm_pattern" in action_map and "gm_pattern" in doc_action_map:
            server_actions = action_map["gm_pattern"]
            doc_actions = doc_action_map["gm_pattern"]
            missing = server_actions - doc_actions
            assert missing == set(), (
                f"gm_pattern actions in server but not in docs: {sorted(missing)}"
            )

    def test_knowledge_actions_match(self, action_map, doc_action_map):
        """gm_knowledge documented actions must match server actions."""
        if "gm_knowledge" in action_map and "gm_knowledge" in doc_action_map:
            server_actions = action_map["gm_knowledge"]
            doc_actions = doc_action_map["gm_knowledge"]
            missing = server_actions - doc_actions
            assert missing == set(), (
                f"gm_knowledge actions in server but not in docs: {sorted(missing)}"
            )

    def test_observe_actions_match(self, action_map, doc_action_map):
        """gm_observe documented actions must match server actions."""
        if "gm_observe" in action_map and "gm_observe" in doc_action_map:
            server_actions = action_map["gm_observe"]
            doc_actions = doc_action_map["gm_observe"]
            missing = server_actions - doc_actions
            assert missing == set(), (
                f"gm_observe actions in server but not in docs: {sorted(missing)}"
            )

    def test_security_actions_match(self, action_map, doc_action_map):
        """gm_security documented actions must match server actions."""
        if "gm_security" in action_map and "gm_security" in doc_action_map:
            server_actions = action_map["gm_security"]
            doc_actions = doc_action_map["gm_security"]
            missing = server_actions - doc_actions
            assert missing == set(), (
                f"gm_security actions in server but not in docs: {sorted(missing)}"
            )


class TestDocVersion:
    """Documentation version must match pyproject.toml."""

    def test_version_matches_pyproject(self, doc_content: str):
        """Version stated in docs must match pyproject.toml version."""
        pyproject = PYPROJECT_PATH.read_text()
        version_match = re.search(r'^version\s*=\s*"([^"]+)"', pyproject, re.MULTILINE)
        assert version_match, "Could not find version in pyproject.toml"
        project_version = version_match.group(1)

        doc_version_match = re.search(r"\*\*Version\s+([^*]+)\*\*", doc_content)
        assert doc_version_match, "Could not find version in docs/mcp-server.md"
        doc_version = doc_version_match.group(1).strip()

        assert doc_version == project_version, (
            f"Doc version ({doc_version}) != pyproject.toml version ({project_version})"
        )

    def test_http_server_version_matches(self, http_server_source: str):
        """HTTP server agent card version must match pyproject.toml."""
        pyproject = PYPROJECT_PATH.read_text()
        version_match = re.search(r'^version\s*=\s*"([^"]+)"', pyproject, re.MULTILINE)
        assert version_match
        project_version = version_match.group(1)

        card_version = re.search(r'"version":\s*"([^"]+)"', http_server_source)
        assert card_version, "Could not find version in http_server.py AGENT_CARD"
        assert card_version.group(1) == project_version, (
            f"HTTP server AGENT_CARD version ({card_version.group(1)}) != "
            f"pyproject.toml version ({project_version})"
        )


class TestDocStructure:
    """Documentation must have required sections."""

    @pytest.mark.parametrize("section", [
        "Architecture",
        "Transport Modes",
        "Configuration",
        "Security Layer",
        "Budget System",
        "Tools Reference",
        "Extension System",
        "Harness Enrichment",
        "Hooks",
        "Solo MCP Server",
        "Running the Server",
        "Environment Variables",
    ])
    def test_required_sections_present(self, doc_content: str, section: str):
        """Each required section must exist as an H2 heading."""
        pattern = re.compile(rf"^## {re.escape(section)}", re.MULTILINE)
        assert pattern.search(doc_content), (
            f"Required section '## {section}' missing from docs/mcp-server.md"
        )

    def test_table_of_contents_entries(self, doc_content: str):
        """Table of Contents must link to existing sections."""
        toc_pattern = re.compile(r"\[([^\]]+)\]\(#([^)]+)\)")
        toc_entries = toc_pattern.findall(doc_content)
        assert len(toc_entries) > 0, "No Table of Contents entries found"

        h2_pattern = re.compile(r"^## (.+)$", re.MULTILINE)
        h2_headings = h2_pattern.findall(doc_content)
        h2_anchors = {h.lower().replace(" ", "-"): h for h in h2_headings}

        for label, anchor in toc_entries:
            assert anchor in h2_anchors, (
                f"TOC entry '{label}' links to #{anchor} but no matching H2 heading found"
            )


class TestSecurityDocAccuracy:
    """Security settings documented must match server.py constants."""

    def test_rate_limit_values(self, doc_content: str, server_source: str):
        """Rate limit values in docs must match server constants."""
        server_limit = re.search(r"_RATE_LIMIT\s*=\s*(\d+)", server_source)
        server_window = re.search(r"_RATE_WINDOW\s*=\s*([\d.]+)", server_source)
        assert server_limit and server_window

        limit_val = int(server_limit.group(1))
        window_val = int(float(server_window.group(1)))

        assert f"{limit_val} calls" in doc_content, (
            f"Rate limit {limit_val} not found in docs"
        )
        assert f"{window_val}-second" in doc_content or f"{window_val}s" in doc_content, (
            f"Rate window {window_val}s not found in docs"
        )

    def test_size_limit_values(self, doc_content: str, server_source: str):
        """Size limit values in docs must match server constants."""
        content_limit = re.search(r"_MAX_CONTENT_SIZE\s*=\s*([\d_]+)", server_source)
        title_limit = re.search(r"_MAX_TITLE_SIZE\s*=\s*([\d_]+)", server_source)
        assert content_limit and title_limit

        content_kb = int(content_limit.group(1).replace("_", "")) // 1000
        title_kb = int(title_limit.group(1).replace("_", "")) // 1000

        assert f"{content_kb}KB" in doc_content, (
            f"Content limit {content_kb}KB not found in docs"
        )
        assert f"{title_kb}KB" in doc_content, (
            f"Title limit {title_kb}KB not found in docs"
        )


class TestSoloMcpServerDocs:
    """Solo MCP server tools must be documented in docs/mcp-server.md."""

    @pytest.fixture
    def solo_source(self) -> str:
        """Load the Solo MCP server source code."""
        assert SOLO_SERVER_PATH.exists(), f"Solo server source missing: {SOLO_SERVER_PATH}"
        return SOLO_SERVER_PATH.read_text()

    def test_all_solo_tools_documented(self, doc_content: str, solo_source: str):
        """Each solo_* tool defined in solo/mcp_server.py must have a #### heading in docs."""
        tool_pattern = re.compile(r"def (solo_\w+)\(")
        defined_tools = set(tool_pattern.findall(solo_source))
        assert len(defined_tools) > 0, "No solo_* tools found in solo/mcp_server.py"

        heading_pattern = re.compile(r"^#### (solo_\w+)", re.MULTILINE)
        documented_tools = set(heading_pattern.findall(doc_content))

        missing = defined_tools - documented_tools
        assert missing == set(), (
            f"Solo tools defined but not documented: {sorted(missing)}"
        )

    def test_no_phantom_solo_docs(self, doc_content: str, solo_source: str):
        """Docs should not reference solo tools that don't exist."""
        tool_pattern = re.compile(r"def (solo_\w+)\(")
        defined_tools = set(tool_pattern.findall(solo_source))

        heading_pattern = re.compile(r"^#### (solo_\w+)", re.MULTILINE)
        documented_tools = set(heading_pattern.findall(doc_content))

        phantom = documented_tools - defined_tools
        assert phantom == set(), (
            f"Solo tools documented but not defined: {sorted(phantom)}"
        )

    def test_solo_section_exists(self, doc_content: str):
        """The Solo MCP Server section must exist."""
        assert "## Solo MCP Server" in doc_content, (
            "Missing '## Solo MCP Server' section in docs/mcp-server.md"
        )

    def test_solo_work_actions_documented(self, doc_content: str, solo_source: str):
        """solo_work actions in source must appear in documentation."""
        # Extract solo_work function body
        match = re.search(
            r"def solo_work\(.*?\n(.*?)(?=\n    mcp\._tool_functions)",
            solo_source,
            re.DOTALL,
        )
        assert match, "Could not find solo_work function in source"
        body = match.group(1)
        server_actions = set(re.findall(r'action\s*==\s*"(\w+)"', body))
        assert len(server_actions) > 0, "No actions found in solo_work"

        # Extract documented actions from the solo_work section
        solo_work_section = re.search(
            r"#### solo_work\n(.*?)(?=\n---|\n####)",
            doc_content,
            re.DOTALL,
        )
        assert solo_work_section, "Could not find solo_work section in docs"
        doc_actions = set(re.findall(r"\|\s*`(\w+)`\s*\|", solo_work_section.group(1)))

        missing = server_actions - doc_actions
        assert missing == set(), (
            f"solo_work actions in server but not in docs: {sorted(missing)}"
        )

    def test_solo_queue_actions_documented(self, doc_content: str, solo_source: str):
        """solo_queue actions in source must appear in documentation."""
        match = re.search(
            r"def solo_queue\(.*?\n(.*?)(?=\n    mcp\._tool_functions)",
            solo_source,
            re.DOTALL,
        )
        assert match, "Could not find solo_queue function in source"
        body = match.group(1)
        server_actions = set(re.findall(r'action\s*==\s*"(\w+)"', body))
        assert len(server_actions) > 0, "No actions found in solo_queue"

        solo_queue_section = re.search(
            r"#### solo_queue\n(.*?)(?=\n---|\n####)",
            doc_content,
            re.DOTALL,
        )
        assert solo_queue_section, "Could not find solo_queue section in docs"
        doc_actions = set(re.findall(r"\|\s*`(\w+)`\s*\|", solo_queue_section.group(1)))

        missing = server_actions - doc_actions
        assert missing == set(), (
            f"solo_queue actions in server but not in docs: {sorted(missing)}"
        )

    def test_solo_config_vars_documented(self, doc_content: str):
        """Key Solo configuration environment variables must be documented."""
        required_vars = ["GM_HOME", "GM_SOLO_MAX_AGENTS", "GM_SOLO_AGENT_TIMEOUT"]
        for var in required_vars:
            assert var in doc_content, (
                f"Solo config variable {var} not found in docs"
            )
