"""
Tests for UndoableInteractionMixin
"""

import pytest
from unittest.mock import Mock, patch
from PyQt6.QtOpenGLWidgets import QOpenGLWidget
from pyPhotoAlbum.mixins.interaction_undo import UndoableInteractionMixin
from pyPhotoAlbum.models import ImageData, TextBoxData
from pyPhotoAlbum.project import Project, Page
from pyPhotoAlbum.page_layout import PageLayout
from pyPhotoAlbum.commands import CommandHistory


# Create test widget with UndoableInteractionMixin
class TestUndoableWidget(UndoableInteractionMixin, QOpenGLWidget):
    """Test widget with undoable interaction mixin"""

    def __init__(self):
        super().__init__()


class TestUndoableInteractionInitialization:
    """Test UndoableInteractionMixin initialization"""

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

        # Should have initialized tracking state object
        assert hasattr(widget, "_interaction_state")
        assert hasattr(widget, "_command_factory")

        # State should be clear initially
        assert widget._interaction_state.element is None
        assert widget._interaction_state.interaction_type is None
        assert widget._interaction_state.position is None
        assert widget._interaction_state.size is None
        assert widget._interaction_state.rotation is None


class TestBeginMove:
    """Test _begin_move method"""

    def test_begin_move_captures_state(self, qtbot):
        """Test that begin_move captures initial position"""
        widget = TestUndoableWidget()
        qtbot.addWidget(widget)

        element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)

        widget._begin_move(element)

        assert widget._interaction_state.element is element
        assert widget._interaction_state.interaction_type == "move"
        assert widget._interaction_state.position == (100, 100)
        assert widget._interaction_state.size is None
        assert widget._interaction_state.rotation is None

    def test_begin_move_updates_existing_state(self, qtbot):
        """Test that begin_move overwrites previous interaction state"""
        widget = TestUndoableWidget()
        qtbot.addWidget(widget)

        element1 = ImageData(image_path="/test1.jpg", x=50, y=50, width=100, height=100)
        element2 = ImageData(image_path="/test2.jpg", x=100, y=100, width=200, height=150)

        widget._begin_move(element1)
        widget._begin_move(element2)

        # Should have element2's state
        assert widget._interaction_state.element is element2
        assert widget._interaction_state.position == (100, 100)


class TestBeginResize:
    """Test _begin_resize method"""

    def test_begin_resize_captures_state(self, qtbot):
        """Test that begin_resize captures initial position and size"""
        widget = TestUndoableWidget()
        qtbot.addWidget(widget)

        element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)

        widget._begin_resize(element)

        assert widget._interaction_state.element is element
        assert widget._interaction_state.interaction_type == "resize"
        assert widget._interaction_state.position == (100, 100)
        assert widget._interaction_state.size == (200, 150)
        assert widget._interaction_state.rotation is None


class TestBeginRotate:
    """Test _begin_rotate method"""

    def test_begin_rotate_captures_state(self, qtbot):
        """Test that begin_rotate captures initial rotation"""
        widget = TestUndoableWidget()
        qtbot.addWidget(widget)

        element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)
        element.rotation = 45.0

        widget._begin_rotate(element)

        assert widget._interaction_state.element is element
        assert widget._interaction_state.interaction_type == "rotate"
        assert widget._interaction_state.position is None
        assert widget._interaction_state.size is None
        assert widget._interaction_state.rotation == 45.0


class TestBeginImagePan:
    """Test _begin_image_pan method"""

    def test_begin_image_pan_captures_crop_info(self, qtbot):
        """Test that begin_image_pan captures initial crop info"""
        widget = TestUndoableWidget()
        qtbot.addWidget(widget)

        element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150, crop_info=(0.1, 0.2, 0.8, 0.7))

        widget._begin_image_pan(element)

        assert widget._interaction_state.element is element
        assert widget._interaction_state.interaction_type == "image_pan"
        assert widget._interaction_state.crop_info == (0.1, 0.2, 0.8, 0.7)

    def test_begin_image_pan_ignores_non_image(self, qtbot):
        """Test that begin_image_pan ignores non-ImageData elements"""
        widget = TestUndoableWidget()
        qtbot.addWidget(widget)

        element = TextBoxData(text_content="Test", x=100, y=100, width=200, height=100)

        widget._begin_image_pan(element)

        # Should not set any state for non-ImageData
        assert widget._interaction_state.element is None
        assert widget._interaction_state.interaction_type is None


