"""
Tests for ViewportMixin
"""

import pytest
from unittest.mock import Mock, MagicMock, patch
from PyQt6.QtWidgets import QApplication
from PyQt6.QtOpenGLWidgets import QOpenGLWidget
from pyPhotoAlbum.mixins.viewport import ViewportMixin
from pyPhotoAlbum.project import Project, Page
from pyPhotoAlbum.page_layout import PageLayout


# Create a minimal test widget class
class TestViewportWidget(ViewportMixin, QOpenGLWidget):
    """Test widget combining ViewportMixin with QOpenGLWidget"""

    pass


class TestViewportMixinInitialization:
    """Test ViewportMixin initialization"""

    def test_initialization_sets_defaults(self, qtbot):
        """Test that mixin initializes with correct defaults"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)

        assert widget.zoom_level == 1.0
        assert widget.pan_offset == [0, 0]
        assert widget.initial_zoom_set is False

    def test_zoom_level_is_mutable(self, qtbot):
        """Test that zoom level can be changed"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)

        widget.zoom_level = 1.5
        assert widget.zoom_level == 1.5

    def test_pan_offset_is_mutable(self, qtbot):
        """Test that pan offset can be changed"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)

        widget.pan_offset = [100, 50]
        assert widget.pan_offset == [100, 50]


class TestViewportCalculations:
    """Test viewport zoom calculations"""

    def test_calculate_fit_to_screen_no_project(self, qtbot):
        """Test fit-to-screen with no project returns 1.0"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)
        widget.resize(800, 600)

        # Mock window() to return a window without project
        mock_window = Mock()
        mock_window.project = None
        widget.window = Mock(return_value=mock_window)

        zoom = widget._calculate_fit_to_screen_zoom()
        assert zoom == 1.0

    def test_calculate_fit_to_screen_empty_project(self, qtbot):
        """Test fit-to-screen with empty project returns 1.0"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)
        widget.resize(800, 600)

        # Mock window() to return a window with empty project
        mock_window = Mock()
        mock_window.project = Project(name="Empty")
        mock_window.project.pages = []
        widget.window = Mock(return_value=mock_window)

        zoom = widget._calculate_fit_to_screen_zoom()
        assert zoom == 1.0

    def test_calculate_fit_to_screen_with_page(self, qtbot):
        """Test fit-to-screen calculates correct zoom for A4 page"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)
        widget.resize(1000, 800)

        # Mock window with project and A4 page
        mock_window = Mock()
        mock_window.project = Project(name="Test")
        mock_window.project.working_dpi = 96

        # A4 page: 210mm x 297mm
        page = Page(layout=PageLayout(width=210, height=297), page_number=1)
        mock_window.project.pages = [page]

        widget.window = Mock(return_value=mock_window)

        zoom = widget._calculate_fit_to_screen_zoom()

        # Calculate expected zoom
        # A4 at 96 DPI: width=794px, height=1123px
        # Window: 1000x800, margins: 100px each side
        # Available: 800x600
        # zoom_w = 800/794 ≈ 1.007, zoom_h = 600/1123 ≈ 0.534
        # Should use min(zoom_w, zoom_h, 1.0) = 0.534

        assert 0.5 < zoom < 0.6  # Approximately 0.534
        assert zoom <= 1.0  # Never zoom beyond 100%

    def test_calculate_fit_to_screen_small_window(self, qtbot):
        """Test fit-to-screen with small window returns small zoom"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)
        widget.resize(400, 300)  # Small window

        mock_window = Mock()
        mock_window.project = Project(name="Test")
        mock_window.project.working_dpi = 96

        page = Page(layout=PageLayout(width=210, height=297), page_number=1)
        mock_window.project.pages = [page]

        widget.window = Mock(return_value=mock_window)

        zoom = widget._calculate_fit_to_screen_zoom()

        # With 400x300 window and 200px margins, available space is 200x100
        # This should produce a very small zoom
        assert zoom < 0.3

    def test_calculate_fit_to_screen_large_window(self, qtbot):
        """Test fit-to-screen with large window caps at 1.0"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)
        widget.resize(3000, 2000)  # Very large window

        mock_window = Mock()
        mock_window.project = Project(name="Test")
        mock_window.project.working_dpi = 96

        page = Page(layout=PageLayout(width=210, height=297), page_number=1)
        mock_window.project.pages = [page]

        widget.window = Mock(return_value=mock_window)

        zoom = widget._calculate_fit_to_screen_zoom()

        # Even with huge window, zoom should not exceed 1.0
        assert zoom == 1.0

    def test_calculate_fit_to_screen_different_dpi(self, qtbot):
        """Test fit-to-screen respects different DPI values"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)
        widget.resize(1000, 800)

        mock_window = Mock()
        mock_window.project = Project(name="Test")
        mock_window.project.working_dpi = 300  # High DPI

        page = Page(layout=PageLayout(width=210, height=297), page_number=1)
        mock_window.project.pages = [page]

        widget.window = Mock(return_value=mock_window)

        zoom = widget._calculate_fit_to_screen_zoom()

        # At 300 DPI, page is much larger in pixels
        # So zoom should be smaller
        assert zoom < 0.3


class TestViewportCentering:
    """Test viewport centering calculations"""

    def test_calculate_center_pan_offset_no_project(self, qtbot):
        """Test center calculation with no project returns [0, 0]"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)
        widget.resize(800, 600)

        # Mock window() to return a window without project
        mock_window = Mock()
        mock_window.project = None
        widget.window = Mock(return_value=mock_window)

        offset = widget._calculate_center_pan_offset(1.0)
        assert offset == [0, 0]

    def test_calculate_center_pan_offset_empty_project(self, qtbot):
        """Test center calculation with empty project returns [0, 0]"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)
        widget.resize(800, 600)

        # Mock window() to return a window with empty project
        mock_window = Mock()
        mock_window.project = Project(name="Empty")
        mock_window.project.pages = []
        widget.window = Mock(return_value=mock_window)

        offset = widget._calculate_center_pan_offset(1.0)
        assert offset == [0, 0]

    def test_calculate_center_pan_offset_with_page_at_100_percent(self, qtbot):
        """Test center calculation for A4 page at 100% zoom"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)
        widget.resize(1000, 800)

        # Mock window with project and A4 page
        mock_window = Mock()
        mock_window.project = Project(name="Test")
        mock_window.project.working_dpi = 96

        # A4 page: 210mm x 297mm
        page = Page(layout=PageLayout(width=210, height=297), page_number=1)
        mock_window.project.pages = [page]

        widget.window = Mock(return_value=mock_window)

        zoom_level = 1.0
        offset = widget._calculate_center_pan_offset(zoom_level)

        # Calculate expected offset
        # A4 at 96 DPI: width=794px, height=1123px
        # Window: 1000x800
        # PAGE_MARGIN = 50
        # x_offset = (1000 - 794) / 2 - 50 = 103 - 50 = 53
        # y_offset = (800 - 1123) / 2 = -161.5

        assert isinstance(offset, list)
        assert len(offset) == 2
        # X offset should center horizontally (accounting for PAGE_MARGIN)
        assert 50 < offset[0] < 60  # Approximately 53
        # Y offset should be negative (page taller than window)
        assert offset[1] < 0

    def test_calculate_center_pan_offset_with_page_at_50_percent(self, qtbot):
        """Test center calculation for A4 page at 50% zoom"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)
        widget.resize(1000, 800)

        mock_window = Mock()
        mock_window.project = Project(name="Test")
        mock_window.project.working_dpi = 96

        # A4 page: 210mm x 297mm
        page = Page(layout=PageLayout(width=210, height=297), page_number=1)
        mock_window.project.pages = [page]

        widget.window = Mock(return_value=mock_window)

        zoom_level = 0.5
        offset = widget._calculate_center_pan_offset(zoom_level)

        # At 50% zoom, page dimensions are halved
        # A4 at 96 DPI and 50%: width=397px, height=561.5px
        # Window: 1000x800
        # PAGE_MARGIN = 50
        # x_offset = (1000 - 397) / 2 - 50 = 301.5 - 50 = 251.5
        # y_offset = (800 - 561.5) / 2 = 119.25

        assert isinstance(offset, list)
        assert len(offset) == 2
        # X offset should be larger (more centering space)
        assert 240 < offset[0] < 260  # Approximately 251.5
        # Y offset should be positive (page fits vertically)
        assert offset[1] > 100

    def test_calculate_center_pan_offset_small_page(self, qtbot):
        """Test center calculation for small page (6x4 photo)"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)
        widget.resize(1000, 800)

        mock_window = Mock()
        mock_window.project = Project(name="Test")
        mock_window.project.working_dpi = 96

        # 6x4 inch photo: 152.4mm x 101.6mm
        page = Page(layout=PageLayout(width=152.4, height=101.6), page_number=1)
        mock_window.project.pages = [page]

        widget.window = Mock(return_value=mock_window)

        zoom_level = 1.0
        offset = widget._calculate_center_pan_offset(zoom_level)

        # Small page should have large positive offsets (lots of centering space)
        assert offset[0] > 150  # Horizontally centered with room to spare
        assert offset[1] > 200  # Vertically centered with room to spare

    def test_calculate_center_pan_offset_large_window(self, qtbot):
        """Test center calculation with large window"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)
        widget.resize(3000, 2000)

        mock_window = Mock()
        mock_window.project = Project(name="Test")
        mock_window.project.working_dpi = 96

        # A4 page: 210mm x 297mm
        page = Page(layout=PageLayout(width=210, height=297), page_number=1)
        mock_window.project.pages = [page]

        widget.window = Mock(return_value=mock_window)

        zoom_level = 1.0
        offset = widget._calculate_center_pan_offset(zoom_level)

        # Large window should have large positive offsets
        assert offset[0] > 1000  # Lots of horizontal space
        assert offset[1] > 400  # Lots of vertical space

    def test_calculate_center_pan_offset_different_zoom_levels(self, qtbot):
        """Test that different zoom levels produce different offsets"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)
        widget.resize(1000, 800)

        mock_window = Mock()
        mock_window.project = Project(name="Test")
        mock_window.project.working_dpi = 96

        page = Page(layout=PageLayout(width=210, height=297), page_number=1)
        mock_window.project.pages = [page]

        widget.window = Mock(return_value=mock_window)

        # Get offsets at different zoom levels
        offset_100 = widget._calculate_center_pan_offset(1.0)
        offset_50 = widget._calculate_center_pan_offset(0.5)
        offset_25 = widget._calculate_center_pan_offset(0.25)

        # As zoom decreases, both offsets should increase (more centering space)
        assert offset_50[0] > offset_100[0]
        assert offset_25[0] > offset_50[0]
        # Y offset behavior depends on if page fits vertically
        # At lower zoom, page is smaller, so more vertical centering space
        assert offset_25[1] > offset_50[1]

    def test_calculate_center_pan_offset_different_dpi(self, qtbot):
        """Test center calculation with different DPI values"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)
        widget.resize(1000, 800)

        mock_window = Mock()
        mock_window.project = Project(name="Test")

        page = Page(layout=PageLayout(width=210, height=297), page_number=1)
        mock_window.project.pages = [page]

        # Test at 96 DPI
        mock_window.project.working_dpi = 96
        widget.window = Mock(return_value=mock_window)
        offset_96 = widget._calculate_center_pan_offset(1.0)

        # Test at 300 DPI (page will be much larger in pixels)
        mock_window.project.working_dpi = 300
        offset_300 = widget._calculate_center_pan_offset(1.0)

        # At higher DPI, page is larger, so centering offsets should be smaller
        # (or negative if page doesn't fit)
        assert offset_300[0] < offset_96[0]
        assert offset_300[1] < offset_96[1]


class TestViewportResizing:
    """Test viewport behavior during window resizing"""

    def test_resizeGL_recenters_when_project_loaded(self, qtbot):
        """Test that resizing window recenters the page"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)

        # Mock window with project
        mock_window = Mock()
        mock_window.project = Project(name="Test")
        mock_window.project.working_dpi = 96

        page = Page(layout=PageLayout(width=210, height=297), page_number=1)
        mock_window.project.pages = [page]
        widget.window = Mock(return_value=mock_window)

        # Simulate initial load (sets initial_zoom_set to True)
        widget.initial_zoom_set = True
        widget.zoom_level = 0.5

        # Get initial centered offset for 1000x800
        with patch.object(widget, "width", return_value=1000):
            with patch.object(widget, "height", return_value=800):
                initial_offset = list(widget._calculate_center_pan_offset(0.5))

        # Trigger a resize to larger window (1200x900)
        # Mock the widget's dimensions during resizeGL
        with patch.object(widget, "width", return_value=1200):
            with patch.object(widget, "height", return_value=900):
                widget.resizeGL(1200, 900)
                new_offset = widget.pan_offset

        # Offsets should be larger due to increased window size
        assert new_offset[0] > initial_offset[0]  # More horizontal space
        assert new_offset[1] > initial_offset[1]  # More vertical space

    def test_resizeGL_does_not_recenter_before_project_load(self, qtbot):
        """Test that resizing before project load doesn't change pan offset"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)
        widget.resize(800, 600)

        # Don't set initial_zoom_set (simulates no project loaded yet)
        widget.initial_zoom_set = False
        widget.pan_offset = [100, 50]

        # Trigger a resize
        widget.resizeGL(1000, 800)

        # Pan offset should remain unchanged (no project to center)
        assert widget.pan_offset == [100, 50]

    def test_resizeGL_maintains_zoom_level(self, qtbot):
        """Test that resizing maintains the current zoom level"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)
        widget.resize(1000, 800)

        # Mock window with project
        mock_window = Mock()
        mock_window.project = Project(name="Test")
        mock_window.project.working_dpi = 96

        page = Page(layout=PageLayout(width=210, height=297), page_number=1)
        mock_window.project.pages = [page]
        widget.window = Mock(return_value=mock_window)

        # Set initial state
        widget.initial_zoom_set = True
        widget.zoom_level = 0.75

        # Trigger a resize
        widget.resizeGL(1200, 900)

        # Zoom level should remain the same
        assert widget.zoom_level == 0.75

    def test_resizeGL_with_different_sizes(self, qtbot):
        """Test that resizing to different sizes produces appropriate centering"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)

        # Mock window with project
        mock_window = Mock()
        mock_window.project = Project(name="Test")
        mock_window.project.working_dpi = 96

        page = Page(layout=PageLayout(width=210, height=297), page_number=1)
        mock_window.project.pages = [page]
        widget.window = Mock(return_value=mock_window)

        widget.initial_zoom_set = True
        widget.zoom_level = 0.5

        # Resize to small window
        widget.resize(600, 400)
        widget.resizeGL(600, 400)
        small_offset = widget.pan_offset.copy()

        # Resize to large window
        widget.resize(2000, 1500)
        widget.resizeGL(2000, 1500)
        large_offset = widget.pan_offset.copy()

        # Larger window should have larger centering offsets
        assert large_offset[0] > small_offset[0]
        assert large_offset[1] > small_offset[1]


class TestViewportOpenGL:
    """Test OpenGL-related viewport methods"""

    def test_initializeGL_sets_clear_color(self, qtbot):
        """Test that initializeGL is callable (actual GL testing is integration)"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)

        # Just verify the method exists and is callable
        assert hasattr(widget, "initializeGL")
        assert callable(widget.initializeGL)

    def test_resizeGL_is_callable(self, qtbot):
        """Test that resizeGL is callable"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)

        assert hasattr(widget, "resizeGL")
        assert callable(widget.resizeGL)


class TestContentBounds:
    """Test get_content_bounds method"""

    def test_get_content_bounds_no_project(self, qtbot):
        """Test content bounds with no project returns defaults"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)
        widget.resize(800, 600)

        # Mock window without project
        mock_window = Mock()
        mock_window.project = None
        widget.window = Mock(return_value=mock_window)

        bounds = widget.get_content_bounds()

        assert bounds["min_x"] == 0
        assert bounds["max_x"] == 800
        assert bounds["min_y"] == 0
        assert bounds["max_y"] == 600
        assert bounds["width"] == 800
        assert bounds["height"] == 600

    def test_get_content_bounds_empty_project(self, qtbot):
        """Test content bounds with empty project returns defaults"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)
        widget.resize(800, 600)

        # Mock window with empty project
        mock_window = Mock()
        mock_window.project = Project(name="Empty")
        mock_window.project.pages = []
        widget.window = Mock(return_value=mock_window)

        bounds = widget.get_content_bounds()

        assert bounds["min_x"] == 0
        assert bounds["max_x"] == 800
        assert bounds["min_y"] == 0
        assert bounds["max_y"] == 600

    def test_get_content_bounds_single_page(self, qtbot):
        """Test content bounds with single A4 page"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)
        widget.resize(1000, 800)

        # Mock window with project and A4 page
        mock_window = Mock()
        mock_window.project = Project(name="Test")
        mock_window.project.working_dpi = 96

        # A4 page: 210mm x 297mm
        page = Page(layout=PageLayout(width=210, height=297), page_number=1)
        mock_window.project.pages = [page]

        widget.window = Mock(return_value=mock_window)
        widget.zoom_level = 1.0

        bounds = widget.get_content_bounds()

        # Should include page dimensions plus margins and spacing
        # A4 at 96 DPI: width=794px, height=1123px
        # Total width = 794 + (2 * 50 margin) = 894
        # Total height = 1123 + (50 margin top) + (50 margin bottom) + (50 spacing) = 1273

        assert bounds["min_x"] == 0
        assert bounds["min_y"] == 0
        assert 890 < bounds["width"] < 900  # ~894
        assert 1260 < bounds["height"] < 1280  # ~1273

    def test_get_content_bounds_multiple_pages(self, qtbot):
        """Test content bounds with multiple pages"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)
        widget.resize(1000, 800)

        # Mock window with project and 3 pages
        mock_window = Mock()
        mock_window.project = Project(name="Test")
        mock_window.project.working_dpi = 96

        # Create 3 A4 pages
        pages = [
            Page(layout=PageLayout(width=210, height=297), page_number=i)
            for i in range(1, 4)
        ]
        mock_window.project.pages = pages

        widget.window = Mock(return_value=mock_window)
        widget.zoom_level = 1.0

        bounds = widget.get_content_bounds()

        # 3 pages vertically: 3 * 1123 + margins + spacings
        # Height = 3369 + 50 (top margin) + 50 (bottom margin) + 3*50 (spacings) = 3619

        assert bounds["min_x"] == 0
        assert bounds["min_y"] == 0
        assert 890 < bounds["width"] < 900  # Same width as single page
        assert 3600 < bounds["height"] < 3640  # ~3619

    def test_get_content_bounds_with_zoom(self, qtbot):
        """Test content bounds respects zoom level"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)
        widget.resize(1000, 800)

        # Mock window with project
        mock_window = Mock()
        mock_window.project = Project(name="Test")
        mock_window.project.working_dpi = 96

        page = Page(layout=PageLayout(width=210, height=297), page_number=1)
        mock_window.project.pages = [page]

        widget.window = Mock(return_value=mock_window)

        # Test at 50% zoom
        widget.zoom_level = 0.5
        bounds_50 = widget.get_content_bounds()

        # Test at 100% zoom
        widget.zoom_level = 1.0
        bounds_100 = widget.get_content_bounds()

        # Test at 200% zoom
        widget.zoom_level = 2.0
        bounds_200 = widget.get_content_bounds()

        # Bounds should scale with zoom
        assert bounds_50["width"] < bounds_100["width"] < bounds_200["width"]
        assert bounds_50["height"] < bounds_100["height"] < bounds_200["height"]

    def test_get_content_bounds_different_page_sizes(self, qtbot):
        """Test content bounds with pages of different sizes"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)
        widget.resize(1000, 800)

        # Mock window with project
        mock_window = Mock()
        mock_window.project = Project(name="Test")
        mock_window.project.working_dpi = 96

        # Create pages of different sizes
        pages = [
            Page(layout=PageLayout(width=210, height=297), page_number=1),  # A4
            Page(layout=PageLayout(width=152.4, height=101.6), page_number=2),  # 6x4 photo
            Page(layout=PageLayout(width=210, height=297), page_number=3),  # A4
        ]
        mock_window.project.pages = pages

        widget.window = Mock(return_value=mock_window)
        widget.zoom_level = 1.0

        bounds = widget.get_content_bounds()

        # Width should be based on the widest page (A4)
        # Height should be sum of all pages
        assert bounds["min_x"] == 0
        assert bounds["min_y"] == 0
        assert 890 < bounds["width"] < 900  # Based on A4 width
        assert bounds["height"] > 2000  # Sum of all pages


class TestPanClamping:
    """Test clamp_pan_offset method"""

    def test_clamp_pan_offset_no_project(self, qtbot):
        """Test clamping with no project does nothing"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)
        widget.resize(800, 600)

        # Mock window without project
        mock_window = Mock()
        mock_window.project = None
        widget.window = Mock(return_value=mock_window)

        widget.pan_offset = [100, 50]
        widget.clamp_pan_offset()

        # Should remain unchanged
        assert widget.pan_offset == [100, 50]

    def test_clamp_pan_offset_empty_project(self, qtbot):
        """Test clamping with empty project does nothing"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)
        widget.resize(800, 600)

        # Mock window with empty project
        mock_window = Mock()
        mock_window.project = Project(name="Empty")
        mock_window.project.pages = []
        widget.window = Mock(return_value=mock_window)

        widget.pan_offset = [100, 50]
        widget.clamp_pan_offset()

        # Should remain unchanged
        assert widget.pan_offset == [100, 50]

    def test_clamp_pan_offset_vertical_limits_tall_content(self, qtbot):
        """Test vertical clamping when content is taller than viewport"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)
        widget.resize(1000, 600)  # Short viewport

        # Mock window with project and tall page
        mock_window = Mock()
        mock_window.project = Project(name="Test")
        mock_window.project.working_dpi = 96

        # A4 page at 100% zoom is 1123px tall
        page = Page(layout=PageLayout(width=210, height=297), page_number=1)
        mock_window.project.pages = [page]

        widget.window = Mock(return_value=mock_window)
        widget.zoom_level = 1.0

        # Try to pan way beyond top
        widget.pan_offset = [0, 1000]
        widget.clamp_pan_offset()

        # Should be clamped to top (max_pan_up = 0)
        assert widget.pan_offset[1] == 0

        # Try to pan way beyond bottom
        widget.pan_offset = [0, -2000]
        widget.clamp_pan_offset()

        # Should be clamped to bottom
        # min_pan_up = -(content_height - viewport_height)
        assert widget.pan_offset[1] > -2000
        assert widget.pan_offset[1] < 0

    def test_clamp_pan_offset_vertical_no_clamp_when_fits(self, qtbot):
        """Test vertical clamping preserves position when content fits"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)
        widget.resize(1000, 2000)  # Tall viewport

        # Mock window with project and small page
        mock_window = Mock()
        mock_window.project = Project(name="Test")
        mock_window.project.working_dpi = 96

        # Small page that fits in viewport
        page = Page(layout=PageLayout(width=152.4, height=101.6), page_number=1)
        mock_window.project.pages = [page]

        widget.window = Mock(return_value=mock_window)
        widget.zoom_level = 1.0

        # Set a specific pan offset
        widget.pan_offset = [0, 200]
        widget.clamp_pan_offset()

        # When content fits, offset should be preserved
        assert widget.pan_offset[1] == 200

    def test_clamp_pan_offset_horizontal_single_page(self, qtbot):
        """Test horizontal clamping for single page"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)
        widget.resize(1000, 800)

        # Mock window with project
        mock_window = Mock()
        mock_window.project = Project(name="Test")
        mock_window.project.working_dpi = 96

        # A4 page
        page = Page(layout=PageLayout(width=210, height=297), page_number=1)
        mock_window.project.pages = [page]

        widget.window = Mock(return_value=mock_window)
        widget.zoom_level = 1.0

        # Try to pan too far left
        widget.pan_offset = [1000, 0]
        original_x = widget.pan_offset[0]
        widget.clamp_pan_offset()

        # Should be clamped
        assert widget.pan_offset[0] < original_x

        # Try to pan too far right
        widget.pan_offset = [-1000, 0]
        original_x = widget.pan_offset[0]
        widget.clamp_pan_offset()

        # Should be clamped
        assert widget.pan_offset[0] > original_x

    def test_clamp_pan_offset_horizontal_multiple_pages(self, qtbot):
        """Test horizontal clamping with multiple pages"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)
        widget.resize(1000, 800)

        # Mock window with project
        mock_window = Mock()
        mock_window.project = Project(name="Test")
        mock_window.project.working_dpi = 96

        # Create multiple pages
        pages = [
            Page(layout=PageLayout(width=210, height=297), page_number=i)
            for i in range(1, 4)
        ]
        mock_window.project.pages = pages

        widget.window = Mock(return_value=mock_window)
        widget.zoom_level = 1.0

        # Set pan offset and clamp
        widget.pan_offset = [500, -200]
        widget.clamp_pan_offset()

        # Should be clamped within reasonable bounds
        assert -1000 < widget.pan_offset[0] < 1000

    def test_clamp_pan_offset_with_zoom_changes(self, qtbot):
        """Test clamping behavior at different zoom levels"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)
        widget.resize(1000, 800)

        # Mock window with project
        mock_window = Mock()
        mock_window.project = Project(name="Test")
        mock_window.project.working_dpi = 96

        page = Page(layout=PageLayout(width=210, height=297), page_number=1)
        mock_window.project.pages = [page]

        widget.window = Mock(return_value=mock_window)

        # Test at different zoom levels
        for zoom in [0.5, 1.0, 2.0]:
            widget.zoom_level = zoom
            widget.pan_offset = [500, -500]
            widget.clamp_pan_offset()

            # Should clamp appropriately for each zoom
            assert isinstance(widget.pan_offset[0], (int, float))
            assert isinstance(widget.pan_offset[1], (int, float))

    def test_clamp_pan_offset_preserves_original_pan_y(self, qtbot):
        """Test that clamping uses original pan_y for page selection"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)
        widget.resize(1000, 600)

        # Mock window with project
        mock_window = Mock()
        mock_window.project = Project(name="Test")
        mock_window.project.working_dpi = 96

        # Multiple pages
        pages = [
            Page(layout=PageLayout(width=210, height=297), page_number=i)
            for i in range(1, 3)
        ]
        mock_window.project.pages = pages

        widget.window = Mock(return_value=mock_window)
        widget.zoom_level = 1.0

        # Set pan offset that will be clamped vertically
        widget.pan_offset = [0, -800]
        original_y = widget.pan_offset[1]
        widget.clamp_pan_offset()

        # Horizontal clamping should have used original Y for page selection
        # even if vertical was clamped
        assert isinstance(widget.pan_offset[0], (int, float))
        assert isinstance(widget.pan_offset[1], (int, float))

    def test_clamp_pan_offset_different_page_widths(self, qtbot):
        """Test horizontal clamping with pages of different widths"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)
        widget.resize(1000, 800)

        # Mock window with project
        mock_window = Mock()
        mock_window.project = Project(name="Test")
        mock_window.project.working_dpi = 96

        # Pages of different widths
        pages = [
            Page(layout=PageLayout(width=210, height=297), page_number=1),  # A4
            Page(layout=PageLayout(width=152.4, height=101.6), page_number=2),  # 6x4
            Page(layout=PageLayout(width=210, height=297), page_number=3),  # A4
        ]
        mock_window.project.pages = pages

        widget.window = Mock(return_value=mock_window)
        widget.zoom_level = 1.0

        # Test clamping while viewing different pages
        widget.pan_offset = [200, 0]  # Viewing first page
        widget.clamp_pan_offset()
        first_page_x = widget.pan_offset[0]

        widget.pan_offset = [200, -800]  # Viewing second page
        widget.clamp_pan_offset()
        second_page_x = widget.pan_offset[0]

        # Both should be clamped appropriately
        assert isinstance(first_page_x, (int, float))
        assert isinstance(second_page_x, (int, float))

    def test_clamp_pan_offset_page_wider_than_viewport(self, qtbot):
        """Test horizontal clamping when page is wider than viewport"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)
        widget.resize(400, 600)  # Narrow viewport

        # Mock window with project
        mock_window = Mock()
        mock_window.project = Project(name="Test")
        mock_window.project.working_dpi = 96

        # A4 page is 794px wide at 96 DPI, wider than 400px viewport
        page = Page(layout=PageLayout(width=210, height=297), page_number=1)
        mock_window.project.pages = [page]

        widget.window = Mock(return_value=mock_window)
        widget.zoom_level = 1.0

        # Should allow panning to see different parts of the page
        widget.pan_offset = [100, 0]
        widget.clamp_pan_offset()

        # Should allow reasonable panning range
        assert -500 < widget.pan_offset[0] < 500

    def test_clamp_pan_offset_interpolation_between_pages(self, qtbot):
        """Test that horizontal clamping interpolates between page centers"""
        widget = TestViewportWidget()
        qtbot.addWidget(widget)
        widget.resize(1000, 800)

        # Mock window with project
        mock_window = Mock()
        mock_window.project = Project(name="Test")
        mock_window.project.working_dpi = 96

        # Two pages of different widths
        pages = [
            Page(layout=PageLayout(width=210, height=297), page_number=1),  # Wide
            Page(layout=PageLayout(width=100, height=150), page_number=2),  # Narrow
        ]
        mock_window.project.pages = pages

        widget.window = Mock(return_value=mock_window)
        widget.zoom_level = 1.0

        # Test clamping at position between pages
        # Position viewport center between the two pages
        widget.pan_offset = [0, -700]  # Between first and second page
        widget.clamp_pan_offset()

        # Should interpolate horizontal clamping between the two page widths
        assert isinstance(widget.pan_offset[0], (int, float))
