"""
Tests for ElementManipulationMixin
"""

import pytest
from unittest.mock import Mock, MagicMock
from PyQt6.QtOpenGLWidgets import QOpenGLWidget
from pyPhotoAlbum.mixins.element_manipulation import ElementManipulationMixin
from pyPhotoAlbum.mixins.element_selection import ElementSelectionMixin
from pyPhotoAlbum.models import ImageData
from pyPhotoAlbum.project import Project, Page
from pyPhotoAlbum.page_layout import PageLayout


# Create test widget combining necessary mixins
class TestManipulationWidget(ElementManipulationMixin, ElementSelectionMixin, QOpenGLWidget):
    """Test widget combining manipulation and selection mixins"""

    def __init__(self):
        super().__init__()
        self._page_renderers = []
        self.drag_start_pos = None
        self.drag_start_element_pos = None


class TestElementManipulationInitialization:
    """Test ElementManipulationMixin initialization"""

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

        assert widget.resize_handle is None
        assert widget.resize_start_pos is None
        assert widget.resize_start_size is None
        assert widget.rotation_mode is False
        assert widget.rotation_start_angle is None
        assert widget.rotation_snap_angle == 15
        assert widget.snap_state == {"is_snapped": False, "last_position": None, "last_size": None}

    def test_rotation_mode_is_mutable(self, qtbot):
        """Test that rotation mode can be toggled"""
        widget = TestManipulationWidget()
        qtbot.addWidget(widget)

        widget.rotation_mode = True
        assert widget.rotation_mode is True

        widget.rotation_mode = False
        assert widget.rotation_mode is False

    def test_rotation_snap_angle_is_configurable(self, qtbot):
        """Test that rotation snap angle can be changed"""
        widget = TestManipulationWidget()
        qtbot.addWidget(widget)

        widget.rotation_snap_angle = 45
        assert widget.rotation_snap_angle == 45


class TestResizeElementNoSnap:
    """Test _resize_element_no_snap method"""

    def test_resize_se_handle_increases_size(self, qtbot):
        """Test SE handle resizes from bottom-right"""
        widget = TestManipulationWidget()
        qtbot.addWidget(widget)

        elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
        widget.selected_element = elem
        widget.resize_handle = "se"
        widget.resize_start_pos = (100, 100)
        widget.resize_start_size = (200, 150)

        # Drag 50 pixels right and down
        widget._resize_element_no_snap(50, 30)

        assert elem.position == (100, 100)  # Position unchanged
        assert elem.size == (250, 180)  # Size increased

    def test_resize_nw_handle_moves_and_resizes(self, qtbot):
        """Test NW handle moves position and adjusts size"""
        widget = TestManipulationWidget()
        qtbot.addWidget(widget)

        elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
        widget.selected_element = elem
        widget.resize_handle = "nw"
        widget.resize_start_pos = (100, 100)
        widget.resize_start_size = (200, 150)

        # Drag 20 pixels left and up (negative deltas in local coordinates mean expansion)
        widget._resize_element_no_snap(-20, -10)

        assert elem.position == (80, 90)  # Moved up-left
        assert elem.size == (220, 160)  # Size increased

    def test_resize_ne_handle(self, qtbot):
        """Test NE handle behavior"""
        widget = TestManipulationWidget()
        qtbot.addWidget(widget)

        elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
        widget.selected_element = elem
        widget.resize_handle = "ne"
        widget.resize_start_pos = (100, 100)
        widget.resize_start_size = (200, 150)

        # Drag right and up
        widget._resize_element_no_snap(30, -20)

        assert elem.position == (100, 80)  # Y moved up, X unchanged
        assert elem.size == (230, 170)  # Both dimensions increased

    def test_resize_sw_handle(self, qtbot):
        """Test SW handle behavior"""
        widget = TestManipulationWidget()
        qtbot.addWidget(widget)

        elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
        widget.selected_element = elem
        widget.resize_handle = "sw"
        widget.resize_start_pos = (100, 100)
        widget.resize_start_size = (200, 150)

        # Drag left and down
        widget._resize_element_no_snap(-15, 25)

        assert elem.position == (85, 100)  # X moved left, Y unchanged
        assert elem.size == (215, 175)  # Both dimensions increased

    def test_resize_enforces_minimum_size(self, qtbot):
        """Test that resize enforces minimum size of 20px"""
        widget = TestManipulationWidget()
        qtbot.addWidget(widget)

        elem = ImageData(image_path="test.jpg", x=100, y=100, width=50, height=50)
        widget.selected_element = elem
        widget.resize_handle = "se"
        widget.resize_start_pos = (100, 100)
        widget.resize_start_size = (50, 50)

        # Try to shrink below minimum
        widget._resize_element_no_snap(-40, -40)

        assert elem.size[0] >= 20  # Width at least 20
        assert elem.size[1] >= 20  # Height at least 20

    def test_resize_no_op_without_resize_start(self, qtbot):
        """Test resize does nothing without start position/size"""
        widget = TestManipulationWidget()
        qtbot.addWidget(widget)

        elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
        widget.selected_element = elem
        widget.resize_handle = "se"
        # Don't set resize_start_pos or resize_start_size

        original_pos = elem.position
        original_size = elem.size

        widget._resize_element_no_snap(50, 50)

        # Should be unchanged
        assert elem.position == original_pos
        assert elem.size == original_size


