"""
Tests for PageSetupDialog
"""

import pytest
from unittest.mock import Mock, MagicMock, patch
from PyQt6.QtWidgets import QDialog
from pyPhotoAlbum.dialogs.page_setup_dialog import PageSetupDialog
from pyPhotoAlbum.project import Project, Page
from pyPhotoAlbum.page_layout import PageLayout


class TestPageSetupDialog:
    """Test PageSetupDialog UI component"""

    def test_dialog_initialization(self, qtbot):
        """Test dialog initializes with project data"""
        project = Project(name="Test")
        project.paper_thickness_mm = 0.1
        project.cover_bleed_mm = 3.0
        project.working_dpi = 96
        project.export_dpi = 300
        project.page_size_mm = (210, 297)

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

        dialog = PageSetupDialog(None, project, initial_page_index=0)
        qtbot.addWidget(dialog)

        # Check dialog is created
        assert dialog.windowTitle() == "Page Setup"
        assert dialog.minimumWidth() == 450

        # Check DPI values initialized correctly
        assert dialog.working_dpi_spinbox.value() == 96
        assert dialog.export_dpi_spinbox.value() == 300

        # Check cover settings initialized correctly
        assert dialog.thickness_spinbox.value() == 0.1
        assert dialog.bleed_spinbox.value() == 3.0

    def test_dialog_page_selection(self, qtbot):
        """Test page selection combo box populated correctly"""
        project = Project(name="Test")
        project.page_size_mm = (210, 297)

        page1 = Page(layout=PageLayout(width=210, height=297), page_number=1)
        page2 = Page(layout=PageLayout(width=210, height=297), page_number=2)
        page2.manually_sized = True
        page3 = Page(layout=PageLayout(width=420, height=297), page_number=3)
        page3.is_double_spread = True

        project.pages = [page1, page2, page3]

        dialog = PageSetupDialog(None, project, initial_page_index=0)
        qtbot.addWidget(dialog)

        # Check combo box has all pages
        assert dialog.page_combo.count() == 3

        # Check page labels
        assert "Page 1" in dialog.page_combo.itemText(0)
        assert "Page 2" in dialog.page_combo.itemText(1)
        assert "*" in dialog.page_combo.itemText(1)  # Manually sized marker
        # Page 3 is a double spread, so it shows as "Pages 3-4"
        assert "Pages 3-4" in dialog.page_combo.itemText(2) or "Page 3" in dialog.page_combo.itemText(2)
        assert "Double Spread" in dialog.page_combo.itemText(2)

    def test_dialog_cover_settings_visibility(self, qtbot):
        """Test cover settings visibility toggled based on page selection"""
        project = Project(name="Test")
        project.page_size_mm = (210, 297)

        page1 = Page(layout=PageLayout(width=210, height=297), page_number=1)
        page2 = Page(layout=PageLayout(width=210, height=297), page_number=2)
        project.pages = [page1, page2]

        dialog = PageSetupDialog(None, project, initial_page_index=0)
        qtbot.addWidget(dialog)

        # Test the _on_page_changed method directly (testing business logic)
        # When showing first page (index 0), cover group should be made visible
        dialog._on_page_changed(0)
        # We can't reliably test isVisible() in headless Qt, but we can verify
        # the method was called and completed without error

        # When showing second page (index 1), cover group should be hidden
        dialog._on_page_changed(1)

        # Test that invalid indices are handled gracefully
        dialog._on_page_changed(-1)  # Should return early
        dialog._on_page_changed(999)  # Should return early

        # Verify page combo was populated correctly
        assert dialog.page_combo.count() == 2

    def test_dialog_cover_disables_size_editing(self, qtbot):
        """Test cover pages disable size editing"""
        project = Project(name="Test")
        project.page_size_mm = (210, 297)

        page1 = Page(layout=PageLayout(width=500, height=297), page_number=1)
        page1.is_cover = True
        project.pages = [page1]

        dialog = PageSetupDialog(None, project, initial_page_index=0)
        qtbot.addWidget(dialog)

        # Size editing should be disabled for covers
        assert not dialog.width_spinbox.isEnabled()
        assert not dialog.height_spinbox.isEnabled()
        assert not dialog.scope_non_manual.isEnabled()
        assert not dialog.scope_all_pages.isEnabled()

    def test_dialog_double_spread_width_calculation(self, qtbot):
        """Test double spread shows per-page width, not total width"""
        project = Project(name="Test")
        project.page_size_mm = (210, 297)

        page = Page(layout=PageLayout(width=420, height=297), page_number=1)
        page.is_double_spread = True
        page.layout.base_width = 210
        project.pages = [page]

        dialog = PageSetupDialog(None, project, initial_page_index=0)
        qtbot.addWidget(dialog)

        # Should show base width (per-page width), not total width
        assert dialog.width_spinbox.value() == 210
        assert dialog.height_spinbox.value() == 297

    def test_dialog_spine_info_calculation(self, qtbot):
        """Test spine info is calculated correctly"""
        project = Project(name="Test")
        project.page_size_mm = (210, 297)
        project.paper_thickness_mm = 0.1
        project.cover_bleed_mm = 3.0

        page1 = Page(layout=PageLayout(width=210, height=297), page_number=1)
        page1.is_cover = False
        page2 = Page(layout=PageLayout(width=210, height=297), page_number=2)
        page3 = Page(layout=PageLayout(width=210, height=297), page_number=3)
        project.pages = [page1, page2, page3]

        dialog = PageSetupDialog(None, project, initial_page_index=0)
        qtbot.addWidget(dialog)

        # Enable cover checkbox
        dialog.cover_checkbox.setChecked(True)

        # Check spine info label has content
        spine_text = dialog.spine_info_label.text()
        assert "Cover Layout" in spine_text
        assert "Front" in spine_text
        assert "Spine" in spine_text
        assert "Back" in spine_text
        assert "Bleed" in spine_text

        # Disable cover checkbox
        dialog.cover_checkbox.setChecked(False)

        # Spine info should be empty
        assert dialog.spine_info_label.text() == ""

    def test_get_values_returns_correct_data(self, qtbot):
        """Test get_values returns all dialog values"""
        project = Project(name="Test")
        project.page_size_mm = (210, 297)
        project.paper_thickness_mm = 0.1
        project.cover_bleed_mm = 3.0
        project.working_dpi = 96
        project.export_dpi = 300

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

        dialog = PageSetupDialog(None, project, initial_page_index=0)
        qtbot.addWidget(dialog)

        # Modify some values
        dialog.width_spinbox.setValue(200)
        dialog.height_spinbox.setValue(280)
        dialog.working_dpi_spinbox.setValue(150)
        dialog.export_dpi_spinbox.setValue(600)
        dialog.scope_all_pages.setChecked(True)
        dialog.cover_checkbox.setChecked(True)
        dialog.thickness_spinbox.setValue(0.15)
        dialog.bleed_spinbox.setValue(5.0)

        values = dialog.get_values()

        # Check all values returned
        assert values["selected_index"] == 0
        assert values["selected_page"] == page
        assert values["is_cover"] is True
        assert values["paper_thickness_mm"] == 0.15
        assert values["cover_bleed_mm"] == 5.0
        assert values["width_mm"] == 200
        assert values["height_mm"] == 280
        assert values["working_dpi"] == 150
        assert values["export_dpi"] == 600
        assert values["apply_scope"] == 2

    def test_dialog_page_change_updates_values(self, qtbot):
        """Test changing selected page updates displayed values"""
        project = Project(name="Test")
        project.page_size_mm = (210, 297)

        page1 = Page(layout=PageLayout(width=210, height=297), page_number=1)
        page2 = Page(layout=PageLayout(width=180, height=250), page_number=2)
        project.pages = [page1, page2]

        dialog = PageSetupDialog(None, project, initial_page_index=0)
        qtbot.addWidget(dialog)

        # Initially showing page 1 values
        assert dialog.width_spinbox.value() == 210
        assert dialog.height_spinbox.value() == 297

        # Change to page 2
        dialog.page_combo.setCurrentIndex(1)

        # Should now show page 2 values
        assert dialog.width_spinbox.value() == 180
        assert dialog.height_spinbox.value() == 250


