"""
Extended tests for PageLayout to improve coverage
Tests rendering logic, snapping, and edge cases
"""

import pytest
from unittest.mock import Mock, MagicMock, patch, call
from pyPhotoAlbum.page_layout import PageLayout, GridLayout
from pyPhotoAlbum.models import ImageData, PlaceholderData, TextBoxData
from pyPhotoAlbum.snapping import SnappingSystem


class TestPageLayoutRendering:
    """Test PageLayout rendering methods"""

    @patch("pyPhotoAlbum.page_layout.glDisable")
    @patch("pyPhotoAlbum.page_layout.glEnable")
    @patch("pyPhotoAlbum.page_layout.glColor3f")
    @patch("pyPhotoAlbum.page_layout.glColor4f")
    @patch("pyPhotoAlbum.page_layout.glBegin")
    @patch("pyPhotoAlbum.page_layout.glEnd")
    @patch("pyPhotoAlbum.page_layout.glVertex2f")
    @patch("pyPhotoAlbum.page_layout.glLineWidth")
    @patch("pyPhotoAlbum.page_layout.glBlendFunc")
    def test_render_basic_empty_page(
        self,
        mock_blend,
        mock_linewidth,
        mock_vertex,
        mock_end,
        mock_begin,
        mock_color4f,
        mock_color3f,
        mock_enable,
        mock_disable,
    ):
        """Test rendering empty page calls OpenGL functions correctly"""
        layout = PageLayout(width=210, height=297)

        # Render with default DPI
        layout.render(dpi=300)

        # Verify OpenGL depth test was disabled then re-enabled
        assert mock_disable.call_count >= 1
        assert mock_enable.call_count >= 1

        # Verify colors were set (shadow, background, border)
        assert mock_color3f.call_count >= 3

        # Verify vertices were drawn (quads for shadow, background, line loop for border)
        assert mock_vertex.call_count >= 8

    @patch("pyPhotoAlbum.page_layout.glDisable")
    @patch("pyPhotoAlbum.page_layout.glEnable")
    @patch("pyPhotoAlbum.page_layout.glColor3f")
    @patch("pyPhotoAlbum.page_layout.glColor4f")
    @patch("pyPhotoAlbum.page_layout.glBegin")
    @patch("pyPhotoAlbum.page_layout.glEnd")
    @patch("pyPhotoAlbum.page_layout.glVertex2f")
    @patch("pyPhotoAlbum.page_layout.glLineWidth")
    @patch("pyPhotoAlbum.page_layout.glBlendFunc")
    def test_render_facing_page_draws_center_line(
        self,
        mock_blend,
        mock_linewidth,
        mock_vertex,
        mock_end,
        mock_begin,
        mock_color4f,
        mock_color3f,
        mock_enable,
        mock_disable,
    ):
        """Test rendering facing page draws center divider line"""
        layout = PageLayout(width=210, height=297, is_facing_page=True)

        layout.render(dpi=300)

        # Verify line width was set for center line
        assert any(call(1.5) in mock_linewidth.call_args_list for call in mock_linewidth.call_args_list)

    @patch("pyPhotoAlbum.page_layout.glDisable")
    @patch("pyPhotoAlbum.page_layout.glEnable")
    @patch("pyPhotoAlbum.page_layout.glColor3f")
    @patch("pyPhotoAlbum.page_layout.glColor4f")
    @patch("pyPhotoAlbum.page_layout.glBegin")
    @patch("pyPhotoAlbum.page_layout.glEnd")
    @patch("pyPhotoAlbum.page_layout.glVertex2f")
    @patch("pyPhotoAlbum.page_layout.glLineWidth")
    @patch("pyPhotoAlbum.page_layout.glBlendFunc")
    def test_render_with_elements_calls_element_render(
        self,
        mock_blend,
        mock_linewidth,
        mock_vertex,
        mock_end,
        mock_begin,
        mock_color4f,
        mock_color3f,
        mock_enable,
        mock_disable,
    ):
        """Test rendering page with elements calls render on each element"""
        layout = PageLayout()

        # Add elements with mock render methods
        elem1 = ImageData(image_path="test1.jpg", x=10, y=20)
        elem2 = PlaceholderData(x=30, y=40)
        elem3 = TextBoxData(text_content="Hello", x=50, y=60)

        elem1.render = Mock()
        elem2.render = Mock()
        elem3.render = Mock()

        layout.add_element(elem1)
        layout.add_element(elem2)
        layout.add_element(elem3)

        layout.render(dpi=300)

        # Verify each element's render was called
        elem1.render.assert_called_once()
        elem2.render.assert_called_once()
        elem3.render.assert_called_once()

    @patch("pyPhotoAlbum.page_layout.glDisable")
    @patch("pyPhotoAlbum.page_layout.glEnable")
    @patch("pyPhotoAlbum.page_layout.glColor3f")
    @patch("pyPhotoAlbum.page_layout.glColor4f")
    @patch("pyPhotoAlbum.page_layout.glBegin")
    @patch("pyPhotoAlbum.page_layout.glEnd")
    @patch("pyPhotoAlbum.page_layout.glVertex2f")
    @patch("pyPhotoAlbum.page_layout.glLineWidth")
    @patch("pyPhotoAlbum.page_layout.glBlendFunc")
    def test_render_with_image_async_loading_requested(
        self,
        mock_blend,
        mock_linewidth,
        mock_vertex,
        mock_end,
        mock_begin,
        mock_color4f,
        mock_color3f,
        mock_enable,
        mock_disable,
    ):
        """Test rendering image without texture triggers async load request"""
        layout = PageLayout()

        # Create mock parent widget with async loader
        mock_parent = Mock()
        mock_parent.async_image_loader = Mock()
        mock_parent.request_image_load = Mock()
        layout._parent_widget = mock_parent

        # Add image element without texture
        elem = ImageData(image_path="test.jpg", x=10, y=20)
        elem.render = Mock()
        elem._async_load_requested = False
        layout.add_element(elem)

        layout.render(dpi=300)

        # Verify async load was requested
        assert elem._async_load_requested is True
        assert elem._async_loading is True
        mock_parent.request_image_load.assert_called_once()

    @patch("pyPhotoAlbum.page_layout.glDisable")
    @patch("pyPhotoAlbum.page_layout.glEnable")
    @patch("pyPhotoAlbum.page_layout.glColor3f")
    @patch("pyPhotoAlbum.page_layout.glColor4f")
    @patch("pyPhotoAlbum.page_layout.glBegin")
    @patch("pyPhotoAlbum.page_layout.glEnd")
    @patch("pyPhotoAlbum.page_layout.glVertex2f")
    @patch("pyPhotoAlbum.page_layout.glLineWidth")
    @patch("pyPhotoAlbum.page_layout.glBlendFunc")
    def test_render_with_different_dpi_scales_correctly(
        self,
        mock_blend,
        mock_linewidth,
        mock_vertex,
        mock_end,
        mock_begin,
        mock_color4f,
        mock_color3f,
        mock_enable,
        mock_disable,
    ):
        """Test rendering with different DPI values"""
        layout = PageLayout(width=210, height=297)

        # Render with different DPIs
        layout.render(dpi=72)
        layout.render(dpi=150)
        layout.render(dpi=600)

        # Should not raise any errors
        assert mock_vertex.call_count > 0


