"""
Tests for MouseInteractionMixin
"""

import pytest
from unittest.mock import Mock, MagicMock, patch
from PyQt6.QtCore import Qt, QPoint, QPointF
from PyQt6.QtGui import QMouseEvent, QWheelEvent
from PyQt6.QtOpenGLWidgets import QOpenGLWidget
from pyPhotoAlbum.mixins.mouse_interaction import MouseInteractionMixin
from pyPhotoAlbum.mixins.viewport import ViewportMixin
from pyPhotoAlbum.mixins.element_selection import ElementSelectionMixin
from pyPhotoAlbum.mixins.element_manipulation import ElementManipulationMixin
from pyPhotoAlbum.mixins.image_pan import ImagePanMixin
from pyPhotoAlbum.mixins.page_navigation import PageNavigationMixin
from pyPhotoAlbum.mixins.interaction_undo import UndoableInteractionMixin
from pyPhotoAlbum.project import Project, Page
from pyPhotoAlbum.page_layout import PageLayout
from pyPhotoAlbum.models import ImageData, TextBoxData


# Create test widget combining necessary mixins
class TestMouseInteractionWidget(
    MouseInteractionMixin,
    PageNavigationMixin,
    ImagePanMixin,
    ElementManipulationMixin,
    ElementSelectionMixin,
    ViewportMixin,
    UndoableInteractionMixin,
    QOpenGLWidget,
):
    """Test widget combining mouse interaction with other required mixins"""

    def __init__(self):
        super().__init__()
        # Initialize additional state not covered by mixins
        self.current_page_index = 0
        self.resize_handle = None
        self.rotation_snap_angle = 15


class TestMouseInteractionInitialization:
    """Test MouseInteractionMixin initialization"""

    def test_widget_initializes_state(self, qtbot):
        """Test that widget initializes mouse interaction state"""
        widget = TestMouseInteractionWidget()
        qtbot.addWidget(widget)

        # Should have initialized state
        assert hasattr(widget, "drag_start_pos")
        assert hasattr(widget, "is_dragging")
        assert hasattr(widget, "is_panning")
        assert widget.drag_start_pos is None
        assert widget.is_dragging is False
        assert widget.is_panning is False


