#!/usr/bin/env python
"""
Tests for MCP server core functionality.

Tests server initialization, tool validation, and basic operations.
"""

import json
import sys
import pytest
import yaml
from pathlib import Path
from cicada.mcp_server import CicadaServer


class TestServerInitialization:
    """Test CicadaServer initialization and configuration."""

    def test_init_config_not_found(self, tmp_path):
        """Should raise FileNotFoundError when config doesn't exist"""
        config_path = tmp_path / "nonexistent.yaml"
        with pytest.raises(FileNotFoundError, match="Config file not found"):
            CicadaServer(str(config_path))

    def test_init_index_not_found(self, tmp_path):
        """Should raise FileNotFoundError when index doesn't exist"""
        config = {
            "repository": {"path": str(tmp_path)},
            "storage": {"index_path": str(tmp_path / "nonexistent.json")},
        }
        config_path = tmp_path / "config.yaml"
        with open(config_path, "w") as f:
            yaml.dump(config, f)

        with pytest.raises(FileNotFoundError, match="Index file not found"):
            CicadaServer(str(config_path))

    @pytest.mark.parametrize("pr_index_content", [None, "{invalid json"])
    def test_pr_index_graceful_handling(self, pr_index_content, tmp_path):
        """Should handle missing or malformed PR index gracefully"""
        index = {"modules": {}, "metadata": {"total_modules": 0}}
        index_path = tmp_path / "index.json"
        with open(index_path, "w") as f:
            json.dump(index, f)

        if pr_index_content is not None:
            pr_index_path = tmp_path / ".cicada" / "pr_index.json"
            pr_index_path.parent.mkdir(parents=True, exist_ok=True)
            pr_index_path.write_text(pr_index_content)

        config = {
            "repository": {"path": str(tmp_path)},
            "storage": {"index_path": str(index_path)},
        }
        config_path = tmp_path / "config.yaml"
        with open(config_path, "w") as f:
            yaml.dump(config, f)

        server = CicadaServer(str(config_path))
        assert server.pr_index is None


class TestToolInputValidation:
    """Test input validation for all MCP tools."""

    @pytest.fixture
    def test_server(self, tmp_path):
        """Create a test server instance"""
        index = {
            "modules": {
                "TestModule": {
                    "file": "lib/test.ex",
                    "line": 1,
                    "functions": [],
                    "public_functions": 0,
                    "private_functions": 0,
                }
            },
            "metadata": {"total_modules": 1, "repo_path": str(tmp_path)},
        }
        index_path = tmp_path / "index.json"
        with open(index_path, "w") as f:
            json.dump(index, f)

        config = {
            "repository": {"path": str(tmp_path)},
            "storage": {"index_path": str(index_path)},
        }
        config_path = tmp_path / "config.yaml"
        with open(config_path, "w") as f:
            yaml.dump(config, f)

        return CicadaServer(str(config_path))

    @pytest.mark.asyncio
    @pytest.mark.parametrize(
        "tool,args,expected_error",
        [
            (
                "search_module",
                {},
                "Either 'module_name' or 'file_path' must be provided",
            ),
            ("search_function", {}, "'function_name' is required"),
            ("search_module_usage", {}, "'module_name' is required"),
            ("find_pr_for_line", {"line_number": 42}, "'file_path' is required"),
            ("find_pr_for_line", {"file_path": "test.ex"}, "'line_number' is required"),
            ("get_commit_history", {}, "'file_path' is required"),
            ("get_blame", {"start_line": 1, "end_line": 10}, "'file_path' is required"),
            ("get_blame", {"file_path": "test.ex"}, "start_line"),
            ("get_file_pr_history", {}, "'file_path' is required"),
        ],
    )
    async def test_tool_validation(self, tool, args, expected_error, test_server):
        """Should validate required parameters for all tools"""
        result = await test_server.call_tool(tool, args)
        assert len(result) == 1
        assert expected_error in result[0].text

    @pytest.mark.asyncio
    async def test_precise_tracking_requires_lines(self, test_server):
        """Should require line range when precise_tracking is enabled"""
        result = await test_server.call_tool(
            "get_commit_history", {"file_path": "test.ex", "precise_tracking": True}
        )
        assert len(result) == 1
        assert "start_line" in result[0].text and "end_line" in result[0].text


