"""Tests for the MCP server module."""

import hashlib
import json
import os
from unittest.mock import MagicMock, mock_open, patch

import pytest
import yaml

from penpot_mcp.server.mcp_server import PenpotMCPServer, create_server


def test_server_initialization():
    """Test server initialization."""
    server = PenpotMCPServer(name="Test Server", test_mode=True)

    # Check that the server has the expected properties
    assert server.mcp is not None
    assert server.api is not None
    assert hasattr(server, '_register_resources')
    assert hasattr(server, '_register_tools')
    assert hasattr(server, 'run')


def test_server_info_resource():
    """Test the server_info resource handler function directly."""
    # Since we can't easily access the registered resource from FastMCP,
    # we'll implement it here based on the implementation in mcp_server.py
    def server_info():
        from penpot_mcp.utils import config
        return {
            "status": "online",
            "name": "Penpot MCP Server",
            "description": "Model Context Provider for Penpot",
            "api_url": config.PENPOT_API_URL
        }
    
    # Call the function
    result = server_info()
    
    # Check the result
    assert isinstance(result, dict)
    assert "status" in result
    assert result["status"] == "online"
    assert "name" in result
    assert "description" in result
    assert "api_url" in result


def test_list_projects_tool_handler(mock_penpot_api):
    """Test the list_projects tool handler directly."""
    # Create a callable that matches what would be registered
    def list_projects():
        try:
            projects = mock_penpot_api.list_projects()
            return {"projects": projects}
        except Exception as e:
            return {"error": str(e)}

    # Call the handler
    result = list_projects()

    # Check the result
    assert isinstance(result, dict)
    assert "projects" in result
    assert len(result["projects"]) == 2
    assert result["projects"][0]["id"] == "project1"
    assert result["projects"][1]["id"] == "project2"

    # Verify API was called
    mock_penpot_api.list_projects.assert_called_once()


def test_get_project_files_tool_handler(mock_penpot_api):
    """Test the get_project_files tool handler directly."""
    # Create a callable that matches what would be registered
    def get_project_files(project_id):
        try:
            files = mock_penpot_api.get_project_files(project_id)
            return {"files": files}
        except Exception as e:
            return {"error": str(e)}

    # Call the handler with a project ID
    result = get_project_files("project1")

    # Check the result
    assert isinstance(result, dict)
    assert "files" in result
    assert len(result["files"]) == 2
    assert result["files"][0]["id"] == "file1"
    assert result["files"][1]["id"] == "file2"

    # Verify API was called with correct parameters
    mock_penpot_api.get_project_files.assert_called_once_with("project1")


def test_get_file_tool_handler(mock_penpot_api):
    """Test the get_file tool handler directly."""
    # Create a callable that matches what would be registered
    def get_file(file_id):
        try:
            file_data = mock_penpot_api.get_file(file_id=file_id)
            return file_data
        except Exception as e:
            return {"error": str(e)}

    # Call the handler with a file ID
    result = get_file("file1")

    # Check the result
    assert isinstance(result, dict)
    assert result["id"] == "file1"
    assert result["name"] == "Test File"
    assert "data" in result
    assert "pages" in result["data"]

    # Verify API was called with correct parameters
    mock_penpot_api.get_file.assert_called_once_with(file_id="file1")


@patch('os.path.join')
@patch('builtins.open', new_callable=mock_open, read_data='{"test": "schema"}')
def test_penpot_schema_resource_handler(mock_file_open, mock_join):
    """Test the schema resource handler directly."""
    # Setup the mock join to return a predictable path
    mock_join.return_value = '/mock/path/to/penpot-schema.json'

    # Create a callable that matches what would be registered
    def penpot_schema():
        from penpot_mcp.utils import config
        schema_path = os.path.join(config.RESOURCES_PATH, 'penpot-schema.json')
        try:
            with open(schema_path, 'r') as f:
                return json.load(f)
        except Exception as e:
            return {"error": f"Failed to load schema: {str(e)}"}

    # Call the handler
    result = penpot_schema()

    # Check result matches our mocked file content
    assert isinstance(result, dict)
    assert "test" in result
    assert result["test"] == "schema"

    # Verify file was opened
    mock_file_open.assert_called_once_with('/mock/path/to/penpot-schema.json', 'r')