class TestPageLayoutSnapLines:
    """Test snap line rendering"""

    @patch("pyPhotoAlbum.page_layout.glDisable")
    @patch("pyPhotoAlbum.page_layout.glEnable")
    @patch("pyPhotoAlbum.page_layout.glColor3f")
    @patch("pyPhotoAlbum.page_layout.glColor4f")
    @patch("pyPhotoAlbum.page_layout.glBegin")
    @patch("pyPhotoAlbum.page_layout.glEnd")
    @patch("pyPhotoAlbum.page_layout.glVertex2f")
    @patch("pyPhotoAlbum.page_layout.glLineWidth")
    @patch("pyPhotoAlbum.page_layout.glBlendFunc")
    def test_render_snap_lines_with_project_settings(
        self,
        mock_blend,
        mock_linewidth,
        mock_vertex,
        mock_end,
        mock_begin,
        mock_color4f,
        mock_color3f,
        mock_enable,
        mock_disable,
    ):
        """Test snap lines use project settings when available"""
        layout = PageLayout()

        # Create mock project with global snapping settings
        mock_project = Mock()
        mock_project.snap_to_grid = True
        mock_project.snap_to_edges = True
        mock_project.snap_to_guides = True
        mock_project.grid_size_mm = 10.0
        mock_project.snap_threshold_mm = 5.0
        mock_project.show_grid = True
        mock_project.show_snap_lines = True

        layout.render(dpi=300, project=mock_project)

        # Verify blending was enabled for transparent lines
        # Check that glEnable was called (for GL_BLEND)
        assert mock_enable.call_count > 0

    @patch("pyPhotoAlbum.page_layout.glDisable")
    @patch("pyPhotoAlbum.page_layout.glEnable")
    @patch("pyPhotoAlbum.page_layout.glColor3f")
    @patch("pyPhotoAlbum.page_layout.glColor4f")
    @patch("pyPhotoAlbum.page_layout.glBegin")
    @patch("pyPhotoAlbum.page_layout.glEnd")
    @patch("pyPhotoAlbum.page_layout.glVertex2f")
    @patch("pyPhotoAlbum.page_layout.glLineWidth")
    @patch("pyPhotoAlbum.page_layout.glBlendFunc")
    def test_render_snap_lines_fallback_to_local_settings(
        self,
        mock_blend,
        mock_linewidth,
        mock_vertex,
        mock_end,
        mock_begin,
        mock_color4f,
        mock_color3f,
        mock_enable,
        mock_disable,
    ):
        """Test snap lines fall back to local settings when no project"""
        layout = PageLayout()
        layout.snapping_system.snap_to_grid = True
        layout.snapping_system.grid_size_mm = 15.0

        # Render without project
        layout.render(dpi=300, project=None)

        # Should complete without error
        assert mock_vertex.call_count > 0

    @patch("pyPhotoAlbum.page_layout.glDisable")
    @patch("pyPhotoAlbum.page_layout.glEnable")
    @patch("pyPhotoAlbum.page_layout.glColor3f")
    @patch("pyPhotoAlbum.page_layout.glColor4f")
    @patch("pyPhotoAlbum.page_layout.glBegin")
    @patch("pyPhotoAlbum.page_layout.glEnd")
    @patch("pyPhotoAlbum.page_layout.glVertex2f")
    @patch("pyPhotoAlbum.page_layout.glLineWidth")
    @patch("pyPhotoAlbum.page_layout.glBlendFunc")
    def test_render_snap_lines_with_guides(
        self,
        mock_blend,
        mock_linewidth,
        mock_vertex,
        mock_end,
        mock_begin,
        mock_color4f,
        mock_color3f,
        mock_enable,
        mock_disable,
    ):
        """Test rendering snap lines with custom guides"""
        layout = PageLayout()

        # Add custom guides using the proper method (position, orientation)
        layout.snapping_system.add_guide(100, "vertical")
        layout.snapping_system.add_guide(150, "horizontal")

        mock_project = Mock()
        mock_project.snap_to_grid = False
        mock_project.snap_to_edges = False
        mock_project.snap_to_guides = True
        mock_project.grid_size_mm = 10.0
        mock_project.snap_threshold_mm = 5.0
        mock_project.show_grid = False
        mock_project.show_snap_lines = True

        layout.render(dpi=300, project=mock_project)

        # Cyan color should be used for guides (0.0, 0.7, 0.9)
        # Check if cyan color was set (at least once)
        cyan_calls = [call for call in mock_color3f.call_args_list if call[0] == (0.0, 0.7, 0.9)]
        assert len(cyan_calls) > 0, "Cyan color for guides should be set"