class TestMousePressEvent:
    """Test mousePressEvent method"""

    def test_left_click_starts_drag(self, qtbot):
        """Test left click starts drag operation - clears selection on empty click"""
        widget = TestMouseInteractionWidget()
        qtbot.addWidget(widget)

        # Mock update method
        widget.update = Mock()

        # Create left click event
        event = Mock()
        event.button = Mock(return_value=Qt.MouseButton.LeftButton)
        event.position = Mock(return_value=QPointF(100, 100))
        event.modifiers = Mock(return_value=Qt.KeyboardModifier.NoModifier)

        # Mock element selection and ghost page check
        widget._get_element_at = Mock(return_value=None)
        widget._get_page_at = Mock(return_value=(None, -1, None))
        widget._check_ghost_page_click = Mock(return_value=False)

        widget.mousePressEvent(event)

        # Should clear selection when clicking on empty space
        assert len(widget.selected_elements) == 0
        assert widget.update.called

    def test_left_click_selects_element(self, qtbot):
        """Test left click on element selects it"""
        widget = TestMouseInteractionWidget()
        qtbot.addWidget(widget)

        widget.update = Mock()

        # Create mock element
        mock_element = ImageData(image_path="/test.jpg", x=50, y=50, width=100, height=100)

        event = Mock()
        event.button = Mock(return_value=Qt.MouseButton.LeftButton)
        event.position = Mock(return_value=QPointF(75, 75))
        event.modifiers = Mock(return_value=Qt.KeyboardModifier.NoModifier)

        # Mock element selection to return the element
        widget._get_element_at = Mock(return_value=mock_element)
        widget._get_page_at = Mock(return_value=(None, -1, None))
        widget._check_ghost_page_click = Mock(return_value=False)

        widget.mousePressEvent(event)

        # Should select the element
        assert mock_element in widget.selected_elements
        assert widget.drag_start_pos == (75, 75)
        assert widget.is_dragging is True

    def test_ctrl_click_image_enters_image_pan_mode(self, qtbot):
        """Test Ctrl+click on ImageData enters image pan mode"""
        widget = TestMouseInteractionWidget()
        qtbot.addWidget(widget)

        widget.update = Mock()
        widget.setCursor = Mock()

        # Create image element with crop info
        element = ImageData(
            image_path="/test.jpg",
            x=50,
            y=50,
            width=100,
            height=100,
            crop_info=(0.0, 0.0, 1.0, 1.0),  # crop_info is a tuple (x, y, width, height)
        )

        event = Mock()
        event.button = Mock(return_value=Qt.MouseButton.LeftButton)
        event.position = Mock(return_value=QPointF(75, 75))
        event.modifiers = Mock(return_value=Qt.KeyboardModifier.ControlModifier)

        # Mock element selection to return the image element
        widget._get_element_at = Mock(return_value=element)
        widget._get_page_at = Mock(return_value=(None, -1, None))
        widget._check_ghost_page_click = Mock(return_value=False)

        widget.mousePressEvent(event)

        # Should enter image pan mode
        assert element in widget.selected_elements
        assert widget.image_pan_mode is True
        assert widget.is_dragging is True
        assert widget.setCursor.called

    def test_middle_click_starts_panning(self, qtbot):
        """Test middle mouse button starts panning"""
        widget = TestMouseInteractionWidget()
        qtbot.addWidget(widget)

        widget.update = Mock()

        event = Mock()
        event.button = Mock(return_value=Qt.MouseButton.MiddleButton)
        event.position = Mock(return_value=QPointF(150, 150))

        widget.mousePressEvent(event)

        # Should start panning
        assert widget.is_panning is True
        assert widget.drag_start_pos == (150, 150)

    def test_click_on_ghost_page_adds_page(self, qtbot):
        """Test clicking on ghost page calls check method"""
        widget = TestMouseInteractionWidget()
        qtbot.addWidget(widget)

        widget.update = Mock()

        # Mock ghost page click check to return True (handled ghost click)
        widget._check_ghost_page_click = Mock(return_value=True)

        event = Mock()
        event.button = Mock(return_value=Qt.MouseButton.LeftButton)
        event.position = Mock(return_value=QPointF(100, 100))
        event.modifiers = Mock(return_value=Qt.KeyboardModifier.NoModifier)

        widget.mousePressEvent(event)

        # Should have called check_ghost_page_click
        assert widget._check_ghost_page_click.called
        # Ghost click returns early, so update should not be called
        assert not widget.update.called