@patch('os.path.join')
@patch('builtins.open', new_callable=mock_open, read_data='{"test": "tree-schema"}')
def test_penpot_tree_schema_resource_handler(mock_file_open, mock_join):
    """Test the tree schema resource handler directly."""
    # Setup the mock join to return a predictable path
    mock_join.return_value = '/mock/path/to/penpot-tree-schema.json'

    # Create a callable that matches what would be registered
    def penpot_tree_schema():
        from penpot_mcp.utils import config
        schema_path = os.path.join(config.RESOURCES_PATH, 'penpot-tree-schema.json')
        try:
            with open(schema_path, 'r') as f:
                return json.load(f)
        except Exception as e:
            return {"error": f"Failed to load tree schema: {str(e)}"}

    # Call the handler
    result = penpot_tree_schema()

    # Check result matches our mocked file content
    assert isinstance(result, dict)
    assert "test" in result
    assert result["test"] == "tree-schema"

    # Verify file was opened
    mock_file_open.assert_called_once_with('/mock/path/to/penpot-tree-schema.json', 'r')


def test_create_server():
    """Test the create_server function."""
    with patch('penpot_mcp.server.mcp_server.PenpotMCPServer') as mock_server_class:
        mock_server_instance = MagicMock()
        mock_server_class.return_value = mock_server_instance

        # Test that create_server passes test_mode=True when in test environment
        with patch('penpot_mcp.server.mcp_server.sys.modules', {'pytest': True}):
            server = create_server()
            mock_server_class.assert_called_once_with(test_mode=True)
            assert server == mock_server_instance


@patch('penpot_mcp.tools.penpot_tree.get_object_subtree_with_fields')
def test_get_object_tree_basic(mock_get_subtree, mock_penpot_api):
    """Test the get_object_tree tool handler with basic parameters."""
    # Setup the mock get_object_subtree_with_fields function
    mock_get_subtree.return_value = {
        "tree": {
            "id": "obj1",
            "type": "frame",
            "name": "Test Object",
            "children": []
        },
        "page_id": "page1"
    }
    
    # Setup the export_object mock for the included image
    export_object_mock = MagicMock()
    export_object_mock.return_value = MagicMock(data=b'test_image_data', format='png')
    
    # Create a callable that matches what would be registered
    def get_object_tree(
        file_id: str, 
        object_id: str, 
        fields: list,  # Now required parameter
        depth: int = -1,
        format: str = "json"
    ):
        try:
            # Get the file data
            file_data = mock_penpot_api.get_file(file_id=file_id)
            
            # Use the mocked utility function
            result = mock_get_subtree(
                file_data, 
                object_id, 
                include_fields=fields,
                depth=depth
            )
            
            # Check if an error occurred
            if "error" in result:
                return result
                
            # Extract the tree and page_id
            simplified_tree = result["tree"]
            page_id = result["page_id"]
            
            # Prepare the result dictionary
            final_result = {"tree": simplified_tree}
            
            # Always include image (no longer optional)
            try:
                image = export_object_mock(
                    file_id=file_id,
                    page_id=page_id,
                    object_id=object_id
                )
                # New format: URI-based instead of base64 data
                image_id = hashlib.md5(f"{file_id}:{object_id}".encode()).hexdigest()
                image_uri = f"render_component://{image_id}"
                final_result["image"] = {
                    "uri": image_uri,
                    "format": image.format if hasattr(image, 'format') else "png"
                }
            except Exception as e:
                final_result["image_error"] = str(e)
            
            # Format the tree as YAML if requested
            if format.lower() == "yaml":
                try:
                    # Convert the entire result to YAML, including the image if present
                    yaml_result = yaml.dump(final_result, default_flow_style=False, sort_keys=False)
                    return {"yaml_result": yaml_result}
                except Exception as e:
                    return {"format_error": f"Error formatting as YAML: {str(e)}"}
            
            # Return the JSON format result
            return final_result
        except Exception as e:
            return {"error": str(e)}
    
    # Call the handler with basic parameters - fields is now required
    result = get_object_tree(
        file_id="file1", 
        object_id="obj1",
        fields=["id", "type", "name"]  # Required parameter
    )
    
    # Check the result
    assert isinstance(result, dict)
    assert "tree" in result
    assert result["tree"]["id"] == "obj1"
    assert result["tree"]["type"] == "frame"
    assert result["tree"]["name"] == "Test Object"
    
    # Check that image is always included
    assert "image" in result
    assert "uri" in result["image"]
    assert result["image"]["uri"].startswith("render_component://")
    assert result["image"]["format"] == "png"
    
    # Verify mocks were called with correct parameters
    mock_penpot_api.get_file.assert_called_once_with(file_id="file1")
    mock_get_subtree.assert_called_once_with(
        mock_penpot_api.get_file.return_value,
        "obj1",
        include_fields=["id", "type", "name"],
        depth=-1
    )