class TestResolveFileToModule:
    """Test _resolve_file_to_module functionality."""

    @pytest.fixture
    def test_server(self, tmp_path):
        """Create a test server with sample modules"""
        index = {
            "modules": {
                "MyApp.User": {
                    "file": "lib/my_app/user.ex",
                    "line": 1,
                    "functions": [],
                    "public_functions": 0,
                    "private_functions": 0,
                },
                "MyApp.Account": {
                    "file": "lib/my_app/account.ex",
                    "line": 1,
                    "functions": [],
                    "public_functions": 0,
                    "private_functions": 0,
                },
            },
            "metadata": {"total_modules": 2},
        }
        index_path = tmp_path / "index.json"
        with open(index_path, "w") as f:
            json.dump(index, f)

        config = {
            "repository": {"path": str(tmp_path)},
            "storage": {"index_path": str(index_path)},
        }
        config_path = tmp_path / "config.yaml"
        with open(config_path, "w") as f:
            yaml.dump(config, f)

        return CicadaServer(str(config_path))

    @pytest.mark.parametrize(
        "file_path,expected",
        [
            ("lib/my_app/user.ex", "MyApp.User"),  # Exact match
            ("/some/path/lib/my_app/user.ex", "MyApp.User"),  # Absolute path
            ("my_app/user.ex", "MyApp.User"),  # Partial path
            ("nonexistent.ex", None),  # No match
        ],
    )
    def test_resolve_file_to_module(self, file_path, expected, test_server):
        """Should resolve file paths to module names"""
        result = test_server._resolve_file_to_module(file_path)
        assert result == expected


class TestSearchModuleUsage:
    """Test _search_module_usage functionality."""

    @pytest.fixture
    def test_server(self, tmp_path):
        """Create a test server with module usage data"""
        index = {
            "modules": {
                "MyApp.User": {
                    "file": "lib/my_app/user.ex",
                    "line": 1,
                    "functions": [],
                    "public_functions": 0,
                    "private_functions": 0,
                    "aliases": {},
                    "imports": [],
                    "requires": [],
                    "uses": [],
                    "calls": [],
                },
                "MyApp.Account": {
                    "file": "lib/my_app/account.ex",
                    "line": 1,
                    "functions": [],
                    "public_functions": 0,
                    "private_functions": 0,
                    "aliases": {"User": "MyApp.User"},
                    "imports": ["MyApp.User"],
                    "requires": [],
                    "uses": [],
                    "calls": [
                        {"module": "User", "function": "create", "arity": 1, "line": 10}
                    ],
                },
            },
            "metadata": {"total_modules": 2},
        }
        index_path = tmp_path / "index.json"
        with open(index_path, "w") as f:
            json.dump(index, f)

        config = {
            "repository": {"path": str(tmp_path)},
            "storage": {"index_path": str(index_path)},
        }
        config_path = tmp_path / "config.yaml"
        with open(config_path, "w") as f:
            yaml.dump(config, f)

        return CicadaServer(str(config_path))

    @pytest.mark.asyncio
    async def test_module_not_found(self, test_server):
        """Should return error when module doesn't exist"""
        result = await test_server._search_module_usage(
            "NonExistent.Module", "markdown"
        )
        assert len(result) == 1
        assert "not found in index" in result[0].text

    @pytest.mark.asyncio
    async def test_finds_usage_patterns(self, test_server):
        """Should find aliases, imports, and function calls"""
        result = await test_server._search_module_usage("MyApp.User", "markdown")
        assert len(result) == 1
        text = result[0].text
        assert "MyApp.Account" in text
        assert "alias" in text.lower() or "Aliases" in text
        assert "import" in text.lower() or "Imports" in text
        assert "create/1" in text


