"""
MCP Tools Unit Tests
====================

MCPツールの単体テスト。
"""

from pathlib import Path
from unittest.mock import AsyncMock, MagicMock

import pytest

from iflow_mcp_nahisaho_codegraph_mcp_server.config import Config
from iflow_mcp_nahisaho_codegraph_mcp_server.core.parser import Entity, EntityType, Location


# Fixtures

@pytest.fixture
def temp_repo(tmp_path: Path) -> Path:
    """Create a temporary repository with test files."""
    (tmp_path / "main.py").write_text("""
def main():
    helper()

def helper():
    pass
""")
    return tmp_path


@pytest.fixture
def config(temp_repo: Path) -> Config:
    """Create a config with temp repo path."""
    return Config(repo_path=temp_repo)


@pytest.fixture
def mock_entity() -> Entity:
    """Create a mock entity."""
    return Entity(
        id="test_func",
        type=EntityType.FUNCTION,
        name="test_function",
        qualified_name="test.test_function",
        location=Location(
            file_path=Path("/test/file.py"),
            start_line=1,
            end_line=5,
            start_column=0,
            end_column=0,
        ),
        source_code="def test_function():\n    pass",
    )


# Tool Registration Tests

class TestToolRegistration:
    """ツール登録のテスト"""

    def test_tool_list_returns_14_tools(self):
        """Verify 14 tools are defined."""
        from mcp.server import Server

        from iflow_mcp_nahisaho_codegraph_mcp_server.mcp.tools import register

        server = Server("test")
        config = Config(repo_path=Path("/tmp"))
        register(server, config)

        # The tools are registered via decorator
        # We can verify by checking handler count
        assert server is not None

    def test_tool_schema_validation(self):
        """Tool schemas have required fields."""
        from mcp.types import Tool

        # Test tool creation with valid schema
        tool = Tool(
            name="test_tool",
            description="Test description",
            inputSchema={
                "type": "object",
                "properties": {
                    "query": {"type": "string"},
                },
                "required": ["query"],
            },
        )
        assert tool.name == "test_tool"
        assert "query" in tool.inputSchema["properties"]


# Handler Tests

class TestQueryCodebaseTool:
    """query_codebase ツールのテスト"""

    @pytest.mark.asyncio
    async def test_query_codebase_handler(self, config: Config):
        """Test query_codebase handler."""
        from iflow_mcp_nahisaho_codegraph_mcp_server.mcp.tools import _handle_query_codebase

        mock_engine = AsyncMock()
        mock_result = MagicMock()
        mock_result.to_dict.return_value = {"entities": [], "relations": []}
        mock_engine.query.return_value = mock_result

        result = await _handle_query_codebase(
            {"query": "find functions", "max_results": 10},
            mock_engine,
            config,
        )

        assert "entities" in result
        mock_engine.query.assert_called_once()


class TestFindDependenciesTool:
    """find_dependencies ツールのテスト"""

    @pytest.mark.asyncio
    async def test_find_dependencies_handler(self, config: Config):
        """Test find_dependencies handler."""
        from iflow_mcp_nahisaho_codegraph_mcp_server.mcp.tools import _handle_find_dependencies

        mock_engine = AsyncMock()
        mock_result = MagicMock()
        mock_result.to_dict.return_value = {"entities": [], "relations": []}
        mock_engine.find_dependencies.return_value = mock_result

        result = await _handle_find_dependencies(
            {"entity_id": "test_id", "depth": 2},
            mock_engine,
            config,
        )

        assert "entities" in result
        mock_engine.find_dependencies.assert_called_once_with("test_id", depth=2)


class TestFindCallersTool:
    """find_callers ツールのテスト"""

    @pytest.mark.asyncio
    async def test_find_callers_handler(self, config: Config, mock_entity: Entity):
        """Test find_callers handler."""
        from iflow_mcp_nahisaho_codegraph_mcp_server.mcp.tools import _handle_find_callers

        mock_engine = AsyncMock()
        mock_engine.find_callers.return_value = [mock_entity]

        result = await _handle_find_callers(
            {"entity_id": "test_id"},
            mock_engine,
            config,
        )

        assert "callers" in result
        assert len(result["callers"]) == 1
        assert result["callers"][0]["name"] == "test_function"


class TestFindCalleesTool:
    """find_callees ツールのテスト"""

    @pytest.mark.asyncio
    async def test_find_callees_handler(self, config: Config, mock_entity: Entity):
        """Test find_callees handler."""
        from iflow_mcp_nahisaho_codegraph_mcp_server.mcp.tools import _handle_find_callees

        mock_engine = AsyncMock()
        mock_engine.find_callees.return_value = [mock_entity]

        result = await _handle_find_callees(
            {"entity_id": "test_id"},
            mock_engine,
            config,
        )

        assert "callees" in result
        assert len(result["callees"]) == 1