class TestPageLayoutEdgeCases:
    """Test edge cases and error conditions"""

    def test_page_layout_with_zero_dimensions(self):
        """Test page layout with very small dimensions"""
        layout = PageLayout(width=0.1, height=0.1)
        assert layout.size == (0.1, 0.1)

    def test_page_layout_with_large_dimensions(self):
        """Test page layout with very large dimensions"""
        layout = PageLayout(width=10000, height=10000)
        assert layout.size == (10000, 10000)

    def test_multiple_grid_layout_changes(self):
        """Test changing grid layout multiple times"""
        layout = PageLayout()

        grid1 = GridLayout(rows=2, columns=2)
        grid2 = GridLayout(rows=3, columns=3)
        grid3 = GridLayout(rows=4, columns=4)

        layout.set_grid_layout(grid1)
        assert layout.grid_layout == grid1

        layout.set_grid_layout(grid2)
        assert layout.grid_layout == grid2

        layout.set_grid_layout(grid3)
        assert layout.grid_layout == grid3

    def test_deserialize_with_missing_snapping_system(self):
        """Test deserialization handles missing snapping_system gracefully"""
        layout = PageLayout()
        data = {
            "size": (210, 297),
            "elements": [],
            # No snapping_system key
        }

        layout.deserialize(data)

        # Should use existing snapping system
        assert layout.snapping_system is not None

    def test_deserialize_element_with_invalid_crop_info(self):
        """Test deserialization handles elements with missing optional fields"""
        layout = PageLayout()
        data = {
            "size": (210, 297),
            "elements": [
                {
                    "type": "image",
                    "image_path": "test.jpg",
                    "position": (10, 20),
                    "size": (100, 100),
                    "rotation": 0,
                    "z_index": 0,
                    # Missing crop_info - should use default
                }
            ],
        }

        layout.deserialize(data)

        assert len(layout.elements) == 1
        assert isinstance(layout.elements[0], ImageData)

    def test_element_z_index_handling(self):
        """Test that elements maintain correct z-order"""
        layout = PageLayout()

        elem1 = ImageData(image_path="test1.jpg", x=10, y=20)
        elem2 = ImageData(image_path="test2.jpg", x=30, y=40)
        elem3 = ImageData(image_path="test3.jpg", x=50, y=60)

        elem1.z_index = 10
        elem2.z_index = 5
        elem3.z_index = 15

        layout.add_element(elem1)
        layout.add_element(elem2)
        layout.add_element(elem3)

        # Elements added in order, z_index is just metadata
        assert layout.elements[0] == elem1
        assert layout.elements[1] == elem2
        assert layout.elements[2] == elem3