class TestMouseMoveEvent:
    """Test mouseMoveEvent method"""

    def test_hover_shows_resize_cursor(self, qtbot):
        """Test hovering over resize handle (not dragging)"""
        widget = TestMouseInteractionWidget()
        qtbot.addWidget(widget)

        widget.update = Mock()
        widget._update_page_status = Mock()

        # Create selected element
        element = ImageData(image_path="/test.jpg", x=50, y=50, width=100, height=100)
        widget.selected_elements.add(element)

        event = Mock()
        event.position = Mock(return_value=QPointF(150, 150))  # Bottom-right corner
        event.buttons = Mock(return_value=Qt.MouseButton.NoButton)

        # Mock resize handle detection
        widget._get_resize_handle_at = Mock(return_value="bottom-right")
        widget._get_element_at = Mock(return_value=element)

        widget.mouseMoveEvent(event)

        # Should call _update_page_status but not update (no drag)
        assert widget._update_page_status.called
        # No dragging, so no update call
        assert not widget.update.called

    def test_drag_moves_element(self, qtbot):
        """Test dragging moves selected element"""
        widget = TestMouseInteractionWidget()
        qtbot.addWidget(widget)

        widget.update = Mock()
        widget._update_page_status = Mock()

        # Setup 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)

        # Create and select element
        element = ImageData(image_path="/test.jpg", x=100, y=100, width=100, height=100)
        page.layout.add_element(element)
        widget.selected_elements.add(element)
        element._parent_page = page

        # Start drag
        widget.drag_start_pos = (150, 150)
        widget.is_dragging = True
        widget.drag_start_element_pos = (100, 100)

        # Mock page detection
        mock_renderer = Mock()
        mock_renderer.screen_to_page = Mock(return_value=(180, 180))
        widget._get_page_at = Mock(return_value=(page, 0, mock_renderer))

        event = Mock()
        event.position = Mock(return_value=QPointF(180, 180))
        event.buttons = Mock(return_value=Qt.MouseButton.LeftButton)
        event.modifiers = Mock(return_value=Qt.KeyboardModifier.NoModifier)

        widget.mouseMoveEvent(event)

        # Element should have moved (with snapping, so position should change)
        assert element.position != (100, 100)
        assert widget.update.called

    def test_middle_button_panning(self, qtbot):
        """Test middle button drag pans viewport"""
        widget = TestMouseInteractionWidget()
        qtbot.addWidget(widget)

        widget.update = Mock()
        widget.clamp_pan_offset = Mock()  # Mock clamping to allow any pan offset

        # Start panning
        widget.is_panning = True
        widget.drag_start_pos = (100, 100)
        initial_pan = widget.pan_offset.copy()

        event = Mock()
        event.position = Mock(return_value=QPointF(150, 150))
        event.buttons = Mock(return_value=Qt.MouseButton.MiddleButton)

        widget.mouseMoveEvent(event)

        # Pan offset should have changed
        assert widget.pan_offset != initial_pan
        assert widget.pan_offset == [50.0, 50.0]  # Moved by 50 pixels in each direction
        assert widget.update.called
        assert widget.clamp_pan_offset.called  # Clamping should be called

    def test_ctrl_drag_pans_image_in_frame(self, qtbot):
        """Test Ctrl+drag pans image within frame"""
        widget = TestMouseInteractionWidget()
        qtbot.addWidget(widget)

        widget.update = Mock()
        widget._update_page_status = Mock()

        # Create image element with crop info
        element = ImageData(
            image_path="/test.jpg",
            x=100,
            y=100,
            width=100,
            height=100,
            crop_info=(0.0, 0.0, 1.0, 1.0),  # crop_info is a tuple (x, y, width, height)
        )
        widget.selected_elements.add(element)

        # Start image pan drag
        widget.drag_start_pos = (150, 150)
        widget.is_dragging = True
        widget.image_pan_mode = True
        widget._image_pan_start = (0.0, 0.0)

        event = Mock()
        event.position = Mock(return_value=QPointF(160, 160))
        event.buttons = Mock(return_value=Qt.MouseButton.LeftButton)
        event.modifiers = Mock(return_value=Qt.KeyboardModifier.ControlModifier)

        # Mock _handle_image_pan_move method
        widget._handle_image_pan_move = Mock()

        widget.mouseMoveEvent(event)

        # Should call _handle_image_pan_move
        assert widget._handle_image_pan_move.called


class TestMouseReleaseEvent:
    """Test mouseReleaseEvent method"""

    def test_release_clears_drag_state(self, qtbot):
        """Test mouse release clears drag state"""
        widget = TestMouseInteractionWidget()
        qtbot.addWidget(widget)

        widget.setCursor = Mock()

        # Setup drag state
        widget.is_dragging = True
        widget.drag_start_pos = (100, 100)

        event = Mock()
        event.button = Mock(return_value=Qt.MouseButton.LeftButton)

        widget.mouseReleaseEvent(event)

        # Should clear drag state
        assert widget.is_dragging is False
        assert widget.drag_start_pos is None
        assert widget.setCursor.called

    def test_release_clears_panning_state(self, qtbot):
        """Test mouse release clears panning state"""
        widget = TestMouseInteractionWidget()
        qtbot.addWidget(widget)

        widget.update = Mock()

        # Setup panning state
        widget.is_panning = True
        widget.drag_start_pos = (100, 100)

        event = Mock()
        event.button = Mock(return_value=Qt.MouseButton.MiddleButton)

        widget.mouseReleaseEvent(event)

        # Should clear panning state
        assert widget.is_panning is False
        assert widget.drag_start_pos is None