class TestResizeElementWithSnap:
    """Test _resize_element method with snapping"""

    def test_resize_with_snap_calls_snapping_system(self, qtbot):
        """Test resize with snap uses snapping system"""
        widget = TestManipulationWidget()
        qtbot.addWidget(widget)

        # Create element with parent page
        elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
        page = Page(layout=PageLayout(width=210, height=297), page_number=1)
        page.layout.add_element(elem)
        elem._parent_page = page

        widget.selected_element = elem
        widget.resize_handle = "se"
        widget.resize_start_pos = (100, 100)
        widget.resize_start_size = (200, 150)

        # Mock window and project
        mock_window = Mock()
        mock_window.project = Project(name="Test")
        mock_window.project.working_dpi = 96
        widget.window = Mock(return_value=mock_window)

        # Mock snap_resize to return modified values
        mock_snap_sys = page.layout.snapping_system
        mock_snap_sys.snap_resize = Mock(return_value=((100, 100), (250, 180)))

        widget._resize_element(50, 30)

        # Verify snap_resize was called
        assert mock_snap_sys.snap_resize.called
        call_args = mock_snap_sys.snap_resize.call_args
        # snap_resize is called with a SnapResizeParams object as first positional arg
        params = call_args[0][0]
        assert params.dx == 50
        assert params.dy == 30
        assert params.resize_handle == "se"

        # Verify element was updated
        assert elem.size == (250, 180)

    def test_resize_without_parent_page_uses_no_snap(self, qtbot):
        """Test resize without parent page falls back to no-snap"""
        widget = TestManipulationWidget()
        qtbot.addWidget(widget)

        elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
        widget.selected_element = elem
        widget.resize_handle = "se"
        widget.resize_start_pos = (100, 100)
        widget.resize_start_size = (200, 150)

        # No _parent_page attribute
        widget._resize_element(50, 30)

        # Should use no-snap logic
        assert elem.size == (250, 180)

    def test_resize_enforces_minimum_size_with_snap(self, qtbot):
        """Test minimum size is enforced even with snapping"""
        widget = TestManipulationWidget()
        qtbot.addWidget(widget)

        elem = ImageData(image_path="test.jpg", x=100, y=100, width=50, height=50)
        page = Page(layout=PageLayout(width=210, height=297), page_number=1)
        page.layout.add_element(elem)
        elem._parent_page = page

        widget.selected_element = elem
        widget.resize_handle = "se"
        widget.resize_start_pos = (100, 100)
        widget.resize_start_size = (50, 50)

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

        # Mock snap to return tiny size
        mock_snap_sys = page.layout.snapping_system
        mock_snap_sys.snap_resize = Mock(return_value=((100, 100), (5, 5)))

        widget._resize_element(-45, -45)

        # Should enforce minimum
        assert elem.size[0] >= 20
        assert elem.size[1] >= 20