class TestGridLayoutEdgeCases:
    """Test GridLayout edge cases"""

    def test_grid_with_single_cell(self):
        """Test grid layout with single cell (1x1)"""
        grid = GridLayout(rows=1, columns=1, spacing=5.0)

        pos = grid.get_cell_position(0, 0, page_width=100, page_height=100)
        size = grid.get_cell_size(page_width=100, page_height=100)

        assert pos == (5.0, 5.0)
        # (100 - 10) / 1 = 90
        assert size == (90.0, 90.0)

    def test_grid_with_no_spacing(self):
        """Test grid layout with zero spacing"""
        grid = GridLayout(rows=2, columns=2, spacing=0.0)

        pos = grid.get_cell_position(0, 0, page_width=100, page_height=100)
        size = grid.get_cell_size(page_width=100, page_height=100)

        assert pos == (0.0, 0.0)
        assert size == (50.0, 50.0)

    def test_grid_with_large_spacing(self):
        """Test grid layout with large spacing"""
        grid = GridLayout(rows=2, columns=2, spacing=20.0)

        size = grid.get_cell_size(page_width=200, page_height=200)

        # (200 - 60) / 2 = 70
        assert size == (70.0, 70.0)

    def test_grid_merge_multiple_cells(self):
        """Test merging many cells"""
        grid = GridLayout(rows=5, columns=5)

        for row in range(3):
            for col in range(3):
                grid.merge_cells(row, col)

        assert len(grid.merged_cells) == 9

    def test_grid_cell_position_at_boundaries(self):
        """Test cell positions at grid boundaries"""
        grid = GridLayout(rows=3, columns=3, spacing=10.0)

        # Top-left corner
        pos_tl = grid.get_cell_position(0, 0, page_width=300, page_height=300)
        assert pos_tl[0] == 10.0
        assert pos_tl[1] == 10.0

        # Bottom-right corner (2, 2)
        pos_br = grid.get_cell_position(2, 2, page_width=300, page_height=300)
        # (300 - 40) / 3 = 86.67, x = 10 + 2 * (86.67 + 10) = 203.33
        assert pos_br[0] == pytest.approx(203.333, rel=0.01)
        assert pos_br[1] == pytest.approx(203.333, rel=0.01)


class TestPageLayoutSnappingIntegration:
    """Test integration with snapping system"""

    def test_snapping_system_initialized(self):
        """Test that PageLayout initializes with snapping system"""
        layout = PageLayout()
        assert isinstance(layout.snapping_system, SnappingSystem)

    def test_snapping_system_serialization(self):
        """Test that snapping system is included in serialization"""
        layout = PageLayout()
        layout.snapping_system.snap_to_grid = True
        layout.snapping_system.grid_size_mm = 20.0

        data = layout.serialize()

        assert "snapping_system" in data
        assert data["snapping_system"]["snap_to_grid"] is True
        assert data["snapping_system"]["grid_size_mm"] == 20.0

    def test_snapping_system_deserialization(self):
        """Test that snapping system is restored from serialization"""
        layout = PageLayout()

        # First create a layout with snapping settings
        layout.snapping_system.snap_to_grid = True
        layout.snapping_system.snap_to_edges = False
        layout.snapping_system.snap_to_guides = True
        layout.snapping_system.grid_size_mm = 25.0
        layout.snapping_system.snap_threshold_mm = 3.0
        layout.snapping_system.add_guide(100, "vertical")
        layout.snapping_system.add_guide(150, "horizontal")

        # Serialize and deserialize
        data = layout.serialize()
        restored = PageLayout()
        restored.deserialize(data)

        assert restored.snapping_system.snap_to_grid is True
        assert restored.snapping_system.snap_to_edges is False
        assert restored.snapping_system.snap_to_guides is True
        assert restored.snapping_system.grid_size_mm == 25.0
        assert restored.snapping_system.snap_threshold_mm == 3.0
        assert len(restored.snapping_system.guides) == 2