class TestGetCodeSnippetTool:
    """get_code_snippet ツールのテスト"""

    @pytest.mark.asyncio
    async def test_get_code_snippet_found(self, config: Config, mock_entity: Entity):
        """Test get_code_snippet when entity found."""
        from iflow_mcp_nahisaho_codegraph_mcp_server.mcp.tools import _handle_get_code_snippet

        mock_engine = AsyncMock()
        mock_engine.get_entity.return_value = mock_entity

        result = await _handle_get_code_snippet(
            {"entity_id": "test_id"},
            mock_engine,
            config,
        )

        assert "source" in result
        assert "def test_function" in result["source"]

    @pytest.mark.asyncio
    async def test_get_code_snippet_not_found(self, config: Config):
        """Test get_code_snippet when entity not found."""
        from iflow_mcp_nahisaho_codegraph_mcp_server.mcp.tools import _handle_get_code_snippet

        mock_engine = AsyncMock()
        mock_engine.get_entity.return_value = None

        result = await _handle_get_code_snippet(
            {"entity_id": "nonexistent"},
            mock_engine,
            config,
        )

        assert "error" in result


class TestReadFileTool:
    """read_file_content ツールのテスト"""

    @pytest.mark.asyncio
    async def test_read_file_full(self, temp_repo: Path, config: Config):
        """Test reading full file."""
        from iflow_mcp_nahisaho_codegraph_mcp_server.mcp.tools import _handle_read_file

        mock_engine = AsyncMock()

        result = await _handle_read_file(
            {"file_path": "main.py"},
            mock_engine,
            config,
        )

        assert "content" in result
        assert "def main" in result["content"]

    @pytest.mark.asyncio
    async def test_read_file_range(self, temp_repo: Path, config: Config):
        """Test reading file with line range."""
        from iflow_mcp_nahisaho_codegraph_mcp_server.mcp.tools import _handle_read_file

        mock_engine = AsyncMock()

        result = await _handle_read_file(
            {"file_path": "main.py", "start_line": 2, "end_line": 4},
            mock_engine,
            config,
        )

        assert "content" in result
        assert "lines" in result

    @pytest.mark.asyncio
    async def test_read_file_not_found(self, temp_repo: Path, config: Config):
        """Test reading nonexistent file."""
        from iflow_mcp_nahisaho_codegraph_mcp_server.mcp.tools import _handle_read_file

        mock_engine = AsyncMock()

        result = await _handle_read_file(
            {"file_path": "nonexistent.py"},
            mock_engine,
            config,
        )

        assert "error" in result


class TestReindexTool:
    """reindex_repository ツールのテスト"""

    @pytest.mark.asyncio
    async def test_reindex_incremental(self, temp_repo: Path, config: Config):
        """Test incremental reindex."""
        from iflow_mcp_nahisaho_codegraph_mcp_server.mcp.tools import _handle_reindex

        mock_engine = AsyncMock()

        result = await _handle_reindex(
            {"incremental": True},
            mock_engine,
            config,
        )

        assert "entities" in result
        assert "duration" in result


class TestExecuteCommandTool:
    """execute_shell_command ツールのテスト"""

    @pytest.mark.asyncio
    async def test_execute_simple_command(self, temp_repo: Path, config: Config):
        """Test executing simple command."""
        from iflow_mcp_nahisaho_codegraph_mcp_server.mcp.tools import _handle_execute_command

        mock_engine = AsyncMock()

        result = await _handle_execute_command(
            {"command": "echo hello", "timeout": 5},
            mock_engine,
            config,
        )

        assert "exit_code" in result
        assert result["exit_code"] == 0
        assert "hello" in result["stdout"]

    @pytest.mark.asyncio
    async def test_execute_command_timeout(self, temp_repo: Path, config: Config):
        """Test command timeout with long-running allowed command."""
        from iflow_mcp_nahisaho_codegraph_mcp_server.mcp.tools import _handle_execute_command

        mock_engine = AsyncMock()

        # Use a long-running find command that will timeout
        result = await _handle_execute_command(
            {"command": "find / -name 'nonexistent_file_xyz'", "timeout": 1},
            mock_engine,
            config,
        )

        # May timeout or complete quickly, both are valid
        assert "error" in result or "exit_code" in result