class TestDialogMixin:
    """Test DialogMixin functionality"""

    def test_dialog_mixin_create_dialog_accepted(self, qtbot):
        """Test create_dialog returns values when accepted"""
        from pyPhotoAlbum.mixins.dialog_mixin import DialogMixin

        class TestWindow(DialogMixin):
            pass

        window = TestWindow()

        # Create mock dialog with get_values as a proper method
        mock_dialog = MagicMock(spec=QDialog)
        mock_dialog.exec = Mock(return_value=QDialog.DialogCode.Accepted)
        mock_dialog.get_values = Mock(return_value={"test": "value"})

        # Mock dialog class
        mock_dialog_class = Mock(return_value=mock_dialog)

        result = window.create_dialog(mock_dialog_class)

        assert result == {"test": "value"}
        mock_dialog.exec.assert_called_once()

    def test_dialog_mixin_create_dialog_rejected(self, qtbot):
        """Test create_dialog returns None when rejected"""
        from pyPhotoAlbum.mixins.dialog_mixin import DialogMixin

        class TestWindow(DialogMixin):
            pass

        window = TestWindow()

        # Create mock dialog
        mock_dialog = Mock(spec=QDialog)
        mock_dialog.exec.return_value = QDialog.DialogCode.Rejected

        # Mock dialog class
        mock_dialog_class = Mock(return_value=mock_dialog)

        result = window.create_dialog(mock_dialog_class)

        assert result is None
        mock_dialog.exec.assert_called_once()

    def test_dialog_mixin_show_dialog_with_callback(self, qtbot):
        """Test show_dialog executes callback on acceptance"""
        from pyPhotoAlbum.mixins.dialog_mixin import DialogMixin

        class TestWindow(DialogMixin):
            pass

        window = TestWindow()

        # Create mock dialog with get_values as a proper method
        mock_dialog = MagicMock(spec=QDialog)
        mock_dialog.exec = Mock(return_value=QDialog.DialogCode.Accepted)
        mock_dialog.get_values = Mock(return_value={"test": "value"})

        # Mock dialog class
        mock_dialog_class = Mock(return_value=mock_dialog)

        # Mock callback
        callback = Mock()

        result = window.show_dialog(mock_dialog_class, on_accept=callback)

        assert result is True
        callback.assert_called_once_with({"test": "value"})


