"""Tests for structure_tools module."""

from unittest.mock import AsyncMock, MagicMock

import pytest

from openzim_mcp.config import OpenZimMcpConfig
from openzim_mcp.exceptions import OpenZimMcpRateLimitError
from openzim_mcp.server import OpenZimMcpServer


class TestRegisterStructureTools:
    """Test structure tools registration."""

    def test_register_structure_tools(self, test_config: OpenZimMcpConfig):
        """Test that structure tools are registered correctly."""
        server = OpenZimMcpServer(test_config)
        assert server.mcp is not None


class TestGetArticleStructureTool:
    """Test get_article_structure tool functionality."""

    @pytest.fixture
    def server(self, test_config: OpenZimMcpConfig) -> OpenZimMcpServer:
        """Create a test server instance."""
        return OpenZimMcpServer(test_config)

    @pytest.mark.asyncio
    async def test_get_article_structure_success(self, server: OpenZimMcpServer):
        """Test successful article structure retrieval."""
        server.async_zim_operations.get_article_structure = AsyncMock(
            return_value='{"headings": [{"level": 1, "text": "Title"}]}'
        )
        server.rate_limiter.check_rate_limit = MagicMock()

        result = await server.async_zim_operations.get_article_structure(
            "/path/to/file.zim", "C/Article"
        )

        assert "headings" in result
        server.async_zim_operations.get_article_structure.assert_called_once_with(
            "/path/to/file.zim", "C/Article"
        )

    @pytest.mark.asyncio
    async def test_get_article_structure_rate_limit_error(
        self, server: OpenZimMcpServer
    ):
        """Test rate limit handling in get_article_structure."""
        error = OpenZimMcpRateLimitError("Rate limit exceeded")
        error_msg = server._create_enhanced_error_message(
            operation="get article structure",
            error=error,
            context="Entry: C/Article",
        )
        assert "article structure" in error_msg or "Operation" in error_msg

    @pytest.mark.asyncio
    async def test_get_article_structure_generic_exception(
        self, server: OpenZimMcpServer
    ):
        """Test generic exception handling in get_article_structure."""
        server.async_zim_operations.get_article_structure = AsyncMock(
            side_effect=Exception("Test error")
        )

        with pytest.raises(Exception) as exc_info:
            await server.async_zim_operations.get_article_structure(
                "/path/to/file.zim", "C/Article"
            )
        assert "Test error" in str(exc_info.value)


class TestExtractArticleLinksTool:
    """Test extract_article_links tool functionality."""

    @pytest.fixture
    def server(self, test_config: OpenZimMcpConfig) -> OpenZimMcpServer:
        """Create a test server instance."""
        return OpenZimMcpServer(test_config)

    @pytest.mark.asyncio
    async def test_extract_article_links_success(self, server: OpenZimMcpServer):
        """Test successful link extraction."""
        server.async_zim_operations.extract_article_links = AsyncMock(
            return_value='{"internal": ["A/Link1"], "external": ["example.com"]}'
        )
        server.rate_limiter.check_rate_limit = MagicMock()

        result = await server.async_zim_operations.extract_article_links(
            "/path/to/file.zim", "C/Article"
        )

        assert "internal" in result or "external" in result
        server.async_zim_operations.extract_article_links.assert_called_once_with(
            "/path/to/file.zim", "C/Article"
        )

    @pytest.mark.asyncio
    async def test_extract_article_links_rate_limit_error(
        self, server: OpenZimMcpServer
    ):
        """Test rate limit handling in extract_article_links."""
        error = OpenZimMcpRateLimitError("Rate limit exceeded")
        error_msg = server._create_enhanced_error_message(
            operation="extract article links",
            error=error,
            context="Entry: C/Article",
        )
        assert "links" in error_msg or "Operation" in error_msg


class TestGetEntrySummaryTool:
    """Test get_entry_summary tool functionality."""

    @pytest.fixture
    def server(self, test_config: OpenZimMcpConfig) -> OpenZimMcpServer:
        """Create a test server instance."""
        return OpenZimMcpServer(test_config)

    @pytest.mark.asyncio
    async def test_get_entry_summary_success(self, server: OpenZimMcpServer):
        """Test successful entry summary retrieval."""
        server.async_zim_operations.get_entry_summary = AsyncMock(
            return_value='{"title": "Article", "summary": "Summary.", "word_count": 5}'
        )
        server.rate_limiter.check_rate_limit = MagicMock()

        result = await server.async_zim_operations.get_entry_summary(
            "/path/to/file.zim", "C/Article", 200
        )

        assert "summary" in result
        server.async_zim_operations.get_entry_summary.assert_called_once_with(
            "/path/to/file.zim", "C/Article", 200
        )

    @pytest.mark.asyncio
    async def test_get_entry_summary_custom_max_words(self, server: OpenZimMcpServer):
        """Test entry summary with custom max_words."""
        server.async_zim_operations.get_entry_summary = AsyncMock(
            return_value='{"summary": "Longer summary.", "word_count": 500}'
        )

        result = await server.async_zim_operations.get_entry_summary(
            "/path/to/file.zim", "C/Article", 500
        )

        assert "summary" in result