@patch('penpot_mcp.tools.penpot_tree.get_object_subtree_with_fields')
def test_get_object_tree_with_fields_and_depth(mock_get_subtree, mock_penpot_api):
    """Test the get_object_tree tool handler with custom field list and depth."""
    # Setup the mock get_object_subtree_with_fields function
    mock_get_subtree.return_value = {
        "tree": {
            "id": "obj1",
            "name": "Test Object",  # Only id and name fields included
            "children": []
        },
        "page_id": "page1"
    }
    
    # Setup the export_object mock for the included image
    export_object_mock = MagicMock()
    export_object_mock.return_value = MagicMock(data=b'test_image_data', format='png')
    
    # Create a callable that matches what would be registered
    def get_object_tree(
        file_id: str, 
        object_id: str, 
        fields: list,  # Now required parameter
        depth: int = -1,
        format: str = "json"
    ):
        try:
            # Get the file data
            file_data = mock_penpot_api.get_file(file_id=file_id)
            
            # Use the mocked utility function
            result = mock_get_subtree(
                file_data, 
                object_id, 
                include_fields=fields,
                depth=depth
            )
            
            # Extract the tree and page_id
            simplified_tree = result["tree"]
            page_id = result["page_id"]
            
            # Prepare the result dictionary
            final_result = {"tree": simplified_tree}
            
            # Always include image (no longer optional)
            try:
                image = export_object_mock(
                    file_id=file_id,
                    page_id=page_id,
                    object_id=object_id
                )
                # New format: URI-based instead of base64 data
                image_id = hashlib.md5(f"{file_id}:{object_id}".encode()).hexdigest()
                image_uri = f"render_component://{image_id}"
                final_result["image"] = {
                    "uri": image_uri,
                    "format": image.format if hasattr(image, 'format') else "png"
                }
            except Exception as e:
                final_result["image_error"] = str(e)
            
            # Format the tree as YAML if requested
            if format.lower() == "yaml":
                try:
                    # Convert the entire result to YAML, including the image if present
                    yaml_result = yaml.dump(final_result, default_flow_style=False, sort_keys=False)
                    return {"yaml_result": yaml_result}
                except Exception as e:
                    return {"format_error": f"Error formatting as YAML: {str(e)}"}
            
            # Return the JSON format result
            return final_result
        except Exception as e:
            return {"error": str(e)}
    
    # Call the handler with custom fields and depth
    result = get_object_tree(
        file_id="file1", 
        object_id="obj1", 
        fields=["id", "name"],  # Updated parameter name
        depth=2
    )
    
    # Check the result
    assert isinstance(result, dict)
    assert "tree" in result
    assert result["tree"]["id"] == "obj1"
    assert result["tree"]["name"] == "Test Object"
    assert "type" not in result["tree"]  # Type field should not be included
    
    # Check that image is always included
    assert "image" in result
    assert "uri" in result["image"]
    assert result["image"]["uri"].startswith("render_component://")
    assert result["image"]["format"] == "png"
    
    # Verify mocks were called with correct parameters
    mock_penpot_api.get_file.assert_called_once_with(file_id="file1")
    mock_get_subtree.assert_called_once_with(
        mock_penpot_api.get_file.return_value,
        "obj1",
        include_fields=["id", "name"],
        depth=2
    )