class TestMouseDoubleClickEvent:
    """Test mouseDoubleClickEvent method"""

    def test_double_click_text_starts_editing(self, qtbot):
        """Test double-clicking text element starts editing"""
        widget = TestMouseInteractionWidget()
        qtbot.addWidget(widget)

        # Create text element with correct constructor
        text_element = TextBoxData(
            text_content="Test",
            x=100,
            y=100,
            width=100,
            height=50,
            font_settings={"family": "Arial", "size": 12, "color": (0, 0, 0)},
        )

        # Mock _edit_text_element method
        widget._edit_text_element = Mock()

        # Mock element selection to return text element
        widget._get_element_at = Mock(return_value=text_element)

        # Create real event (not Mock) for button()
        event = QMouseEvent(
            QMouseEvent.Type.MouseButtonDblClick,
            QPointF(125, 125),
            Qt.MouseButton.LeftButton,
            Qt.MouseButton.LeftButton,
            Qt.KeyboardModifier.NoModifier,
        )

        widget.mouseDoubleClickEvent(event)

        # Should call _edit_text_element
        widget._edit_text_element.assert_called_once_with(text_element)

    def test_double_click_non_text_does_nothing(self, qtbot):
        """Test double-clicking non-text element does nothing"""
        widget = TestMouseInteractionWidget()
        qtbot.addWidget(widget)

        # Create image element (not text)
        element = ImageData(image_path="/test.jpg", x=100, y=100, width=100, height=100)

        widget._edit_text_element = Mock()
        widget._get_element_at = Mock(return_value=element)

        # Create real event (not Mock) for button()
        event = QMouseEvent(
            QMouseEvent.Type.MouseButtonDblClick,
            QPointF(125, 125),
            Qt.MouseButton.LeftButton,
            Qt.MouseButton.LeftButton,
            Qt.KeyboardModifier.NoModifier,
        )

        widget.mouseDoubleClickEvent(event)

        # Should not call _edit_text_element
        assert not widget._edit_text_element.called


class TestWheelEvent:
    """Test wheelEvent method"""

    def test_scroll_pans_viewport(self, qtbot):
        """Test scroll wheel pans viewport without Ctrl"""
        widget = TestMouseInteractionWidget()
        qtbot.addWidget(widget)

        widget.update = Mock()

        initial_pan = widget.pan_offset[1]

        event = Mock()
        event.angleDelta = Mock(return_value=Mock(x=Mock(return_value=0), y=Mock(return_value=-120)))  # Scroll down
        event.modifiers = Mock(return_value=Qt.KeyboardModifier.NoModifier)

        widget.wheelEvent(event)

        # Should pan viewport
        assert widget.pan_offset[1] != initial_pan
        assert widget.update.called

    def test_ctrl_scroll_zooms(self, qtbot):
        """Test Ctrl+scroll zooms viewport"""
        widget = TestMouseInteractionWidget()
        qtbot.addWidget(widget)

        widget.update = Mock()

        initial_zoom = widget.zoom_level

        event = Mock()
        event.angleDelta = Mock(return_value=Mock(y=Mock(return_value=120)))  # Scroll up
        event.modifiers = Mock(return_value=Qt.KeyboardModifier.ControlModifier)
        event.position = Mock(return_value=QPointF(100, 100))

        widget.wheelEvent(event)

        # Should zoom in
        assert widget.zoom_level > initial_zoom
        assert widget.update.called

    def test_scroll_up_pans_viewport_up(self, qtbot):
        """Test scrolling up pans viewport upward"""
        widget = TestMouseInteractionWidget()
        qtbot.addWidget(widget)

        widget.update = Mock()
        # Mock clamp_pan_offset to prevent it from resetting pan_offset
        widget.clamp_pan_offset = Mock()

        initial_pan = widget.pan_offset[1]

        event = Mock()
        event.angleDelta = Mock(return_value=Mock(x=Mock(return_value=0), y=Mock(return_value=120)))  # Scroll up
        event.modifiers = Mock(return_value=Qt.KeyboardModifier.NoModifier)

        widget.wheelEvent(event)

        # Should pan viewport up (increase pan_offset[1])
        assert widget.pan_offset[1] > initial_pan
        assert widget.update.called

    def test_scroll_down_pans_viewport_down(self, qtbot):
        """Test scrolling down pans viewport downward"""
        widget = TestMouseInteractionWidget()
        qtbot.addWidget(widget)

        widget.update = Mock()
        # Mock clamp_pan_offset to prevent it from resetting pan_offset
        widget.clamp_pan_offset = Mock()

        initial_pan = widget.pan_offset[1]

        event = Mock()
        event.angleDelta = Mock(return_value=Mock(x=Mock(return_value=0), y=Mock(return_value=-120)))  # Scroll down
        event.modifiers = Mock(return_value=Qt.KeyboardModifier.NoModifier)

        widget.wheelEvent(event)

        # Should pan viewport down (decrease pan_offset[1])
        assert widget.pan_offset[1] < initial_pan
        assert widget.update.called