class TestGetTableOfContentsTool:
    """Test get_table_of_contents tool functionality."""

    @pytest.fixture
    def server(self, test_config: OpenZimMcpConfig) -> OpenZimMcpServer:
        """Create a test server instance."""
        return OpenZimMcpServer(test_config)

    @pytest.mark.asyncio
    async def test_get_table_of_contents_success(self, server: OpenZimMcpServer):
        """Test successful TOC retrieval."""
        server.async_zim_operations.get_table_of_contents = AsyncMock(
            return_value='{"toc": [{"level": 1, "text": "Intro"}], "heading_count": 1}'
        )
        server.rate_limiter.check_rate_limit = MagicMock()

        result = await server.async_zim_operations.get_table_of_contents(
            "/path/to/file.zim", "C/Article"
        )

        assert "toc" in result
        server.async_zim_operations.get_table_of_contents.assert_called_once_with(
            "/path/to/file.zim", "C/Article"
        )

    @pytest.mark.asyncio
    async def test_get_table_of_contents_rate_limit_error(
        self, server: OpenZimMcpServer
    ):
        """Test rate limit handling in get_table_of_contents."""
        error = OpenZimMcpRateLimitError("Rate limit exceeded")
        error_msg = server._create_enhanced_error_message(
            operation="get table of contents",
            error=error,
            context="Entry: C/Article",
        )
        assert "table of contents" in error_msg or "Operation" in error_msg


class TestGetBinaryEntryTool:
    """Test get_binary_entry tool functionality."""

    @pytest.fixture
    def server(self, test_config: OpenZimMcpConfig) -> OpenZimMcpServer:
        """Create a test server instance."""
        return OpenZimMcpServer(test_config)

    @pytest.mark.asyncio
    async def test_get_binary_entry_success(self, server: OpenZimMcpServer):
        """Test successful binary entry retrieval."""
        server.async_zim_operations.get_binary_entry = AsyncMock(
            return_value='{"path": "I/img.png", "mime_type": "image/png", "size": 1024}'
        )
        server.rate_limiter.check_rate_limit = MagicMock()

        result = await server.async_zim_operations.get_binary_entry(
            "/path/to/file.zim", "I/image.png", None, True
        )

        assert "mime_type" in result
        server.async_zim_operations.get_binary_entry.assert_called_once_with(
            "/path/to/file.zim", "I/image.png", None, True
        )

    @pytest.mark.asyncio
    async def test_get_binary_entry_metadata_only(self, server: OpenZimMcpServer):
        """Test binary entry retrieval with metadata only."""
        server.async_zim_operations.get_binary_entry = AsyncMock(
            return_value='{"path": "I/img.png", "mime_type": "image/png", "size": 1024}'
        )

        result = await server.async_zim_operations.get_binary_entry(
            "/path/to/file.zim", "I/image.png", None, False
        )

        assert "size" in result

    @pytest.mark.asyncio
    async def test_get_binary_entry_with_max_size(self, server: OpenZimMcpServer):
        """Test binary entry retrieval with max size limit."""
        server.async_zim_operations.get_binary_entry = AsyncMock(
            return_value='{"path": "I/video.mp4", "truncated": true}'
        )

        result = await server.async_zim_operations.get_binary_entry(
            "/path/to/file.zim", "I/video.mp4", 1000000, True
        )

        assert "truncated" in result or "path" in result

    @pytest.mark.asyncio
    async def test_get_binary_entry_rate_limit_error(self, server: OpenZimMcpServer):
        """Test rate limit handling in get_binary_entry."""
        error = OpenZimMcpRateLimitError("Rate limit exceeded")
        error_msg = server._create_enhanced_error_message(
            operation="retrieve binary entry",
            error=error,
            context="Entry: I/image.png",
        )
        assert "binary" in error_msg or "Operation" in error_msg