class TestTransferElementToPage:
    """Test _transfer_element_to_page method"""

    def test_transfer_moves_element_between_pages(self, qtbot):
        """Test element is transferred from source to target page"""
        widget = TestManipulationWidget()
        qtbot.addWidget(widget)

        # Create source and target pages
        source_page = Page(layout=PageLayout(width=210, height=297), page_number=1)
        target_page = Page(layout=PageLayout(width=210, height=297), page_number=2)

        # Create element on source page
        elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
        source_page.layout.add_element(elem)

        # Mock renderer
        mock_renderer = Mock()
        mock_renderer.screen_to_page = Mock(return_value=(150, 175))

        # Transfer element
        widget._transfer_element_to_page(elem, source_page, target_page, 250, 300, mock_renderer)

        # Verify element removed from source
        assert elem not in source_page.layout.elements

        # Verify element added to target
        assert elem in target_page.layout.elements

        # Verify element references updated
        assert elem._parent_page is target_page
        assert elem._page_renderer is mock_renderer

    def test_transfer_centers_element_on_mouse(self, qtbot):
        """Test transferred element is centered on mouse position"""
        widget = TestManipulationWidget()
        qtbot.addWidget(widget)

        source_page = Page(layout=PageLayout(width=210, height=297), page_number=1)
        target_page = Page(layout=PageLayout(width=210, height=297), page_number=2)

        elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
        source_page.layout.add_element(elem)

        # Mock renderer - mouse at (250, 300) screen -> (150, 175) page
        mock_renderer = Mock()
        mock_renderer.screen_to_page = Mock(return_value=(150, 175))

        widget._transfer_element_to_page(elem, source_page, target_page, 250, 300, mock_renderer)

        # Element should be centered: (150 - 200/2, 175 - 150/2) = (50, 100)
        assert elem.position == (50, 100)

    def test_transfer_updates_drag_state(self, qtbot):
        """Test transfer updates drag start position"""
        widget = TestManipulationWidget()
        qtbot.addWidget(widget)

        source_page = Page(layout=PageLayout(width=210, height=297), page_number=1)
        target_page = Page(layout=PageLayout(width=210, height=297), page_number=2)

        elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
        source_page.layout.add_element(elem)

        mock_renderer = Mock()
        mock_renderer.screen_to_page = Mock(return_value=(150, 175))

        widget._transfer_element_to_page(elem, source_page, target_page, 250, 300, mock_renderer)

        # Drag state should be updated
        assert widget.drag_start_pos == (250, 300)
        assert widget.drag_start_element_pos == elem.position


class TestManipulationStateManagement:
    """Test state management"""

    def test_snap_state_dictionary_structure(self, qtbot):
        """Test snap state has expected structure"""
        widget = TestManipulationWidget()
        qtbot.addWidget(widget)

        assert "is_snapped" in widget.snap_state
        assert "last_position" in widget.snap_state
        assert "last_size" in widget.snap_state

    def test_resize_state_can_be_set(self, qtbot):
        """Test resize state variables can be set"""
        widget = TestManipulationWidget()
        qtbot.addWidget(widget)

        widget.resize_handle = "nw"
        widget.resize_start_pos = (10, 20)
        widget.resize_start_size = (100, 200)

        assert widget.resize_handle == "nw"
        assert widget.resize_start_pos == (10, 20)
        assert widget.resize_start_size == (100, 200)

    def test_rotation_state_can_be_set(self, qtbot):
        """Test rotation state variables can be set"""
        widget = TestManipulationWidget()
        qtbot.addWidget(widget)

        widget.rotation_mode = True
        widget.rotation_start_angle = 45.0

        assert widget.rotation_mode is True
        assert widget.rotation_start_angle == 45.0