class TestRotationMode:
    """Test rotation mode functionality"""

    def test_click_in_rotation_mode_starts_rotation(self, qtbot):
        """Test clicking on element in rotation mode starts rotation"""
        widget = TestMouseInteractionWidget()
        qtbot.addWidget(widget)

        widget.update = Mock()
        widget.rotation_mode = True

        # Create and select element
        element = ImageData(image_path="/test.jpg", x=100, y=100, width=100, height=100)
        widget.selected_elements.add(element)

        # Mock the _begin_rotate method
        widget._begin_rotate = Mock()

        event = Mock()
        event.button = Mock(return_value=Qt.MouseButton.LeftButton)
        event.position = Mock(return_value=QPointF(150, 150))
        event.modifiers = Mock(return_value=Qt.KeyboardModifier.NoModifier)

        widget._get_element_at = Mock(return_value=element)
        widget._get_page_at = Mock(return_value=(None, -1, None))
        widget._check_ghost_page_click = Mock(return_value=False)

        widget.mousePressEvent(event)

        # Should start rotation
        assert widget.is_dragging is True
        assert widget.drag_start_pos == (150, 150)
        assert hasattr(widget, "rotation_start_angle")
        widget._begin_rotate.assert_called_once_with(element)

    def test_mouse_move_in_rotation_mode(self, qtbot):
        """Test mouse move in rotation mode rotates element"""
        widget = TestMouseInteractionWidget()
        qtbot.addWidget(widget)

        widget.update = Mock()
        widget._update_page_status = Mock()
        widget.rotation_mode = True

        # Create element with page renderer
        element = ImageData(image_path="/test.jpg", x=100, y=100, width=100, height=100)
        element.rotation = 0
        widget.selected_elements.add(element)

        # Create mock renderer
        mock_renderer = Mock()
        mock_renderer.page_to_screen = Mock(return_value=(150, 150))  # Center of element
        element._page_renderer = mock_renderer

        # Start dragging in rotation mode
        widget.drag_start_pos = (150, 150)
        widget.is_dragging = True
        widget.rotation_start_angle = 0

        # Mock window with show_status
        mock_window = Mock()
        mock_window.show_status = Mock()
        widget.window = Mock(return_value=mock_window)

        event = Mock()
        event.position = Mock(return_value=QPointF(200, 150))  # Mouse to the right

        widget.mouseMoveEvent(event)

        # Rotation should be updated (0 degrees for right side)
        assert element.rotation == 0  # Snapped to nearest 15 degrees
        assert widget.update.called
        mock_window.show_status.assert_called()