@patch('penpot_mcp.tools.penpot_tree.get_object_subtree_with_fields')
def test_get_object_tree_with_yaml_format(mock_get_subtree, mock_penpot_api):
    """Test the get_object_tree tool handler with YAML format output."""
    # Setup the mock get_object_subtree_with_fields function
    mock_get_subtree.return_value = {
        "tree": {
            "id": "obj1",
            "type": "frame",
            "name": "Test Object",
            "children": [
                {
                    "id": "child1",
                    "type": "text",
                    "name": "Child Text"
                }
            ]
        },
        "page_id": "page1"
    }
    
    # Setup the export_object mock for the included image
    export_object_mock = MagicMock()
    export_object_mock.return_value = MagicMock(data=b'test_image_data', format='png')
    
    # Create a callable that matches what would be registered
    def get_object_tree(
        file_id: str, 
        object_id: str, 
        fields: list,  # Now required parameter
        depth: int = -1,
        format: str = "json"
    ):
        try:
            # Get the file data
            file_data = mock_penpot_api.get_file(file_id=file_id)
            
            # Use the mocked utility function
            result = mock_get_subtree(
                file_data, 
                object_id, 
                include_fields=fields,
                depth=depth
            )
            
            # Extract the tree and page_id
            simplified_tree = result["tree"]
            page_id = result["page_id"]
            
            # Prepare the result dictionary
            final_result = {"tree": simplified_tree}
            
            # Always include image (no longer optional)
            try:
                image = export_object_mock(
                    file_id=file_id,
                    page_id=page_id,
                    object_id=object_id
                )
                # New format: URI-based instead of base64 data
                image_id = hashlib.md5(f"{file_id}:{object_id}".encode()).hexdigest()
                image_uri = f"render_component://{image_id}"
                final_result["image"] = {
                    "uri": image_uri,
                    "format": image.format if hasattr(image, 'format') else "png"
                }
            except Exception as e:
                final_result["image_error"] = str(e)
            
            # Format the tree as YAML if requested
            if format.lower() == "yaml":
                try:
                    # Convert the entire result to YAML, including the image if present
                    yaml_result = yaml.dump(final_result, default_flow_style=False, sort_keys=False)
                    return {"yaml_result": yaml_result}
                except Exception as e:
                    return {"format_error": f"Error formatting as YAML: {str(e)}"}
            
            # Return the JSON format result
            return final_result
        except Exception as e:
            return {"error": str(e)}
    
    # Call the handler with YAML format - fields is now required
    result = get_object_tree(
        file_id="file1", 
        object_id="obj1", 
        fields=["id", "type", "name"],  # Required parameter
        format="yaml"
    )
    
    # Check the result
    assert isinstance(result, dict)
    assert "yaml_result" in result
    assert "tree" not in result  # Should not contain the tree field
    
    # Verify the YAML content matches the expected tree structure
    parsed_yaml = yaml.safe_load(result["yaml_result"])
    assert "tree" in parsed_yaml
    assert parsed_yaml["tree"]["id"] == "obj1"
    assert parsed_yaml["tree"]["type"] == "frame"
    assert parsed_yaml["tree"]["name"] == "Test Object"
    assert isinstance(parsed_yaml["tree"]["children"], list)
    assert parsed_yaml["tree"]["children"][0]["id"] == "child1"
    
    # Check that image is included in YAML
    assert "image" in parsed_yaml
    assert "uri" in parsed_yaml["image"]
    assert parsed_yaml["image"]["uri"].startswith("render_component://")
    assert parsed_yaml["image"]["format"] == "png"
    
    # Verify mocks were called with correct parameters
    mock_penpot_api.get_file.assert_called_once_with(file_id="file1")
    mock_get_subtree.assert_called_once_with(
        mock_penpot_api.get_file.return_value,
        "obj1",
        include_fields=["id", "type", "name"],
        depth=-1
    )