class TestListTools:
    """Test list_tools functionality."""

    @pytest.fixture
    def test_server(self, tmp_path):
        """Create a test server instance"""
        index = {"modules": {}, "metadata": {"total_modules": 0}}
        index_path = tmp_path / "index.json"
        with open(index_path, "w") as f:
            json.dump(index, f)

        config = {
            "repository": {"path": str(tmp_path)},
            "storage": {"index_path": str(index_path)},
        }
        config_path = tmp_path / "config.yaml"
        with open(config_path, "w") as f:
            yaml.dump(config, f)

        return CicadaServer(str(config_path))

    @pytest.mark.asyncio
    async def test_returns_all_expected_tools(self, test_server):
        """Should return all expected tools"""
        tools = await test_server.list_tools()
        tool_names = [tool.name for tool in tools]

        expected_tools = [
            "search_module",
            "search_function",
            "search_module_usage",
            "find_pr_for_line",
            "get_commit_history",
            "get_blame",
            "get_file_pr_history",
        ]

        for expected in expected_tools:
            assert expected in tool_names, f"Tool '{expected}' not found"

    @pytest.mark.asyncio
    async def test_tools_have_descriptions_and_schemas(self, test_server):
        """Should have descriptions and input schemas for all tools"""
        tools = await test_server.list_tools()

        for tool in tools:
            assert tool.description, f"Tool '{tool.name}' missing description"
            assert (
                len(tool.description) > 10
            ), f"Tool '{tool.name}' has short description"
            assert tool.inputSchema, f"Tool '{tool.name}' missing inputSchema"
            assert (
                "properties" in tool.inputSchema
            ), f"Tool '{tool.name}' schema missing properties"


class TestUnknownTool:
    """Test handling of unknown tool names."""

    @pytest.fixture
    def test_server(self, tmp_path):
        """Create a test server instance"""
        index = {"modules": {}, "metadata": {"total_modules": 0}}
        index_path = tmp_path / "index.json"
        with open(index_path, "w") as f:
            json.dump(index, f)

        config = {
            "repository": {"path": str(tmp_path)},
            "storage": {"index_path": str(index_path)},
        }
        config_path = tmp_path / "config.yaml"
        with open(config_path, "w") as f:
            yaml.dump(config, f)

        return CicadaServer(str(config_path))

    @pytest.mark.asyncio
    async def test_raises_error(self, test_server):
        """Should raise ValueError for unknown tools"""
        with pytest.raises(ValueError, match="Unknown tool"):
            await test_server.call_tool("nonexistent_tool", {})


class TestAddCodeExamples:
    """Test _add_code_examples functionality."""

    @pytest.fixture
    def test_server_with_files(self, tmp_path):
        """Create a test server with actual code files"""
        code_file = tmp_path / "lib" / "test.ex"
        code_file.parent.mkdir(parents=True, exist_ok=True)
        code_file.write_text(
            "defmodule Test do\n"
            "  def function1 do\n"
            "    call_target()\n"
            "  end\n"
            "  def function2 do\n"
            "    call_target()\n"
            "  end\n"
            "end\n"
        )

        index = {
            "modules": {},
            "metadata": {"total_modules": 0, "repo_path": str(tmp_path)},
        }
        index_path = tmp_path / "index.json"
        with open(index_path, "w") as f:
            json.dump(index, f)

        config = {
            "repository": {"path": str(tmp_path)},
            "storage": {"index_path": str(index_path)},
        }
        config_path = tmp_path / "config.yaml"
        with open(config_path, "w") as f:
            yaml.dump(config, f)

        return CicadaServer(str(config_path))

    def test_adds_code_to_call_sites(self, test_server_with_files):
        """Should add code examples to call sites"""
        call_sites = [
            {
                "file": "lib/test.ex",
                "line": 3,
                "calling_module": "Test",
                "calling_function": {"name": "function1", "arity": 0},
            }
        ]

        test_server_with_files._add_code_examples(call_sites)

        assert "code_line" in call_sites[0]
        assert "call_target()" in str(call_sites[0]["code_line"])

    def test_handles_missing_files_gracefully(self, test_server_with_files):
        """Should skip code examples for missing files"""
        call_sites = [
            {"file": "lib/nonexistent.ex", "line": 10, "calling_module": "Test"}
        ]

        test_server_with_files._add_code_examples(call_sites)

        assert "code_line" not in call_sites[0]


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
