"""
Tests for ImagePanMixin
"""

import pytest
from unittest.mock import Mock
from PyQt6.QtOpenGLWidgets import QOpenGLWidget
from pyPhotoAlbum.mixins.image_pan import ImagePanMixin
from pyPhotoAlbum.mixins.element_selection import ElementSelectionMixin
from pyPhotoAlbum.mixins.viewport import ViewportMixin
from pyPhotoAlbum.models import ImageData, PlaceholderData


# Create test widget combining necessary mixins
class TestImagePanWidget(ImagePanMixin, ElementSelectionMixin, ViewportMixin, QOpenGLWidget):
    """Test widget combining image pan, selection, and viewport mixins"""

    def __init__(self):
        super().__init__()
        self.drag_start_pos = None


class TestImagePanInitialization:
    """Test ImagePanMixin initialization"""

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

        assert widget.image_pan_mode is False
        assert widget.image_pan_start_crop is None

    def test_image_pan_mode_is_mutable(self, qtbot):
        """Test that image pan mode can be toggled"""
        widget = TestImagePanWidget()
        qtbot.addWidget(widget)

        widget.image_pan_mode = True
        assert widget.image_pan_mode is True

        widget.image_pan_mode = False
        assert widget.image_pan_mode is False


class TestHandleImagePanMove:
    """Test _handle_image_pan_move method"""

    def test_pan_right_shifts_crop_left(self, qtbot):
        """Test panning mouse right shifts crop window left (shows more of right side)"""
        widget = TestImagePanWidget()
        qtbot.addWidget(widget)

        elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
        elem.crop_info = (0.2, 0.2, 0.8, 0.8)  # 60% view in center

        widget.selected_element = elem
        widget.image_pan_mode = True
        widget.image_pan_start_crop = (0.2, 0.2, 0.8, 0.8)
        widget.drag_start_pos = (100, 100)
        widget.zoom_level = 1.0

        # Pan mouse 50 pixels right
        widget._handle_image_pan_move(150, 100, elem)

        # Crop should shift left (x_min increases)
        # crop_dx = -50 / (200 * 1.0) = -0.25
        # new_x_min = 0.2 + (-0.25) = -0.05 -> clamped to 0.0
        # new_x_max = 0.0 + 0.6 = 0.6
        assert elem.crop_info[0] == 0.0  # Left edge
        assert abs(elem.crop_info[2] - 0.6) < 0.001  # Right edge (floating point tolerance)

    def test_pan_down_shifts_crop_up(self, qtbot):
        """Test panning mouse down shifts crop window up"""
        widget = TestImagePanWidget()
        qtbot.addWidget(widget)

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

        widget.selected_element = elem
        widget.image_pan_mode = True
        widget.image_pan_start_crop = (0.2, 0.2, 0.8, 0.8)
        widget.drag_start_pos = (100, 100)
        widget.zoom_level = 1.0

        # Pan mouse 30 pixels down
        widget._handle_image_pan_move(100, 130, elem)

        # crop_dy = -30 / (150 * 1.0) = -0.2
        # new_y_min = 0.2 + (-0.2) = 0.0
        # new_y_max = 0.0 + 0.6 = 0.6
        assert elem.crop_info[1] == 0.0  # Top edge
        assert abs(elem.crop_info[3] - 0.6) < 0.001  # Bottom edge (floating point tolerance)

    def test_pan_clamps_to_image_boundaries(self, qtbot):
        """Test panning is clamped to 0-1 range"""
        widget = TestImagePanWidget()
        qtbot.addWidget(widget)

        elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
        elem.crop_info = (0.1, 0.1, 0.6, 0.6)

        widget.selected_element = elem
        widget.image_pan_mode = True
        widget.image_pan_start_crop = (0.1, 0.1, 0.6, 0.6)
        widget.drag_start_pos = (100, 100)
        widget.zoom_level = 1.0

        # Try to pan way past boundaries
        widget._handle_image_pan_move(500, 500, elem)

        # Crop should be clamped to valid 0-1 range
        assert 0.0 <= elem.crop_info[0] <= 1.0
        assert 0.0 <= elem.crop_info[1] <= 1.0
        assert 0.0 <= elem.crop_info[2] <= 1.0
        assert 0.0 <= elem.crop_info[3] <= 1.0

        # And crop window dimensions should be preserved
        crop_width = elem.crop_info[2] - elem.crop_info[0]
        crop_height = elem.crop_info[3] - elem.crop_info[1]
        assert abs(crop_width - 0.5) < 0.001
        assert abs(crop_height - 0.5) < 0.001

    def test_pan_respects_zoom_level(self, qtbot):
        """Test panning calculation respects zoom level"""
        widget = TestImagePanWidget()
        qtbot.addWidget(widget)

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

        widget.selected_element = elem
        widget.image_pan_mode = True
        widget.image_pan_start_crop = (0.2, 0.2, 0.8, 0.8)
        widget.drag_start_pos = (100, 100)
        widget.zoom_level = 2.0  # Zoomed in 2x

        # Pan 100 pixels right at 2x zoom
        widget._handle_image_pan_move(200, 100, elem)

        # crop_dx = -100 / (200 * 2.0) = -0.25
        # new_x_min = 0.2 + (-0.25) = -0.05 -> clamped to 0.0
        assert elem.crop_info[0] == 0.0

    def test_pan_no_op_when_not_in_pan_mode(self, qtbot):
        """Test panning does nothing when not in pan mode"""
        widget = TestImagePanWidget()
        qtbot.addWidget(widget)

        elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
        original_crop = (0.2, 0.2, 0.8, 0.8)
        elem.crop_info = original_crop

        widget.selected_element = elem
        widget.image_pan_mode = False  # Not in pan mode
        widget.drag_start_pos = (100, 100)
        widget.zoom_level = 1.0

        widget._handle_image_pan_move(200, 200, elem)

        # Crop should be unchanged
        assert elem.crop_info == original_crop

    def test_pan_no_op_on_non_image_element(self, qtbot):
        """Test panning does nothing on non-ImageData elements"""
        widget = TestImagePanWidget()
        qtbot.addWidget(widget)

        elem = PlaceholderData(x=100, y=100, width=200, height=150)

        widget.image_pan_mode = True
        widget.drag_start_pos = (100, 100)
        widget.zoom_level = 1.0

        # Should not crash, just do nothing
        widget._handle_image_pan_move(200, 200, elem)

    def test_pan_no_op_without_drag_start(self, qtbot):
        """Test panning does nothing without drag start position"""
        widget = TestImagePanWidget()
        qtbot.addWidget(widget)

        elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
        original_crop = (0.2, 0.2, 0.8, 0.8)
        elem.crop_info = original_crop

        widget.selected_element = elem
        widget.image_pan_mode = True
        widget.drag_start_pos = None  # No drag start
        widget.zoom_level = 1.0

        widget._handle_image_pan_move(200, 200, elem)

        # Crop should be unchanged
        assert elem.crop_info == original_crop

    def test_pan_uses_default_crop_when_none(self, qtbot):
        """Test panning uses (0,0,1,1) when start crop is None"""
        widget = TestImagePanWidget()
        qtbot.addWidget(widget)

        elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
        elem.crop_info = (0, 0, 1, 1)

        widget.selected_element = elem
        widget.image_pan_mode = True
        widget.image_pan_start_crop = None  # No start crop
        widget.drag_start_pos = (100, 100)
        widget.zoom_level = 1.0

        # Pan 100 pixels right
        widget._handle_image_pan_move(200, 100, elem)

        # Should use full image as start (crop_width = 1.0)
        # crop_dx = -100 / 200 = -0.5
        # new_x_min = 0 + (-0.5) = -0.5 -> clamped to 0
        # new_x_max = 0 + 1.0 = 1.0
        assert elem.crop_info[0] == 0.0
        assert elem.crop_info[2] == 1.0

    def test_pan_maintains_crop_dimensions(self, qtbot):
        """Test panning maintains the crop window dimensions"""
        widget = TestImagePanWidget()
        qtbot.addWidget(widget)

        elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
        original_crop = (0.2, 0.3, 0.7, 0.8)  # width=0.5, height=0.5
        elem.crop_info = original_crop

        widget.selected_element = elem
        widget.image_pan_mode = True
        widget.image_pan_start_crop = original_crop
        widget.drag_start_pos = (100, 100)
        widget.zoom_level = 1.0

        # Pan 20 pixels right and 15 pixels down
        widget._handle_image_pan_move(120, 115, elem)

        # Crop dimensions should remain the same
        new_crop = elem.crop_info
        new_width = new_crop[2] - new_crop[0]
        new_height = new_crop[3] - new_crop[1]

        original_width = original_crop[2] - original_crop[0]
        original_height = original_crop[3] - original_crop[1]

        assert abs(new_width - original_width) < 0.001
        assert abs(new_height - original_height) < 0.001

    def test_pan_left_boundary_clamping(self, qtbot):
        """Test panning respects left boundary"""
        widget = TestImagePanWidget()
        qtbot.addWidget(widget)

        elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
        elem.crop_info = (0.5, 0.2, 1.0, 0.8)  # Right half

        widget.selected_element = elem
        widget.image_pan_mode = True
        widget.image_pan_start_crop = (0.5, 0.2, 1.0, 0.8)
        widget.drag_start_pos = (100, 100)
        widget.zoom_level = 1.0

        # Try to pan left beyond boundary (pan mouse left = positive crop delta)
        widget._handle_image_pan_move(50, 100, elem)

        # crop_dx = -(-50) / 200 = 0.25
        # new_x_min = 0.5 + 0.25 = 0.75
        # But if we go further...
        widget.drag_start_pos = (100, 100)
        widget._handle_image_pan_move(0, 100, elem)

        # crop_dx = -(-100) / 200 = 0.5
        # new_x_min = 0.5 + 0.5 = 1.0
        # new_x_max = 1.0 + 0.5 = 1.5 -> should clamp
        assert elem.crop_info[2] == 1.0  # Right boundary
        assert elem.crop_info[0] == 0.5  # 1.0 - crop_width