class TestPageLayoutBackwardCompatibility:
    """Test backward compatibility with older data formats"""

    def test_deserialize_without_base_width(self):
        """Test deserializing data without base_width field"""
        layout = PageLayout()
        data = {
            "size": (200, 280),
            # No base_width field
            "elements": [],
        }

        layout.deserialize(data)

        # Should default to width from size
        assert layout.base_width == 200

    def test_deserialize_without_is_facing_page(self):
        """Test deserializing data without is_facing_page field"""
        layout = PageLayout()
        data = {
            "size": (210, 297),
            # No is_facing_page field
            "elements": [],
        }

        layout.deserialize(data)

        # Should default to False
        assert layout.is_facing_page is False

    def test_deserialize_without_show_snap_lines(self):
        """Test deserializing data without show_snap_lines field"""
        layout = PageLayout()
        data = {
            "size": (210, 297),
            "elements": [],
            # No show_snap_lines field
        }

        layout.deserialize(data)

        # Should default to True
        assert layout.show_snap_lines is True


class TestPageLayoutComplexScenarios:
    """Test complex real-world scenarios"""

    def test_layout_with_many_elements(self):
        """Test layout with large number of elements"""
        layout = PageLayout()

        # Add 100 elements
        for i in range(100):
            elem = ImageData(image_path=f"test{i}.jpg", x=i * 10, y=i * 10)
            elem.z_index = i
            layout.add_element(elem)

        assert len(layout.elements) == 100

        # Serialize and deserialize
        data = layout.serialize()
        restored = PageLayout()
        restored.deserialize(data)

        assert len(restored.elements) == 100
        # Elements should be sorted by z_index
        for i in range(100):
            assert restored.elements[i].z_index == i

    def test_layout_with_mixed_element_types(self):
        """Test layout with all element types mixed"""
        layout = PageLayout()

        for i in range(10):
            if i % 3 == 0:
                elem = ImageData(image_path=f"img{i}.jpg", x=i * 20, y=i * 20)
            elif i % 3 == 1:
                elem = PlaceholderData(x=i * 20, y=i * 20)
            else:
                elem = TextBoxData(text_content=f"Text {i}", x=i * 20, y=i * 20)

            elem.z_index = i
            layout.add_element(elem)

        data = layout.serialize()
        restored = PageLayout()
        restored.deserialize(data)

        # Count element types
        images = sum(1 for e in restored.elements if isinstance(e, ImageData))
        placeholders = sum(1 for e in restored.elements if isinstance(e, PlaceholderData))
        textboxes = sum(1 for e in restored.elements if isinstance(e, TextBoxData))

        assert images == 4  # 0, 3, 6, 9
        assert placeholders == 3  # 1, 4, 7
        assert textboxes == 3  # 2, 5, 8

    @patch("pyPhotoAlbum.page_layout.glDisable")
    @patch("pyPhotoAlbum.page_layout.glEnable")
    @patch("pyPhotoAlbum.page_layout.glColor3f")
    @patch("pyPhotoAlbum.page_layout.glColor4f")
    @patch("pyPhotoAlbum.page_layout.glBegin")
    @patch("pyPhotoAlbum.page_layout.glEnd")
    @patch("pyPhotoAlbum.page_layout.glVertex2f")
    @patch("pyPhotoAlbum.page_layout.glLineWidth")
    @patch("pyPhotoAlbum.page_layout.glBlendFunc")
    def test_render_facing_page_with_elements(
        self,
        mock_blend,
        mock_linewidth,
        mock_vertex,
        mock_end,
        mock_begin,
        mock_color4f,
        mock_color3f,
        mock_enable,
        mock_disable,
    ):
        """Test rendering facing page with elements"""
        layout = PageLayout(width=210, height=297, is_facing_page=True)

        # Add elements on both sides
        left_elem = ImageData(image_path="left.jpg", x=50, y=100)
        right_elem = ImageData(image_path="right.jpg", x=250, y=100)

        left_elem.render = Mock()
        right_elem.render = Mock()

        layout.add_element(left_elem)
        layout.add_element(right_elem)

        layout.render(dpi=300)

        # Both elements should be rendered
        left_elem.render.assert_called_once()
        right_elem.render.assert_called_once()

        # Center line should be drawn
        assert any(call(1.5) in mock_linewidth.call_args_list for call in mock_linewidth.call_args_list)