@patch('penpot_mcp.tools.penpot_tree.get_object_subtree_with_fields')
def test_get_object_tree_with_include_image(mock_get_subtree, mock_penpot_api):
    """Test the get_object_tree tool handler with image inclusion (always included now)."""
    # Setup the mock get_object_subtree_with_fields function
    mock_get_subtree.return_value = {
        "tree": {
            "id": "obj1",
            "type": "frame",
            "name": "Test Object",
            "children": []
        },
        "page_id": "page1"
    }
    
    # Setup the export_object mock for the included image
    export_object_mock = MagicMock()
    export_object_mock.return_value = MagicMock(data=b'test_image_data', format='png')
    
    # Create a callable that matches what would be registered
    def get_object_tree(
        file_id: str, 
        object_id: str, 
        fields: list,  # Now required parameter
        depth: int = -1,
        format: str = "json"
    ):
        try:
            # Get the file data
            file_data = mock_penpot_api.get_file(file_id=file_id)
            
            # Use the mocked utility function
            result = mock_get_subtree(
                file_data, 
                object_id, 
                include_fields=fields,
                depth=depth
            )
            
            # Extract the tree and page_id
            simplified_tree = result["tree"]
            page_id = result["page_id"]
            
            # Prepare the result dictionary
            final_result = {"tree": simplified_tree}
            
            # Always include image (no longer optional)
            try:
                image = export_object_mock(
                    file_id=file_id,
                    page_id=page_id,
                    object_id=object_id
                )
                # New format: URI-based instead of base64 data
                image_id = hashlib.md5(f"{file_id}:{object_id}".encode()).hexdigest()
                image_uri = f"render_component://{image_id}"
                final_result["image"] = {
                    "uri": image_uri,
                    "format": image.format if hasattr(image, 'format') else "png"
                }
            except Exception as e:
                final_result["image_error"] = str(e)
            
            # Format the tree as YAML if requested
            if format.lower() == "yaml":
                try:
                    # Convert the entire result to YAML, including the image if present
                    yaml_result = yaml.dump(final_result, default_flow_style=False, sort_keys=False)
                    return {"yaml_result": yaml_result}
                except Exception as e:
                    return {"format_error": f"Error formatting as YAML: {str(e)}"}
            
            # Return the JSON format result
            return final_result
        except Exception as e:
            return {"error": str(e)}
    
    # Call the handler with required fields parameter
    result = get_object_tree(
        file_id="file1", 
        object_id="obj1", 
        fields=["id", "type", "name"]  # Updated parameter name
    )
    
    # Check the result
    assert isinstance(result, dict)
    assert "tree" in result
    assert result["tree"]["id"] == "obj1"
    assert result["tree"]["type"] == "frame"
    assert result["tree"]["name"] == "Test Object"
    
    # Check that image is always included
    assert "image" in result
    assert "uri" in result["image"]
    assert result["image"]["uri"].startswith("render_component://")
    assert result["image"]["format"] == "png"
    
    # Verify mocks were called with correct parameters
    mock_penpot_api.get_file.assert_called_once_with(file_id="file1")
    mock_get_subtree.assert_called_once_with(
        mock_penpot_api.get_file.return_value,
        "obj1",
        include_fields=["id", "type", "name"],
        depth=-1
    )