class TestDialogActionDecorator:
    """Test the @dialog_action decorator functionality"""

    def test_decorator_with_title_override(self, qtbot):
        """Test decorator can set custom dialog title"""
        from pyPhotoAlbum.decorators import dialog_action

        # We'll test that the decorator can pass through kwargs
        # This is more of a structural test
        decorator = dialog_action(dialog_class=PageSetupDialog, requires_pages=True)
        assert decorator.dialog_class == PageSetupDialog
        assert decorator.requires_pages is True

    def test_decorator_without_pages_requirement(self, qtbot):
        """Test decorator can disable page requirement"""
        from pyPhotoAlbum.decorators import dialog_action

        decorator = dialog_action(dialog_class=PageSetupDialog, requires_pages=False)
        assert decorator.requires_pages is False

    def test_dialog_action_class_decorator(self, qtbot):
        """Test DialogAction class directly"""
        from pyPhotoAlbum.decorators import DialogAction

        decorator = DialogAction(dialog_class=PageSetupDialog, requires_pages=True)
        assert decorator.dialog_class == PageSetupDialog
        assert decorator.requires_pages is True


class TestDialogMixinEdgeCases:
    """Test edge cases for DialogMixin"""

    def test_create_dialog_without_get_values(self, qtbot):
        """Test create_dialog when dialog has no get_values method"""
        from pyPhotoAlbum.mixins.dialog_mixin import DialogMixin

        class TestWindow(DialogMixin):
            pass

        window = TestWindow()

        # Create mock dialog WITHOUT get_values
        mock_dialog = MagicMock(spec=QDialog)
        mock_dialog.exec = Mock(return_value=QDialog.DialogCode.Accepted)
        # Explicitly make get_values unavailable
        del mock_dialog.get_values

        mock_dialog_class = Mock(return_value=mock_dialog)

        result = window.create_dialog(mock_dialog_class)

        # Should return True when accepted even without get_values
        assert result is True

    def test_create_dialog_with_title(self, qtbot):
        """Test create_dialog with custom title"""
        from pyPhotoAlbum.mixins.dialog_mixin import DialogMixin

        class TestWindow(DialogMixin):
            pass

        window = TestWindow()

        mock_dialog = MagicMock(spec=QDialog)
        mock_dialog.exec = Mock(return_value=QDialog.DialogCode.Accepted)
        mock_dialog.get_values = Mock(return_value={"data": "test"})

        mock_dialog_class = Mock(return_value=mock_dialog)

        result = window.create_dialog(mock_dialog_class, title="Custom Title")

        # Verify setWindowTitle was called
        mock_dialog.setWindowTitle.assert_called_once_with("Custom Title")
        assert result == {"data": "test"}

    def test_show_dialog_rejected(self, qtbot):
        """Test show_dialog when user rejects dialog"""
        from pyPhotoAlbum.mixins.dialog_mixin import DialogMixin

        class TestWindow(DialogMixin):
            pass

        window = TestWindow()

        mock_dialog = MagicMock(spec=QDialog)
        mock_dialog.exec = Mock(return_value=QDialog.DialogCode.Rejected)

        mock_dialog_class = Mock(return_value=mock_dialog)
        callback = Mock()

        result = window.show_dialog(mock_dialog_class, on_accept=callback)

        # Callback should not be called
        callback.assert_not_called()
        assert result is False