class TestEndInteraction:
    """Test _end_interaction method"""

    @patch("pyPhotoAlbum.commands.MoveElementCommand")
    def test_end_interaction_creates_move_command(self, mock_cmd_class, qtbot):
        """Test that ending move interaction creates MoveElementCommand"""
        widget = TestUndoableWidget()
        qtbot.addWidget(widget)

        # Setup mock project
        mock_window = Mock()
        mock_window.project = Mock()
        mock_window.project.history = Mock()
        widget.window = Mock(return_value=mock_window)

        element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)

        widget._begin_move(element)

        # Move the element
        element.position = (150, 160)

        widget._end_interaction()

        # Should have created and executed command
        assert mock_cmd_class.called
        mock_cmd_class.assert_called_once_with(element, (100, 100), (150, 160))
        assert mock_window.project.history.execute.called

    @patch("pyPhotoAlbum.commands.ResizeElementCommand")
    def test_end_interaction_creates_resize_command(self, mock_cmd_class, qtbot):
        """Test that ending resize interaction creates ResizeElementCommand"""
        widget = TestUndoableWidget()
        qtbot.addWidget(widget)

        mock_window = Mock()
        mock_window.project = Mock()
        mock_window.project.history = Mock()
        widget.window = Mock(return_value=mock_window)

        element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)

        widget._begin_resize(element)

        # Resize the element
        element.position = (90, 90)
        element.size = (250, 200)

        widget._end_interaction()

        # Should have created and executed command
        assert mock_cmd_class.called
        mock_cmd_class.assert_called_once_with(
            element,
            (100, 100),  # old position
            (200, 150),  # old size
            (90, 90),  # new position
            (250, 200),  # new size
        )
        assert mock_window.project.history.execute.called

    @patch("pyPhotoAlbum.commands.RotateElementCommand")
    def test_end_interaction_creates_rotate_command(self, mock_cmd_class, qtbot):
        """Test that ending rotate interaction creates RotateElementCommand"""
        widget = TestUndoableWidget()
        qtbot.addWidget(widget)

        mock_window = Mock()
        mock_window.project = Mock()
        mock_window.project.history = Mock()
        widget.window = Mock(return_value=mock_window)

        element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)
        element.rotation = 0

        widget._begin_rotate(element)

        # Rotate the element
        element.rotation = 90

        widget._end_interaction()

        # Should have created and executed command
        assert mock_cmd_class.called
        mock_cmd_class.assert_called_once_with(element, 0, 90)
        assert mock_window.project.history.execute.called

    @patch("pyPhotoAlbum.commands.AdjustImageCropCommand")
    def test_end_interaction_creates_crop_command(self, mock_cmd_class, qtbot):
        """Test that ending image pan interaction creates AdjustImageCropCommand"""
        widget = TestUndoableWidget()
        qtbot.addWidget(widget)

        mock_window = Mock()
        mock_window.project = Mock()
        mock_window.project.history = Mock()
        widget.window = Mock(return_value=mock_window)

        element = ImageData(
            image_path="/test.jpg",
            x=100,
            y=100,
            width=200,
            height=150,
            crop_info=(0.0, 0.0, 1.0, 1.0),  # Tuple format used in code
        )

        widget._begin_image_pan(element)

        # Pan the image
        element.crop_info = (0.1, 0.1, 0.8, 0.8)

        widget._end_interaction()

        # Should have created and executed command
        assert mock_cmd_class.called
        assert mock_window.project.history.execute.called

    def test_end_interaction_ignores_insignificant_move(self, qtbot):
        """Test that tiny moves (< 0.1 units) don't create commands"""
        widget = TestUndoableWidget()
        qtbot.addWidget(widget)

        mock_window = Mock()
        mock_window.project = Mock()
        mock_window.project.history = Mock()
        widget.window = Mock(return_value=mock_window)

        element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)

        widget._begin_move(element)

        # Move element by very small amount
        element.position = (100.05, 100.05)

        widget._end_interaction()

        # Should NOT have executed any command
        assert not mock_window.project.history.execute.called

    def test_end_interaction_ignores_no_change(self, qtbot):
        """Test that interactions with no change don't create commands"""
        widget = TestUndoableWidget()
        qtbot.addWidget(widget)

        mock_window = Mock()
        mock_window.project = Mock()
        mock_window.project.history = Mock()
        widget.window = Mock(return_value=mock_window)

        element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)
        element.rotation = 45

        widget._begin_rotate(element)

        # Don't change rotation

        widget._end_interaction()

        # Should NOT have executed any command
        assert not mock_window.project.history.execute.called

    def test_end_interaction_clears_state(self, qtbot):
        """Test that end_interaction clears tracking state"""
        widget = TestUndoableWidget()
        qtbot.addWidget(widget)

        mock_window = Mock()
        mock_window.project = Mock()
        mock_window.project.history = Mock()
        widget.window = Mock(return_value=mock_window)

        element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)

        widget._begin_move(element)
        element.position = (150, 150)
        widget._end_interaction()

        # State should be cleared
        assert widget._interaction_state.element is None
        assert widget._interaction_state.interaction_type is None
        assert widget._interaction_state.position is None

    def test_end_interaction_no_project(self, qtbot):
        """Test that end_interaction handles missing project gracefully"""
        widget = TestUndoableWidget()
        qtbot.addWidget(widget)

        # No project attribute on window
        mock_window = Mock()
        del mock_window.project
        widget.window = Mock(return_value=mock_window)

        element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)

        widget._begin_move(element)
        element.position = (150, 150)

        # Should not crash
        widget._end_interaction()

        # State should be cleared
        assert widget._interaction_state.element is None

    def test_end_interaction_no_element(self, qtbot):
        """Test that end_interaction handles no element gracefully"""
        widget = TestUndoableWidget()
        qtbot.addWidget(widget)

        # Call without beginning any interaction
        widget._end_interaction()

        # Should not crash, state should remain clear
        assert widget._interaction_state.element is None