class TestCommandSecurityValidation:
    """コマンドセキュリティ検証のテスト"""

    def test_validate_command_allowed(self):
        """Test that allowed commands pass validation."""
        from iflow_mcp_nahisaho_codegraph_mcp_server.mcp.tools import _validate_command

        allowed_commands = [
            "git status",
            "grep -r pattern .",
            "find . -name '*.py'",
            "python --version",
            "ls -la",
            "echo hello",
        ]

        for cmd in allowed_commands:
            is_valid, error = _validate_command(cmd)
            assert is_valid, f"Command '{cmd}' should be allowed: {error}"

    def test_validate_command_blocked_dangerous(self):
        """Test that dangerous commands are blocked."""
        from iflow_mcp_nahisaho_codegraph_mcp_server.mcp.tools import _validate_command

        dangerous_commands = [
            "rm -rf /",
            "sudo apt install",
            "curl http://evil.com",
            "wget http://evil.com",
            "chmod 777 file",
            "echo foo > /etc/passwd",
            "cat file | bash",
            "$(whoami)",
            "eval 'code'",
        ]

        for cmd in dangerous_commands:
            is_valid, error = _validate_command(cmd)
            assert not is_valid, f"Command '{cmd}' should be blocked"

    def test_validate_command_blocked_not_whitelisted(self):
        """Test that non-whitelisted commands are blocked."""
        from iflow_mcp_nahisaho_codegraph_mcp_server.mcp.tools import _validate_command

        blocked_commands = [
            "apt-get install",
            "brew install",
            "systemctl restart",
            "unknown_command",
        ]

        for cmd in blocked_commands:
            is_valid, error = _validate_command(cmd)
            assert not is_valid, f"Command '{cmd}' should be blocked"

    @pytest.mark.asyncio
    async def test_execute_blocked_command(self, temp_repo: Path, config: Config):
        """Test that blocked commands return error."""
        from iflow_mcp_nahisaho_codegraph_mcp_server.mcp.tools import _handle_execute_command

        mock_engine = AsyncMock()

        result = await _handle_execute_command(
            {"command": "rm -rf /", "timeout": 5},
            mock_engine,
            config,
        )

        assert "error" in result
        assert "rejected" in result["error"]


class TestPathTraversalProtection:
    """パストラバーサル保護のテスト"""

    def test_validate_path_normal(self, temp_repo: Path):
        """Test normal path validation."""
        from iflow_mcp_nahisaho_codegraph_mcp_server.mcp.tools import _validate_path

        # Create test file
        test_file = temp_repo / "test.py"
        test_file.write_text("# test")

        result = _validate_path("test.py", temp_repo)
        assert result is not None
        assert result == test_file.resolve()

    def test_validate_path_traversal_blocked(self, temp_repo: Path):
        """Test that path traversal is blocked."""
        from iflow_mcp_nahisaho_codegraph_mcp_server.mcp.tools import _validate_path

        dangerous_paths = [
            "../../../etc/passwd",
            "/etc/passwd",
            "foo/../../../etc/shadow",
        ]

        for path in dangerous_paths:
            result = _validate_path(path, temp_repo)
            assert result is None, f"Path '{path}' should be blocked"

    def test_validate_path_subdirectory(self, temp_repo: Path):
        """Test subdirectory paths are allowed."""
        from iflow_mcp_nahisaho_codegraph_mcp_server.mcp.tools import _validate_path

        # Create subdirectory
        subdir = temp_repo / "src" / "module"
        subdir.mkdir(parents=True)
        test_file = subdir / "test.py"
        test_file.write_text("# test")

        result = _validate_path("src/module/test.py", temp_repo)
        assert result is not None
        assert result == test_file.resolve()

    @pytest.mark.asyncio
    async def test_read_file_traversal_blocked(self, temp_repo: Path, config: Config):
        """Test read_file blocks traversal attempts."""
        from iflow_mcp_nahisaho_codegraph_mcp_server.mcp.tools import _handle_read_file

        mock_engine = AsyncMock()

        result = await _handle_read_file(
            {"file_path": "../../../etc/passwd"},
            mock_engine,
            config,
        )

        assert "error" in result
        assert "access denied" in result["error"]


class TestDispatchTool:
    """_dispatch_tool のテスト"""

    @pytest.mark.asyncio
    async def test_dispatch_unknown_tool(self, config: Config):
        """Test dispatching unknown tool."""
        from iflow_mcp_nahisaho_codegraph_mcp_server.mcp.tools import _dispatch_tool

        mock_engine = AsyncMock()

        result = await _dispatch_tool(
            "unknown_tool",
            {},
            mock_engine,
            config,
        )

        assert "error" in result
        assert "Unknown tool" in result["error"]

    @pytest.mark.asyncio
    async def test_dispatch_known_tool(self, config: Config):
        """Test dispatching known tool."""
        from iflow_mcp_nahisaho_codegraph_mcp_server.mcp.tools import _dispatch_tool

        mock_engine = AsyncMock()
        mock_result = MagicMock()
        mock_result.to_dict.return_value = {"entities": []}
        mock_engine.query.return_value = mock_result

        result = await _dispatch_tool(
            "query_codebase",
            {"query": "test"},
            mock_engine,
            config,
        )

        assert "entities" in result