class TestResizeMode:
    """Test resize mode functionality"""

    def test_click_on_resize_handle_starts_resize(self, qtbot):
        """Test clicking on resize handle starts resize"""
        widget = TestMouseInteractionWidget()
        qtbot.addWidget(widget)

        widget.update = Mock()
        widget.rotation_mode = False

        # Create and select element
        element = ImageData(image_path="/test.jpg", x=100, y=100, width=100, height=100)
        widget.selected_elements.add(element)

        # Mock the _begin_resize method and _get_resize_handle_at
        widget._begin_resize = Mock()
        widget._get_resize_handle_at = Mock(return_value="bottom-right")

        event = Mock()
        event.button = Mock(return_value=Qt.MouseButton.LeftButton)
        event.position = Mock(return_value=QPointF(200, 200))  # Bottom-right corner
        event.modifiers = Mock(return_value=Qt.KeyboardModifier.NoModifier)

        widget._get_element_at = Mock(return_value=element)
        widget._get_page_at = Mock(return_value=(None, -1, None))
        widget._check_ghost_page_click = Mock(return_value=False)

        widget.mousePressEvent(event)

        # Should start resize
        assert widget.is_dragging is True
        assert widget.resize_handle == "bottom-right"
        assert widget.drag_start_pos == (200, 200)
        widget._begin_resize.assert_called_once_with(element)

    def test_mouse_move_in_resize_mode(self, qtbot):
        """Test mouse move in resize mode resizes element"""
        widget = TestMouseInteractionWidget()
        qtbot.addWidget(widget)

        widget.update = Mock()
        widget._update_page_status = Mock()

        # Create and select element
        element = ImageData(image_path="/test.jpg", x=100, y=100, width=100, height=100)
        widget.selected_elements.add(element)

        # Start dragging in resize mode
        widget.drag_start_pos = (200, 200)
        widget.is_dragging = True
        widget.resize_handle = "bottom-right"
        widget.resize_start_pos = (100, 100)
        widget.resize_start_size = (100, 100)

        # Mock _resize_element
        widget._resize_element = Mock()

        event = Mock()
        event.position = Mock(return_value=QPointF(220, 220))

        widget.mouseMoveEvent(event)

        # Should call _resize_element with deltas
        widget._resize_element.assert_called_once()
        assert widget.update.called