@patch('penpot_mcp.tools.penpot_tree.get_object_subtree_with_fields')
def test_get_object_tree_with_yaml_and_image(mock_get_subtree, mock_penpot_api):
    """Test the get_object_tree tool handler with YAML format and image inclusion (always included now)."""
    # Setup the mock get_object_subtree_with_fields function
    mock_get_subtree.return_value = {
        "tree": {
            "id": "obj1",
            "type": "frame",
            "name": "Test Object",
            "children": []
        },
        "page_id": "page1"
    }
    
    # Setup the export_object mock for the included image
    export_object_mock = MagicMock()
    export_object_mock.return_value = MagicMock(data=b'test_image_data', format='png')
    
    # Create a callable that matches what would be registered
    def get_object_tree(
        file_id: str, 
        object_id: str, 
        fields: list,  # Now required parameter
        depth: int = -1,
        format: str = "json"
    ):
        try:
            # Get the file data
            file_data = mock_penpot_api.get_file(file_id=file_id)
            
            # Use the mocked utility function
            result = mock_get_subtree(
                file_data, 
                object_id, 
                include_fields=fields,
                depth=depth
            )
            
            # Extract the tree and page_id
            simplified_tree = result["tree"]
            page_id = result["page_id"]
            
            # Prepare the result dictionary
            final_result = {"tree": simplified_tree}
            
            # Always include image (no longer optional)
            try:
                image = export_object_mock(
                    file_id=file_id,
                    page_id=page_id,
                    object_id=object_id
                )
                # New format: URI-based instead of base64 data
                image_id = hashlib.md5(f"{file_id}:{object_id}".encode()).hexdigest()
                image_uri = f"render_component://{image_id}"
                final_result["image"] = {
                    "uri": image_uri,
                    "format": image.format if hasattr(image, 'format') else "png"
                }
            except Exception as e:
                final_result["image_error"] = str(e)
            
            # Format the tree as YAML if requested
            if format.lower() == "yaml":
                try:
                    # Convert the entire result to YAML, including the image if present
                    yaml_result = yaml.dump(final_result, default_flow_style=False, sort_keys=False)
                    return {"yaml_result": yaml_result}
                except Exception as e:
                    return {"format_error": f"Error formatting as YAML: {str(e)}"}
            
            # Return the JSON format result
            return final_result
        except Exception as e:
            return {"error": str(e)}
    
    # Call the handler with required fields parameter and YAML format
    result = get_object_tree(
        file_id="file1", 
        object_id="obj1", 
        fields=["id", "type", "name"],  # Updated parameter name
        format="yaml"
    )
    
    # Check the result
    assert isinstance(result, dict)
    assert "yaml_result" in result
    assert "tree" not in result  # Should not contain the tree field directly
    
    # Verify the YAML content contains both tree and image with URI
    parsed_yaml = yaml.safe_load(result["yaml_result"])
    assert "tree" in parsed_yaml
    assert parsed_yaml["tree"]["id"] == "obj1"
    assert parsed_yaml["tree"]["type"] == "frame"
    assert parsed_yaml["tree"]["name"] == "Test Object"
    assert "image" in parsed_yaml
    assert "uri" in parsed_yaml["image"]
    
    # Verify the URI format in the YAML
    assert parsed_yaml["image"]["uri"].startswith("render_component://")
    assert parsed_yaml["image"]["format"] == "png"
    
    # Verify mocks were called with correct parameters
    mock_penpot_api.get_file.assert_called_once_with(file_id="file1")
    mock_get_subtree.assert_called_once_with(
        mock_penpot_api.get_file.return_value,
        "obj1",
        include_fields=["id", "type", "name"],
        depth=-1
    )


def test_rendered_component_resource():
    """Test the rendered component resource handler."""
    server = PenpotMCPServer(test_mode=True)

    component_id = "test_component_id"
    mock_image = MagicMock()
    mock_image.format = "png"

    # Mock the rendered_components dictionary
    server.rendered_components = {component_id: mock_image}

    # Get the resource handler function dynamically (this is tricky in real usage)
    # For testing, we'll implement the function directly based on the code
    def get_rendered_component(component_id: str):
        if component_id in server.rendered_components:
            return server.rendered_components[component_id]
        raise Exception(f"Component with ID {component_id} not found")

    # Test with a valid component ID
    result = get_rendered_component(component_id)
    assert result == mock_image

    # Test with an invalid component ID
    with pytest.raises(Exception) as excinfo:
        get_rendered_component("invalid_id")
    assert "not found" in str(excinfo.value)