class TestPageSetupDialogEdgeCases:
    """Test edge cases in PageSetupDialog"""

    def test_dialog_with_cover_page(self, qtbot):
        """Test dialog correctly handles cover pages"""
        project = Project(name="Test")
        project.page_size_mm = (210, 297)
        project.paper_thickness_mm = 0.1
        project.cover_bleed_mm = 3.0

        page1 = Page(layout=PageLayout(width=500, height=297), page_number=1)
        page1.is_cover = True
        project.pages = [page1]

        dialog = PageSetupDialog(None, project, initial_page_index=0)
        qtbot.addWidget(dialog)

        # Cover checkbox should be checked
        assert dialog.cover_checkbox.isChecked()

        # Width spinbox should show full cover width
        assert dialog.width_spinbox.value() == 500

    def test_dialog_invalid_initial_page_index(self, qtbot):
        """Test dialog handles invalid initial page index gracefully"""
        project = Project(name="Test")
        project.page_size_mm = (210, 297)

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

        # Invalid initial index (out of bounds)
        dialog = PageSetupDialog(None, project, initial_page_index=999)
        qtbot.addWidget(dialog)

        # Should still work, defaulting to first available page or handling gracefully
        assert dialog.page_combo.count() == 1

    def test_on_page_changed_invalid_index(self, qtbot):
        """Test _on_page_changed handles invalid indices"""
        project = Project(name="Test")
        project.page_size_mm = (210, 297)

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

        dialog = PageSetupDialog(None, project, initial_page_index=0)
        qtbot.addWidget(dialog)

        # Call with negative index - should return early
        dialog._on_page_changed(-1)

        # Call with out of bounds index - should return early
        dialog._on_page_changed(999)

        # Dialog should still be functional
        assert dialog.page_combo.count() == 1

    def test_update_spine_info_when_not_cover(self, qtbot):
        """Test spine info is empty when cover checkbox is unchecked"""
        project = Project(name="Test")
        project.page_size_mm = (210, 297)

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

        dialog = PageSetupDialog(None, project, initial_page_index=0)
        qtbot.addWidget(dialog)

        # Uncheck cover
        dialog.cover_checkbox.setChecked(False)

        # Spine info should be empty
        assert dialog.spine_info_label.text() == ""