class TestClearInteractionState:
    """Test _clear_interaction_state method"""

    def test_clear_interaction_state(self, qtbot):
        """Test that clear_interaction_state resets all tracking"""
        widget = TestUndoableWidget()
        qtbot.addWidget(widget)

        element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)

        # Set up some state
        widget._begin_move(element)
        assert widget._interaction_state.element is not None

        # Clear it
        widget._clear_interaction_state()

        # Everything should be None
        assert widget._interaction_state.element is None
        assert widget._interaction_state.interaction_type is None
        assert widget._interaction_state.position is None
        assert widget._interaction_state.size is None
        assert widget._interaction_state.rotation is None

    def test_clear_interaction_state_with_crop_info(self, qtbot):
        """Test that clear_interaction_state handles crop info"""
        widget = TestUndoableWidget()
        qtbot.addWidget(widget)

        element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150, crop_info=(0.0, 0.0, 1.0, 1.0))

        widget._begin_image_pan(element)
        # After begin_image_pan, crop_info should be stored
        assert widget._interaction_state.crop_info is not None

        widget._clear_interaction_state()

        # Crop info should be cleared
        assert widget._interaction_state.crop_info is None


class TestCancelInteraction:
    """Test _cancel_interaction method"""

    def test_cancel_interaction_clears_state(self, qtbot):
        """Test that cancel_interaction clears state without creating command"""
        widget = TestUndoableWidget()
        qtbot.addWidget(widget)

        mock_window = Mock()
        mock_window.project = Mock()
        mock_window.project.history = Mock()
        widget.window = Mock(return_value=mock_window)

        element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)

        widget._begin_move(element)
        element.position = (150, 150)

        # Cancel instead of ending
        widget._cancel_interaction()

        # Should NOT have created any command
        assert not mock_window.project.history.execute.called

        # State should be cleared
        assert widget._interaction_state.element is None
        assert widget._interaction_state.interaction_type is None


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

    def test_multiple_begin_calls(self, qtbot):
        """Test that multiple begin calls overwrite state correctly"""
        widget = TestUndoableWidget()
        qtbot.addWidget(widget)

        element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)

        widget._begin_move(element)
        widget._begin_resize(element)
        widget._begin_rotate(element)

        # Should have rotate state (last call wins)
        assert widget._interaction_state.interaction_type == "rotate"
        assert widget._interaction_state.rotation == 0

    @patch("pyPhotoAlbum.commands.ResizeElementCommand")
    def test_resize_with_only_size_change(self, mock_cmd_class, qtbot):
        """Test resize command when only size changes (position same)"""
        widget = TestUndoableWidget()
        qtbot.addWidget(widget)

        mock_window = Mock()
        mock_window.project = Mock()
        mock_window.project.history = Mock()
        widget.window = Mock(return_value=mock_window)

        element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)

        widget._begin_resize(element)

        # Only change size
        element.size = (250, 200)

        widget._end_interaction()

        # Should still create command
        assert mock_cmd_class.called
        assert mock_window.project.history.execute.called

    @patch("pyPhotoAlbum.commands.ResizeElementCommand")
    def test_resize_with_only_position_change(self, mock_cmd_class, qtbot):
        """Test resize command when only position changes (size same)"""
        widget = TestUndoableWidget()
        qtbot.addWidget(widget)

        mock_window = Mock()
        mock_window.project = Mock()
        mock_window.project.history = Mock()
        widget.window = Mock(return_value=mock_window)

        element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150)

        widget._begin_resize(element)

        # Only change position
        element.position = (90, 90)

        widget._end_interaction()

        # Should still create command
        assert mock_cmd_class.called
        assert mock_window.project.history.execute.called