class TestMultiSelect:
    """Test multi-select functionality"""

    def test_ctrl_click_non_image_adds_to_selection(self, qtbot):
        """Test Ctrl+click on non-ImageData adds to multi-selection"""
        widget = TestMouseInteractionWidget()
        qtbot.addWidget(widget)

        widget.update = Mock()

        # Create and add first element
        element1 = TextBoxData(
            text_content="Test 1",
            x=100,
            y=100,
            width=100,
            height=50,
            font_settings={"family": "Arial", "size": 12, "color": (0, 0, 0)},
        )
        widget.selected_elements.add(element1)

        # Create second element
        element2 = TextBoxData(
            text_content="Test 2",
            x=250,
            y=100,
            width=100,
            height=50,
            font_settings={"family": "Arial", "size": 12, "color": (0, 0, 0)},
        )

        event = Mock()
        event.button = Mock(return_value=Qt.MouseButton.LeftButton)
        event.position = Mock(return_value=QPointF(300, 125))
        event.modifiers = Mock(return_value=Qt.KeyboardModifier.ControlModifier)

        widget._get_element_at = Mock(return_value=element2)
        widget._get_page_at = Mock(return_value=(None, -1, None))
        widget._check_ghost_page_click = Mock(return_value=False)

        widget.mousePressEvent(event)

        # Should add to selection
        assert element1 in widget.selected_elements
        assert element2 in widget.selected_elements
        assert len(widget.selected_elements) == 2

    def test_ctrl_click_selected_element_removes_from_selection(self, qtbot):
        """Test Ctrl+click on already selected element removes it"""
        widget = TestMouseInteractionWidget()
        qtbot.addWidget(widget)

        widget.update = Mock()

        # Create and select two elements
        element1 = TextBoxData(
            text_content="Test 1",
            x=100,
            y=100,
            width=100,
            height=50,
            font_settings={"family": "Arial", "size": 12, "color": (0, 0, 0)},
        )
        element2 = TextBoxData(
            text_content="Test 2",
            x=250,
            y=100,
            width=100,
            height=50,
            font_settings={"family": "Arial", "size": 12, "color": (0, 0, 0)},
        )
        widget.selected_elements.add(element1)
        widget.selected_elements.add(element2)

        event = Mock()
        event.button = Mock(return_value=Qt.MouseButton.LeftButton)
        event.position = Mock(return_value=QPointF(150, 125))
        event.modifiers = Mock(return_value=Qt.KeyboardModifier.ControlModifier)

        widget._get_element_at = Mock(return_value=element1)
        widget._get_page_at = Mock(return_value=(None, -1, None))
        widget._check_ghost_page_click = Mock(return_value=False)

        widget.mousePressEvent(event)

        # Should remove from selection
        assert element1 not in widget.selected_elements
        assert element2 in widget.selected_elements
        assert len(widget.selected_elements) == 1

    def test_shift_click_adds_to_selection(self, qtbot):
        """Test Shift+click adds element to selection"""
        widget = TestMouseInteractionWidget()
        qtbot.addWidget(widget)

        widget.update = Mock()

        # Create and select first element
        element1 = ImageData(image_path="/test1.jpg", x=100, y=100, width=100, height=100)
        widget.selected_elements.add(element1)

        # Create second element
        element2 = ImageData(image_path="/test2.jpg", x=250, y=100, width=100, height=100)

        event = Mock()
        event.button = Mock(return_value=Qt.MouseButton.LeftButton)
        event.position = Mock(return_value=QPointF(300, 150))
        event.modifiers = Mock(return_value=Qt.KeyboardModifier.ShiftModifier)

        widget._get_element_at = Mock(return_value=element2)
        widget._get_page_at = Mock(return_value=(None, -1, None))
        widget._check_ghost_page_click = Mock(return_value=False)

        widget.mousePressEvent(event)

        # Should add to selection
        assert element1 in widget.selected_elements
        assert element2 in widget.selected_elements
        assert len(widget.selected_elements) == 2

    def test_shift_click_selected_element_removes_it(self, qtbot):
        """Test Shift+click on selected element removes it"""
        widget = TestMouseInteractionWidget()
        qtbot.addWidget(widget)

        widget.update = Mock()

        # Create and select two elements
        element1 = ImageData(image_path="/test1.jpg", x=100, y=100, width=100, height=100)
        element2 = ImageData(image_path="/test2.jpg", x=250, y=100, width=100, height=100)
        widget.selected_elements.add(element1)
        widget.selected_elements.add(element2)

        event = Mock()
        event.button = Mock(return_value=Qt.MouseButton.LeftButton)
        event.position = Mock(return_value=QPointF(150, 150))
        event.modifiers = Mock(return_value=Qt.KeyboardModifier.ShiftModifier)

        widget._get_element_at = Mock(return_value=element1)
        widget._get_page_at = Mock(return_value=(None, -1, None))
        widget._check_ghost_page_click = Mock(return_value=False)

        widget.mousePressEvent(event)

        # Should remove from selection
        assert element1 not in widget.selected_elements
        assert element2 in widget.selected_elements
        assert len(widget.selected_elements) == 1


class TestElementPositioningWithoutParentPage:
    """Test element positioning when element has no parent page"""

    def test_drag_element_without_parent_page(self, qtbot):
        """Test dragging element that has no _parent_page attribute"""
        widget = TestMouseInteractionWidget()
        qtbot.addWidget(widget)

        widget.update = Mock()
        widget._update_page_status = Mock()

        # Create element without _parent_page
        element = ImageData(image_path="/test.jpg", x=100, y=100, width=100, height=100)
        widget.selected_elements.add(element)

        # Start dragging
        widget.drag_start_pos = (150, 150)
        widget.is_dragging = True
        widget.drag_start_element_pos = (100, 100)

        # Mock page detection to return a page
        mock_page = Mock()
        mock_renderer = Mock()
        widget._get_page_at = Mock(return_value=(mock_page, 0, mock_renderer))

        event = Mock()
        event.position = Mock(return_value=QPointF(180, 180))

        widget.mouseMoveEvent(event)

        # Element position should be updated (without snapping since no parent page)
        assert element.position == (130, 130)  # Moved by 30 pixels / zoom_level (1.0)
        assert widget.update.called