def test_search_object_basic(mock_penpot_api):
    """Test the search_object tool basic functionality."""
    # Mock the file contents with more detailed mock data
    mock_file_data = {
        "id": "file1",
        "name": "Test File",
        "pagesIndex": {
            "page1": {
                "id": "page1",
                "name": "Page 1",
                "objects": {
                    "obj1": {"id": "obj1", "name": "Button Component", "type": "frame"},
                    "obj2": {"id": "obj2", "name": "Header Text", "type": "text"},
                    "obj3": {"id": "obj3", "name": "Button Label", "type": "text"}
                }
            },
            "page2": {
                "id": "page2",
                "name": "Page 2",
                "objects": {
                    "obj4": {"id": "obj4", "name": "Footer Button", "type": "frame"},
                    "obj5": {"id": "obj5", "name": "Copyright Text", "type": "text"}
                }
            }
        }
    }
    
    # Override the get_file return value for this test
    mock_penpot_api.get_file.return_value = mock_file_data
    
    # Create a function to simulate the search_object tool
    def get_cached_file(file_id):
        # Call the mock API to ensure it's tracked for assertions
        return mock_penpot_api.get_file(file_id=file_id)
    
    def search_object(file_id: str, query: str):
        try:
            # Get the file data using cache
            file_data = get_cached_file(file_id)
            if "error" in file_data:
                return file_data
            
            # Create case-insensitive pattern for matching
            import re
            pattern = re.compile(query, re.IGNORECASE)
            
            # Store matching objects
            matches = []
            
            # Search through each page in the file
            for page_id, page_data in file_data.get('pagesIndex', {}).items():
                page_name = page_data.get('name', 'Unnamed')
                
                # Search through objects in this page
                for obj_id, obj_data in page_data.get('objects', {}).items():
                    obj_name = obj_data.get('name', '')
                    
                    # Check if the name contains the query (case-insensitive)
                    if pattern.search(obj_name):
                        matches.append({
                            'id': obj_id,
                            'name': obj_name,
                            'page_id': page_id,
                            'page_name': page_name,
                            'object_type': obj_data.get('type', 'unknown')
                        })
            
            return {'objects': matches}
        except Exception as e:
            return {"error": str(e)}
    
    # Test searching for "button" (should find 3 objects)
    result = search_object("file1", "button")
    assert "objects" in result
    assert len(result["objects"]) == 3
    
    # Check the first match
    button_matches = [obj for obj in result["objects"] if "Button Component" == obj["name"]]
    assert len(button_matches) == 1
    assert button_matches[0]["id"] == "obj1"
    assert button_matches[0]["page_id"] == "page1"
    assert button_matches[0]["page_name"] == "Page 1"
    assert button_matches[0]["object_type"] == "frame"
    
    # Check that it found objects across pages
    footer_button_matches = [obj for obj in result["objects"] if "Footer Button" == obj["name"]]
    assert len(footer_button_matches) == 1
    assert footer_button_matches[0]["page_id"] == "page2"
    
    # Verify API was called with correct parameters
    mock_penpot_api.get_file.assert_called_with(file_id="file1")


def test_search_object_case_insensitive(mock_penpot_api):
    """Test the search_object tool with case-insensitive search."""
    # Mock the file contents with more detailed mock data
    mock_file_data = {
        "id": "file1",
        "name": "Test File",
        "pagesIndex": {
            "page1": {
                "id": "page1",
                "name": "Page 1",
                "objects": {
                    "obj1": {"id": "obj1", "name": "Button Component", "type": "frame"},
                    "obj2": {"id": "obj2", "name": "HEADER TEXT", "type": "text"},
                    "obj3": {"id": "obj3", "name": "button Label", "type": "text"}
                }
            }
        }
    }
    
    # Override the get_file return value for this test
    mock_penpot_api.get_file.return_value = mock_file_data
    
    # Create a function to simulate the search_object tool
    def get_cached_file(file_id):
        # Call the mock API to ensure it's tracked for assertions
        return mock_penpot_api.get_file(file_id=file_id)
    
    def search_object(file_id: str, query: str):
        try:
            # Get the file data using cache
            file_data = get_cached_file(file_id)
            if "error" in file_data:
                return file_data
            
            # Create case-insensitive pattern for matching
            import re
            pattern = re.compile(query, re.IGNORECASE)
            
            # Store matching objects
            matches = []
            
            # Search through each page in the file
            for page_id, page_data in file_data.get('pagesIndex', {}).items():
                page_name = page_data.get('name', 'Unnamed')
                
                # Search through objects in this page
                for obj_id, obj_data in page_data.get('objects', {}).items():
                    obj_name = obj_data.get('name', '')
                    
                    # Check if the name contains the query (case-insensitive)
                    if pattern.search(obj_name):
                        matches.append({
                            'id': obj_id,
                            'name': obj_name,
                            'page_id': page_id,
                            'page_name': page_name,
                            'object_type': obj_data.get('type', 'unknown')
                        })
            
            return {'objects': matches}
        except Exception as e:
            return {"error": str(e)}
    
    # Test with lowercase query for uppercase text
    result = search_object("file1", "header")
    assert "objects" in result
    assert len(result["objects"]) == 1
    assert result["objects"][0]["name"] == "HEADER TEXT"
    
    # Test with uppercase query for lowercase text
    result = search_object("file1", "BUTTON")
    assert "objects" in result
    assert len(result["objects"]) == 2
    
    # Check mixed case matching
    button_matches = sorted([obj["name"] for obj in result["objects"]])
    assert button_matches == ["Button Component", "button Label"]
    
    # Verify API was called
    mock_penpot_api.get_file.assert_called_with(file_id="file1")