class TestPageSetupIntegration:
    """Integration tests for page_setup with decorator"""

    def test_page_setup_decorator_requires_pages(self, qtbot):
        """Test page_setup decorator returns early when no pages"""
        from PyQt6.QtWidgets import QMainWindow
        from pyPhotoAlbum.mixins.base import ApplicationStateMixin
        from pyPhotoAlbum.mixins.operations.page_ops import PageOperationsMixin

        class TestWindow(PageOperationsMixin, ApplicationStateMixin, QMainWindow):
            def __init__(self):
                super().__init__()
                self._project = Project(name="Test")
                self._project.pages = []  # No pages
                self._gl_widget = Mock()
                self._status_bar = Mock()
                self._update_view_called = False

            def update_view(self):
                self._update_view_called = True

            def show_status(self, message, timeout=0):
                pass

        window = TestWindow()
        qtbot.addWidget(window)

        # Should return early without showing dialog
        result = window.page_setup()

        # No update should occur
        assert not window._update_view_called
        assert result is None

    def test_page_setup_applies_values(self, qtbot):
        """Test page_setup applies dialog values to project"""
        from PyQt6.QtWidgets import QMainWindow
        from pyPhotoAlbum.mixins.base import ApplicationStateMixin
        from pyPhotoAlbum.mixins.operations.page_ops import PageOperationsMixin

        class TestWindow(PageOperationsMixin, ApplicationStateMixin, QMainWindow):
            def __init__(self):
                super().__init__()
                self._project = Project(name="Test")
                self._project.page_size_mm = (210, 297)
                self._project.working_dpi = 96
                self._project.export_dpi = 300
                self._project.paper_thickness_mm = 0.1
                self._project.cover_bleed_mm = 3.0

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

                self._gl_widget = Mock()
                self._gl_widget._page_renderers = []
                self._status_bar = Mock()
                self._update_view_called = False
                self._status_message = None

            def _get_most_visible_page_index(self):
                return 0

            def update_view(self):
                self._update_view_called = True

            def show_status(self, message, timeout=0):
                self._status_message = message

        window = TestWindow()
        qtbot.addWidget(window)

        # Create mock values that would come from dialog
        values = {
            "selected_index": 0,
            "selected_page": window.project.pages[0],
            "is_cover": False,
            "paper_thickness_mm": 0.15,
            "cover_bleed_mm": 5.0,
            "width_mm": 200,
            "height_mm": 280,
            "working_dpi": 150,
            "export_dpi": 600,
            "apply_scope": 1,
        }

        # Access the unwrapped function to test business logic directly
        # The decorator wraps the function, so we need to get the original
        # or call it through the wrapper with the right setup
        import inspect

        # Get the original function before decorators
        original_func = window.page_setup
        # Decorators return wrappers, but we can call them with values directly
        # by accessing the innermost wrapped function
        while hasattr(original_func, "__wrapped__"):
            original_func = original_func.__wrapped__

        # If no __wrapped__, the decorator system is different
        # Let's just call the business logic method manually
        # First, let's extract and call just the business logic
        from pyPhotoAlbum.mixins.operations import page_ops

        # Get the undecorated method from the class
        undecorated_page_setup = page_ops.PageOperationsMixin.page_setup
        # Find the innermost function
        while hasattr(undecorated_page_setup, "__wrapped__"):
            undecorated_page_setup = undecorated_page_setup.__wrapped__

        # Call the business logic directly
        undecorated_page_setup(window, values)

        # Check values applied to project
        assert window.project.paper_thickness_mm == 0.15
        assert window.project.cover_bleed_mm == 5.0
        assert window.project.working_dpi == 150
        assert window.project.export_dpi == 600
        assert window.project.page_size_mm == (200, 280)  # apply_scope=1

        # Check page size updated
        assert window.project.pages[0].layout.size == (200, 280)
        assert window.project.pages[0].manually_sized is True

        # Check view updated
        assert window._update_view_called
        assert window._status_message is not None

    def test_page_setup_cover_designation(self, qtbot):
        """Test page_setup correctly designates and un-designates covers"""
        from PyQt6.QtWidgets import QMainWindow
        from pyPhotoAlbum.mixins.base import ApplicationStateMixin
        from pyPhotoAlbum.mixins.operations.page_ops import PageOperationsMixin

        class TestWindow(PageOperationsMixin, ApplicationStateMixin, QMainWindow):
            def __init__(self):
                super().__init__()
                self._project = Project(name="Test")
                self._project.page_size_mm = (210, 297)
                self._project.working_dpi = 96
                self._project.export_dpi = 300
                self._project.paper_thickness_mm = 0.1
                self._project.cover_bleed_mm = 3.0

                page = Page(layout=PageLayout(width=210, height=297), page_number=1)
                page.is_cover = False
                self._project.pages = [page]

                self._gl_widget = Mock()
                self._gl_widget._page_renderers = []
                self._status_bar = Mock()
                self._update_view_called = False

            def _get_most_visible_page_index(self):
                return 0

            def update_view(self):
                self._update_view_called = True

            def show_status(self, message, timeout=0):
                pass

        window = TestWindow()
        qtbot.addWidget(window)

        # Test designating first page as cover
        values = {
            "selected_index": 0,
            "selected_page": window.project.pages[0],
            "is_cover": True,  # Designate as cover
            "paper_thickness_mm": 0.1,
            "cover_bleed_mm": 3.0,
            "width_mm": 210,
            "height_mm": 297,
            "working_dpi": 96,
            "export_dpi": 300,
            "apply_scope": 0,
        }

        # Get the undecorated method
        from pyPhotoAlbum.mixins.operations import page_ops

        undecorated_page_setup = page_ops.PageOperationsMixin.page_setup
        while hasattr(undecorated_page_setup, "__wrapped__"):
            undecorated_page_setup = undecorated_page_setup.__wrapped__

        # Mock update_cover_dimensions
        window.project.update_cover_dimensions = Mock()

        # Call with cover designation
        undecorated_page_setup(window, values)

        # Check cover was designated
        assert window.project.pages[0].is_cover is True
        assert window.project.has_cover is True
        window.project.update_cover_dimensions.assert_called_once()

    def test_page_setup_double_spread_sizing(self, qtbot):
        """Test page_setup correctly handles double spread page sizing"""
        from PyQt6.QtWidgets import QMainWindow
        from pyPhotoAlbum.mixins.base import ApplicationStateMixin
        from pyPhotoAlbum.mixins.operations.page_ops import PageOperationsMixin

        class TestWindow(PageOperationsMixin, ApplicationStateMixin, QMainWindow):
            def __init__(self):
                super().__init__()
                self._project = Project(name="Test")
                self._project.page_size_mm = (210, 297)
                self._project.working_dpi = 96
                self._project.export_dpi = 300
                self._project.paper_thickness_mm = 0.1
                self._project.cover_bleed_mm = 3.0

                # Create double spread page
                page = Page(layout=PageLayout(width=420, height=297), page_number=1)
                page.is_double_spread = True
                page.layout.base_width = 210
                page.layout.is_facing_page = True
                self._project.pages = [page]

                self._gl_widget = Mock()
                self._gl_widget._page_renderers = []
                self._status_bar = Mock()
                self._update_view_called = False

            def _get_most_visible_page_index(self):
                return 0

            def update_view(self):
                self._update_view_called = True

            def show_status(self, message, timeout=0):
                pass

        window = TestWindow()
        qtbot.addWidget(window)

        # Test changing double spread page size
        values = {
            "selected_index": 0,
            "selected_page": window.project.pages[0],
            "is_cover": False,
            "paper_thickness_mm": 0.1,
            "cover_bleed_mm": 3.0,
            "width_mm": 200,  # New base width
            "height_mm": 280,  # New height
            "working_dpi": 96,
            "export_dpi": 300,
            "apply_scope": 0,
        }

        from pyPhotoAlbum.mixins.operations import page_ops

        undecorated_page_setup = page_ops.PageOperationsMixin.page_setup
        while hasattr(undecorated_page_setup, "__wrapped__"):
            undecorated_page_setup = undecorated_page_setup.__wrapped__

        undecorated_page_setup(window, values)

        # Check double spread sizing
        assert window.project.pages[0].layout.base_width == 200
        assert window.project.pages[0].layout.size == (400, 280)  # Double width
        assert window.project.pages[0].manually_sized is True

    def test_set_as_default_updates_double_spread_pages(self, qtbot):
        """Test set_as_default updates existing double spread pages with doubled width"""
        from PyQt6.QtWidgets import QMainWindow
        from pyPhotoAlbum.mixins.base import ApplicationStateMixin
        from pyPhotoAlbum.mixins.operations.page_ops import PageOperationsMixin

        class TestWindow(PageOperationsMixin, ApplicationStateMixin, QMainWindow):
            def __init__(self):
                super().__init__()
                self._project = Project(name="Test")
                self._project.page_size_mm = (210, 297)
                self._project.working_dpi = 96
                self._project.export_dpi = 300
                self._project.paper_thickness_mm = 0.1
                self._project.cover_bleed_mm = 3.0

                # Normal page being edited
                page1 = Page(layout=PageLayout(width=210, height=297), page_number=1)
                # Double spread page (not manually sized - should be updated)
                page2 = Page(layout=PageLayout(width=420, height=297), page_number=2)
                page2.is_double_spread = True
                page2.layout.base_width = 210
                page2.layout.is_facing_page = True
                # Manually sized page (should NOT be updated)
                page3 = Page(layout=PageLayout(width=150, height=200), page_number=3)
                page3.manually_sized = True
                self._project.pages = [page1, page2, page3]

                self._gl_widget = Mock()
                self._gl_widget._page_renderers = []
                self._status_bar = Mock()
                self._update_view_called = False

            def _get_most_visible_page_index(self):
                return 0

            def update_view(self):
                self._update_view_called = True

            def show_status(self, message, timeout=0):
                pass

        window = TestWindow()
        qtbot.addWidget(window)

        values = {
            "selected_index": 0,
            "selected_page": window.project.pages[0],
            "is_cover": False,
            "paper_thickness_mm": 0.1,
            "cover_bleed_mm": 3.0,
            "width_mm": 200,
            "height_mm": 280,
            "working_dpi": 96,
            "export_dpi": 300,
            "apply_scope": 1,
        }

        from pyPhotoAlbum.mixins.operations import page_ops

        undecorated_page_setup = page_ops.PageOperationsMixin.page_setup
        while hasattr(undecorated_page_setup, "__wrapped__"):
            undecorated_page_setup = undecorated_page_setup.__wrapped__

        undecorated_page_setup(window, values)

        # Project default updated
        assert window.project.page_size_mm == (200, 280)
        # Double spread page updated with doubled width
        assert window.project.pages[1].layout.base_width == 200
        assert window.project.pages[1].layout.size == (400, 280)
        # Manually sized page NOT updated
        assert window.project.pages[2].layout.size == (150, 200)

    def test_apply_scope_all_pages_overrides_manual_sizing(self, qtbot):
        """Test apply_scope=2 updates all pages including manually sized ones"""
        from PyQt6.QtWidgets import QMainWindow
        from pyPhotoAlbum.mixins.base import ApplicationStateMixin
        from pyPhotoAlbum.mixins.operations.page_ops import PageOperationsMixin

        class TestWindow(PageOperationsMixin, ApplicationStateMixin, QMainWindow):
            def __init__(self):
                super().__init__()
                self._project = Project(name="Test")
                self._project.page_size_mm = (210, 297)
                self._project.working_dpi = 96
                self._project.export_dpi = 300
                self._project.paper_thickness_mm = 0.1
                self._project.cover_bleed_mm = 3.0

                page1 = Page(layout=PageLayout(width=210, height=297), page_number=1)
                page2 = Page(layout=PageLayout(width=420, height=297), page_number=2)
                page2.is_double_spread = True
                page2.layout.base_width = 210
                page2.layout.is_facing_page = True
                page3 = Page(layout=PageLayout(width=150, height=200), page_number=3)
                page3.manually_sized = True
                self._project.pages = [page1, page2, page3]

                self._gl_widget = Mock()
                self._gl_widget._page_renderers = []
                self._status_bar = Mock()
                self._update_view_called = False

            def _get_most_visible_page_index(self):
                return 0

            def update_view(self):
                self._update_view_called = True

            def show_status(self, message, timeout=0):
                pass

        window = TestWindow()
        qtbot.addWidget(window)

        values = {
            "selected_index": 0,
            "selected_page": window.project.pages[0],
            "is_cover": False,
            "paper_thickness_mm": 0.1,
            "cover_bleed_mm": 3.0,
            "width_mm": 200,
            "height_mm": 280,
            "working_dpi": 96,
            "export_dpi": 300,
            "apply_scope": 2,
        }

        from pyPhotoAlbum.mixins.operations import page_ops

        undecorated_page_setup = page_ops.PageOperationsMixin.page_setup
        while hasattr(undecorated_page_setup, "__wrapped__"):
            undecorated_page_setup = undecorated_page_setup.__wrapped__

        undecorated_page_setup(window, values)

        # Project default updated
        assert window.project.page_size_mm == (200, 280)
        # Double spread updated
        assert window.project.pages[1].layout.size == (400, 280)
        # Manually sized page IS updated when scope=2
        assert window.project.pages[2].layout.size == (200, 280)