class TestWheelEventWhileDragging:
    """Test wheel events during drag operations"""

    def test_ctrl_scroll_while_dragging_adjusts_drag_start_pos(self, qtbot):
        """Test Ctrl+scroll while dragging adjusts drag_start_pos for zoom"""
        widget = TestMouseInteractionWidget()
        qtbot.addWidget(widget)

        widget.update = Mock()
        widget.clamp_pan_offset = Mock()

        # Setup dragging state
        widget.is_dragging = True
        widget.drag_start_pos = (100, 100)

        event = Mock()
        event.angleDelta = Mock(return_value=Mock(y=Mock(return_value=120)))  # Zoom in
        event.modifiers = Mock(return_value=Qt.KeyboardModifier.ControlModifier)
        event.position = Mock(return_value=QPointF(150, 150))

        old_drag_pos = widget.drag_start_pos

        widget.wheelEvent(event)

        # drag_start_pos should be adjusted
        assert widget.drag_start_pos != old_drag_pos
        assert widget.is_dragging is True

    def test_scroll_while_dragging_adjusts_drag_start_pos(self, qtbot):
        """Test scrolling while dragging adjusts drag_start_pos for pan"""
        widget = TestMouseInteractionWidget()
        qtbot.addWidget(widget)

        widget.update = Mock()
        widget.clamp_pan_offset = Mock()

        # Setup dragging state
        widget.is_dragging = True
        widget.drag_start_pos = (100, 100)
        initial_drag_y = widget.drag_start_pos[1]

        event = Mock()
        event.angleDelta = Mock(return_value=Mock(x=Mock(return_value=0), y=Mock(return_value=120)))  # Scroll up
        event.modifiers = Mock(return_value=Qt.KeyboardModifier.NoModifier)

        widget.wheelEvent(event)

        # drag_start_pos y should be adjusted for pan
        assert widget.drag_start_pos[1] != initial_drag_y
        assert widget.is_dragging is True


class TestEditTextElement:
    """Test _edit_text_element dialog functionality"""

    def test_edit_text_element_accepted(self, qtbot):
        """Test editing text element when dialog is accepted"""
        widget = TestMouseInteractionWidget()
        qtbot.addWidget(widget)

        widget.update = Mock()

        # Create text element
        text_element = TextBoxData(
            text_content="Original",
            x=100,
            y=100,
            width=100,
            height=50,
            font_settings={"family": "Arial", "size": 12, "color": (0, 0, 0)},
            alignment="left",
        )

        # Mock TextEditDialog - patch where it's imported
        with patch("pyPhotoAlbum.text_edit_dialog.TextEditDialog") as MockDialog:
            # Create mock instance
            mock_instance = Mock()
            MockDialog.return_value = mock_instance

            # Mock DialogCode.Accepted (needed for comparison in code)
            mock_dialog_code = Mock()
            mock_dialog_code.Accepted = 1
            MockDialog.DialogCode = mock_dialog_code

            # Set up the dialog to return Accepted (1)
            mock_instance.exec.return_value = 1
            mock_instance.get_values.return_value = {
                "text_content": "Updated",
                "font_settings": {"family": "Helvetica", "size": 14, "color": (0, 0, 0)},
                "alignment": "center",
            }

            widget._edit_text_element(text_element)

            # Verify dialog was created and methods called
            MockDialog.assert_called_once_with(text_element, widget)
            mock_instance.exec.assert_called_once()
            mock_instance.get_values.assert_called_once()

            # Should update element
            assert text_element.text_content == "Updated"
            assert text_element.font_settings["family"] == "Helvetica"
            assert text_element.alignment == "center"
            assert widget.update.called

    def test_edit_text_element_rejected(self, qtbot):
        """Test editing text element when dialog is rejected"""
        widget = TestMouseInteractionWidget()
        qtbot.addWidget(widget)

        widget.update = Mock()

        # Create text element
        text_element = TextBoxData(
            text_content="Original",
            x=100,
            y=100,
            width=100,
            height=50,
            font_settings={"family": "Arial", "size": 12, "color": (0, 0, 0)},
            alignment="left",
        )

        original_content = text_element.text_content

        # Mock TextEditDialog
        with patch("pyPhotoAlbum.text_edit_dialog.TextEditDialog") as MockDialog:
            mock_dialog = MockDialog.return_value
            mock_dialog.exec = Mock(return_value=0)  # Rejected

            widget._edit_text_element(text_element)

            # Should not update element
            assert text_element.text_content == original_content
            assert not widget.update.called