def test_search_object_no_matches(mock_penpot_api):
    """Test the search_object tool when no matches are found."""
    # Mock the file contents
    mock_file_data = {
        "id": "file1",
        "name": "Test File",
        "pagesIndex": {
            "page1": {
                "id": "page1",
                "name": "Page 1",
                "objects": {
                    "obj1": {"id": "obj1", "name": "Button Component", "type": "frame"},
                    "obj2": {"id": "obj2", "name": "Header Text", "type": "text"}
                }
            }
        }
    }
    
    # Override the get_file return value for this test
    mock_penpot_api.get_file.return_value = mock_file_data
    
    # Create a function to simulate the search_object tool
    def get_cached_file(file_id):
        # Call the mock API to ensure it's tracked for assertions
        return mock_penpot_api.get_file(file_id=file_id)
    
    def search_object(file_id: str, query: str):
        try:
            # Get the file data using cache
            file_data = get_cached_file(file_id)
            if "error" in file_data:
                return file_data
            
            # Create case-insensitive pattern for matching
            import re
            pattern = re.compile(query, re.IGNORECASE)
            
            # Store matching objects
            matches = []
            
            # Search through each page in the file
            for page_id, page_data in file_data.get('pagesIndex', {}).items():
                page_name = page_data.get('name', 'Unnamed')
                
                # Search through objects in this page
                for obj_id, obj_data in page_data.get('objects', {}).items():
                    obj_name = obj_data.get('name', '')
                    
                    # Check if the name contains the query (case-insensitive)
                    if pattern.search(obj_name):
                        matches.append({
                            'id': obj_id,
                            'name': obj_name,
                            'page_id': page_id,
                            'page_name': page_name,
                            'object_type': obj_data.get('type', 'unknown')
                        })
            
            return {'objects': matches}
        except Exception as e:
            return {"error": str(e)}
    
    # Test with a query that won't match anything
    result = search_object("file1", "nonexistent")
    assert "objects" in result
    assert len(result["objects"]) == 0  # Empty array
    
    # Verify API was called
    mock_penpot_api.get_file.assert_called_with(file_id="file1")


def test_search_object_error_handling(mock_penpot_api):
    """Test the search_object tool error handling."""
    # Make the API throw an exception
    mock_penpot_api.get_file.side_effect = Exception("API error")
    
    def get_cached_file(file_id):
        try:
            return mock_penpot_api.get_file(file_id=file_id)
        except Exception as e:
            return {"error": str(e)}
    
    def search_object(file_id: str, query: str):
        try:
            # Get the file data using cache
            file_data = get_cached_file(file_id)
            if "error" in file_data:
                return file_data
            
            # Create case-insensitive pattern for matching
            import re
            pattern = re.compile(query, re.IGNORECASE)
            
            # Store matching objects
            matches = []
            
            # Search through each page in the file
            for page_id, page_data in file_data.get('pagesIndex', {}).items():
                page_name = page_data.get('name', 'Unnamed')
                
                # Search through objects in this page
                for obj_id, obj_data in page_data.get('objects', {}).items():
                    obj_name = obj_data.get('name', '')
                    
                    # Check if the name contains the query (case-insensitive)
                    if pattern.search(obj_name):
                        matches.append({
                            'id': obj_id,
                            'name': obj_name,
                            'page_id': page_id,
                            'page_name': page_name,
                            'object_type': obj_data.get('type', 'unknown')
                        })
            
            return {'objects': matches}
        except Exception as e:
            return {"error": str(e)}
    
    # Test with error from API
    result = search_object("file1", "button")
    assert "error" in result
    assert "API error" in result["error"]
    
    # Verify API was called
    mock_penpot_api.get_file.assert_called_with(file_id="file1")