class TestInputSanitizationStructure:
    """Test input sanitization in structure tools."""

    def test_sanitize_inputs(self, test_config: OpenZimMcpConfig):
        """Test that all inputs are validated correctly."""
        from openzim_mcp.constants import INPUT_LIMIT_ENTRY_PATH, INPUT_LIMIT_FILE_PATH
        from openzim_mcp.exceptions import OpenZimMcpValidationError
        from openzim_mcp.security import sanitize_input

        # Valid inputs should pass
        valid_path = "p" * 100
        sanitized_path = sanitize_input(valid_path, INPUT_LIMIT_FILE_PATH)
        assert sanitized_path == valid_path

        valid_entry = "e" * 100
        sanitized_entry = sanitize_input(valid_entry, INPUT_LIMIT_ENTRY_PATH)
        assert sanitized_entry == valid_entry

        # Long inputs should raise errors
        long_path = "p" * 2000
        with pytest.raises(OpenZimMcpValidationError):
            sanitize_input(long_path, INPUT_LIMIT_FILE_PATH)

        long_entry = "e" * 1000
        with pytest.raises(OpenZimMcpValidationError):
            sanitize_input(long_entry, INPUT_LIMIT_ENTRY_PATH)


class TestStructureToolsDirectInvocation:
    """Test structure tools by directly invoking registered tool handlers."""

    @pytest.fixture
    def advanced_server(self, temp_dir):
        """Create a server in advanced mode."""
        from openzim_mcp.config import CacheConfig, OpenZimMcpConfig

        config = OpenZimMcpConfig(
            allowed_directories=[str(temp_dir)],
            tool_mode="advanced",
            cache=CacheConfig(enabled=False),
        )
        return OpenZimMcpServer(config)

    @pytest.mark.asyncio
    async def test_get_article_structure_tool_invocation(
        self, advanced_server, temp_dir
    ):
        """Test invoking get_article_structure tool handler directly."""
        # Mock the async operations
        advanced_server.async_zim_operations.get_article_structure = AsyncMock(
            return_value='{"headings": [{"level": 1, "text": "Test"}], "sections": []}'
        )

        # Find and call the registered tool
        tools = advanced_server.mcp._tool_manager._tools
        if "get_article_structure" in tools:
            tool_handler = tools["get_article_structure"].fn
            result = await tool_handler(
                zim_file_path=str(temp_dir / "test.zim"),
                entry_path="C/Article",
            )
            assert "headings" in result

    @pytest.mark.asyncio
    async def test_get_article_structure_with_rate_limit(
        self, advanced_server, temp_dir
    ):
        """Test get_article_structure when rate limited."""
        advanced_server.rate_limiter.check_rate_limit = MagicMock(
            side_effect=OpenZimMcpRateLimitError("Rate limit exceeded")
        )

        tools = advanced_server.mcp._tool_manager._tools
        if "get_article_structure" in tools:
            tool_handler = tools["get_article_structure"].fn
            result = await tool_handler(
                zim_file_path=str(temp_dir / "test.zim"),
                entry_path="C/Article",
            )
            assert "Error" in result or "Rate limit" in result

    @pytest.mark.asyncio
    async def test_extract_article_links_tool_invocation(
        self, advanced_server, temp_dir
    ):
        """Test invoking extract_article_links tool handler directly."""
        advanced_server.async_zim_operations.extract_article_links = AsyncMock(
            return_value='{"internal_links": [], "external_links": []}'
        )

        tools = advanced_server.mcp._tool_manager._tools
        if "extract_article_links" in tools:
            tool_handler = tools["extract_article_links"].fn
            result = await tool_handler(
                zim_file_path=str(temp_dir / "test.zim"),
                entry_path="C/Article",
            )
            assert "links" in result

    @pytest.mark.asyncio
    async def test_extract_article_links_with_exception(
        self, advanced_server, temp_dir
    ):
        """Test extract_article_links when an exception occurs."""
        advanced_server.async_zim_operations.extract_article_links = AsyncMock(
            side_effect=Exception("Test error")
        )

        tools = advanced_server.mcp._tool_manager._tools
        if "extract_article_links" in tools:
            tool_handler = tools["extract_article_links"].fn
            result = await tool_handler(
                zim_file_path=str(temp_dir / "test.zim"),
                entry_path="C/Article",
            )
            # Should return error message, not raise
            assert "Error" in result or "error" in result.lower()

    @pytest.mark.asyncio
    async def test_get_entry_summary_tool_invocation(self, advanced_server, temp_dir):
        """Test invoking get_entry_summary tool handler directly."""
        advanced_server.async_zim_operations.get_entry_summary = AsyncMock(
            return_value='{"title": "Article", "summary": "Test", "word_count": 50}'
        )

        tools = advanced_server.mcp._tool_manager._tools
        if "get_entry_summary" in tools:
            tool_handler = tools["get_entry_summary"].fn
            result = await tool_handler(
                zim_file_path=str(temp_dir / "test.zim"),
                entry_path="C/Article",
                max_words=100,
            )
            assert "summary" in result

    @pytest.mark.asyncio
    async def test_get_entry_summary_with_exception(self, advanced_server, temp_dir):
        """Test get_entry_summary when an exception occurs."""
        advanced_server.async_zim_operations.get_entry_summary = AsyncMock(
            side_effect=ValueError("Invalid entry")
        )

        tools = advanced_server.mcp._tool_manager._tools
        if "get_entry_summary" in tools:
            tool_handler = tools["get_entry_summary"].fn
            result = await tool_handler(
                zim_file_path=str(temp_dir / "test.zim"),
                entry_path="C/Invalid",
            )
            assert "Error" in result or "error" in result.lower()

    @pytest.mark.asyncio
    async def test_get_table_of_contents_tool_invocation(
        self, advanced_server, temp_dir
    ):
        """Test invoking get_table_of_contents tool handler directly."""
        advanced_server.async_zim_operations.get_table_of_contents = AsyncMock(
            return_value='{"toc": [], "heading_count": 0, "max_depth": 0}'
        )

        tools = advanced_server.mcp._tool_manager._tools
        if "get_table_of_contents" in tools:
            tool_handler = tools["get_table_of_contents"].fn
            result = await tool_handler(
                zim_file_path=str(temp_dir / "test.zim"),
                entry_path="C/Article",
            )
            assert "toc" in result

    @pytest.mark.asyncio
    async def test_get_table_of_contents_with_exception(
        self, advanced_server, temp_dir
    ):
        """Test get_table_of_contents when an exception occurs."""
        advanced_server.async_zim_operations.get_table_of_contents = AsyncMock(
            side_effect=RuntimeError("Failed to parse")
        )

        tools = advanced_server.mcp._tool_manager._tools
        if "get_table_of_contents" in tools:
            tool_handler = tools["get_table_of_contents"].fn
            result = await tool_handler(
                zim_file_path=str(temp_dir / "test.zim"),
                entry_path="C/Article",
            )
            assert "Error" in result or "error" in result.lower()

    @pytest.mark.asyncio
    async def test_get_binary_entry_tool_invocation(self, advanced_server, temp_dir):
        """Test invoking get_binary_entry tool handler directly."""
        advanced_server.async_zim_operations.get_binary_entry = AsyncMock(
            return_value='{"path": "I/img.png", "mime_type": "image/png", "size": 1024}'
        )

        tools = advanced_server.mcp._tool_manager._tools
        if "get_binary_entry" in tools:
            tool_handler = tools["get_binary_entry"].fn
            result = await tool_handler(
                zim_file_path=str(temp_dir / "test.zim"),
                entry_path="I/image.png",
            )
            assert "mime_type" in result

    @pytest.mark.asyncio
    async def test_get_binary_entry_with_all_params(self, advanced_server, temp_dir):
        """Test get_binary_entry with all parameters specified."""
        advanced_server.async_zim_operations.get_binary_entry = AsyncMock(
            return_value='{"path": "I/doc.pdf", "mime_type": "application/pdf"}'
        )

        tools = advanced_server.mcp._tool_manager._tools
        if "get_binary_entry" in tools:
            tool_handler = tools["get_binary_entry"].fn
            result = await tool_handler(
                zim_file_path=str(temp_dir / "test.zim"),
                entry_path="I/doc.pdf",
                max_size_bytes=5000000,
                include_data=False,
            )
            assert "pdf" in result

    @pytest.mark.asyncio
    async def test_get_binary_entry_with_exception(self, advanced_server, temp_dir):
        """Test get_binary_entry when an exception occurs."""
        advanced_server.async_zim_operations.get_binary_entry = AsyncMock(
            side_effect=IOError("File not found")
        )

        tools = advanced_server.mcp._tool_manager._tools
        if "get_binary_entry" in tools:
            tool_handler = tools["get_binary_entry"].fn
            result = await tool_handler(
                zim_file_path=str(temp_dir / "test.zim"),
                entry_path="I/missing.png",
            )
            # Error messages may be formatted with **Error** or **Resource Not Found**
            assert "**" in result and (
                "Error" in result or "Not Found" in result or "Operation" in result
            )
