"""
Unit tests for pyPhotoAlbum snapping system
"""

import pytest
from pyPhotoAlbum.snapping import SnappingSystem, Guide


class TestGuide:
    """Tests for Guide class"""

    def test_guide_initialization(self):
        """Test Guide initialization"""
        guide = Guide(position=50.0, orientation="vertical")
        assert guide.position == 50.0
        assert guide.orientation == "vertical"

    def test_guide_serialization(self):
        """Test Guide serialization to dictionary"""
        guide = Guide(position=75.5, orientation="horizontal")
        data = guide.serialize()

        assert data["position"] == 75.5
        assert data["orientation"] == "horizontal"

    def test_guide_deserialization(self):
        """Test Guide deserialization from dictionary"""
        data = {"position": 100.0, "orientation": "vertical"}
        guide = Guide.deserialize(data)

        assert guide.position == 100.0
        assert guide.orientation == "vertical"

    def test_guide_deserialization_with_defaults(self):
        """Test Guide deserialization with missing fields uses defaults"""
        data = {}
        guide = Guide.deserialize(data)

        assert guide.position == 0
        assert guide.orientation == "vertical"


class TestSnappingSystem:
    """Tests for SnappingSystem class"""

    def test_initialization_default(self):
        """Test SnappingSystem initialization with default values"""
        system = SnappingSystem()

        assert system.snap_threshold_mm == 5.0
        assert system.grid_size_mm == 10.0
        assert system.snap_to_grid == False
        assert system.snap_to_edges == True
        assert system.snap_to_guides == True
        assert len(system.guides) == 0

    def test_initialization_with_threshold(self):
        """Test SnappingSystem initialization with custom threshold"""
        system = SnappingSystem(snap_threshold_mm=3.0)
        assert system.snap_threshold_mm == 3.0

    def test_add_guide(self):
        """Test adding a guide"""
        system = SnappingSystem()
        guide = system.add_guide(position=50.0, orientation="vertical")

        assert len(system.guides) == 1
        assert guide.position == 50.0
        assert guide.orientation == "vertical"
        assert guide in system.guides

    def test_add_multiple_guides(self):
        """Test adding multiple guides"""
        system = SnappingSystem()
        guide1 = system.add_guide(position=50.0, orientation="vertical")
        guide2 = system.add_guide(position=100.0, orientation="horizontal")
        guide3 = system.add_guide(position=150.0, orientation="vertical")

        assert len(system.guides) == 3
        assert guide1 in system.guides
        assert guide2 in system.guides
        assert guide3 in system.guides

    def test_remove_guide(self):
        """Test removing a guide"""
        system = SnappingSystem()
        guide = system.add_guide(position=50.0, orientation="vertical")

        system.remove_guide(guide)
        assert len(system.guides) == 0
        assert guide not in system.guides

    def test_remove_guide_not_in_list(self):
        """Test removing a guide that's not in the list does nothing"""
        system = SnappingSystem()
        guide1 = system.add_guide(position=50.0, orientation="vertical")
        guide2 = Guide(position=100.0, orientation="horizontal")

        # Should not raise an error
        system.remove_guide(guide2)
        assert len(system.guides) == 1
        assert guide1 in system.guides

    def test_clear_guides(self):
        """Test clearing all guides"""
        system = SnappingSystem()
        system.add_guide(position=50.0, orientation="vertical")
        system.add_guide(position=100.0, orientation="horizontal")
        system.add_guide(position=150.0, orientation="vertical")

        system.clear_guides()
        assert len(system.guides) == 0

    def test_snap_position_no_snapping_enabled(self):
        """Test snap_position with all snapping disabled"""
        system = SnappingSystem()
        system.snap_to_grid = False
        system.snap_to_edges = False
        system.snap_to_guides = False

        position = (25.0, 35.0)
        size = (100.0, 100.0)
        page_size = (210.0, 297.0)

        snapped = system.snap_position(position, size, page_size, dpi=300)
        assert snapped == position  # Should not snap

    def test_snap_position_to_edges(self):
        """Test snap_position snapping to page edges"""
        system = SnappingSystem(snap_threshold_mm=5.0)
        system.snap_to_grid = False
        system.snap_to_edges = True
        system.snap_to_guides = False

        # Position near left edge (should snap to 0)
        position = (10.0, 50.0)  # Close to 0 in pixels
        size = (100.0, 100.0)
        page_size = (210.0, 297.0)  # A4 size in mm

        snapped = system.snap_position(position, size, page_size, dpi=300)
        assert snapped[0] == 0  # Should snap to left edge

    def test_snap_position_to_grid(self):
        """Test snap_position snapping to grid"""
        system = SnappingSystem(snap_threshold_mm=5.0)
        system.snap_to_grid = True
        system.snap_to_edges = False
        system.snap_to_guides = False
        system.grid_size_mm = 10.0

        # Position near a grid line
        dpi = 300
        grid_size_px = 10.0 * dpi / 25.4  # ~118 pixels

        position = (grid_size_px + 5, grid_size_px + 5)  # Close to a grid point
        size = (100.0, 100.0)
        page_size = (210.0, 297.0)

        snapped = system.snap_position(position, size, page_size, dpi=dpi)

        # Should snap to nearest grid line
        assert abs(snapped[0] - grid_size_px) < 1  # Allow small floating point error
        assert abs(snapped[1] - grid_size_px) < 1

    def test_snap_position_to_guides(self):
        """Test snap_position snapping to guides"""
        system = SnappingSystem(snap_threshold_mm=5.0)
        system.snap_to_grid = False
        system.snap_to_edges = False
        system.snap_to_guides = True

        dpi = 300
        guide_pos_mm = 50.0
        guide_pos_px = guide_pos_mm * dpi / 25.4

        system.add_guide(position=guide_pos_mm, orientation="vertical")
        system.add_guide(position=guide_pos_mm, orientation="horizontal")

        # Position near the guides
        position = (guide_pos_px + 5, guide_pos_px + 5)
        size = (100.0, 100.0)
        page_size = (210.0, 297.0)

        snapped = system.snap_position(position, size, page_size, dpi=dpi)

        # Should snap to guides
        assert abs(snapped[0] - guide_pos_px) < 1
        assert abs(snapped[1] - guide_pos_px) < 1

    def test_snap_position_outside_threshold(self):
        """Test snap_position when position is outside snap threshold"""
        system = SnappingSystem(snap_threshold_mm=2.0)  # Small threshold
        system.snap_to_edges = True

        # Position far from edges
        position = (500.0, 600.0)
        size = (100.0, 100.0)
        page_size = (210.0, 297.0)

        snapped = system.snap_position(position, size, page_size, dpi=300)
        assert snapped == position  # Should not snap when too far

    def test_snap_resize_bottom_right_handle(self):
        """Test snap_resize with bottom-right handle"""
        from pyPhotoAlbum.snapping import SnapResizeParams

        system = SnappingSystem(snap_threshold_mm=5.0)
        system.snap_to_grid = True
        system.grid_size_mm = 10.0

        position = (100.0, 100.0)
        size = (200.0, 200.0)
        dx = 10.0
        dy = 10.0
        resize_handle = "se"
        page_size = (210.0, 297.0)

        params = SnapResizeParams(
            position=position, size=size, dx=dx, dy=dy, resize_handle=resize_handle, page_size=page_size, dpi=300
        )
        new_pos, new_size = system.snap_resize(params)

        # Position shouldn't change for bottom-right handle
        assert new_pos == position
        # Size should change
        assert new_size[0] > size[0]
        assert new_size[1] > size[1]

    def test_snap_resize_top_left_handle(self):
        """Test snap_resize with top-left handle"""
        from pyPhotoAlbum.snapping import SnapResizeParams

        system = SnappingSystem(snap_threshold_mm=5.0)
        system.snap_to_edges = True

        position = (150.0, 150.0)
        size = (200.0, 200.0)
        dx = -10.0
        dy = -10.0
        resize_handle = "nw"
        page_size = (210.0, 297.0)

        params = SnapResizeParams(
            position=position, size=size, dx=dx, dy=dy, resize_handle=resize_handle, page_size=page_size, dpi=300
        )
        new_pos, new_size = system.snap_resize(params)

        # Both position and size should change for top-left handle
        assert new_pos != position
        assert new_size != size

    def test_snap_resize_top_handle(self):
        """Test snap_resize with top handle only"""
        from pyPhotoAlbum.snapping import SnapResizeParams

        system = SnappingSystem(snap_threshold_mm=5.0)
        system.snap_to_edges = True

        position = (100.0, 100.0)
        size = (200.0, 200.0)
        dx = 0.0
        dy = -10.0
        resize_handle = "n"
        page_size = (210.0, 297.0)

        params = SnapResizeParams(
            position=position, size=size, dx=dx, dy=dy, resize_handle=resize_handle, page_size=page_size, dpi=300
        )
        new_pos, new_size = system.snap_resize(params)

        # X position should stay same, Y should change
        assert new_pos[0] == position[0]
        assert new_pos[1] != position[1]
        # Width should stay same, height should change
        assert new_size[0] == size[0]
        assert new_size[1] != size[1]

    def test_snap_resize_right_handle(self):
        """Test snap_resize with right handle only"""
        from pyPhotoAlbum.snapping import SnapResizeParams

        system = SnappingSystem(snap_threshold_mm=5.0)
        system.snap_to_edges = True

        position = (100.0, 100.0)
        size = (200.0, 200.0)
        dx = 10.0
        dy = 0.0
        resize_handle = "e"
        page_size = (210.0, 297.0)

        params = SnapResizeParams(
            position=position, size=size, dx=dx, dy=dy, resize_handle=resize_handle, page_size=page_size, dpi=300
        )
        new_pos, new_size = system.snap_resize(params)

        # Position should stay same
        assert new_pos == position
        # Width should change, height should stay same
        assert new_size[0] != size[0]
        assert new_size[1] == size[1]

    def test_snap_resize_minimum_size(self):
        """Test snap_resize enforces minimum size"""
        from pyPhotoAlbum.snapping import SnapResizeParams

        system = SnappingSystem(snap_threshold_mm=5.0)
        system.snap_to_edges = False

        position = (100.0, 100.0)
        size = (50.0, 50.0)
        dx = -100.0  # Try to make it very small
        dy = -100.0
        resize_handle = "se"
        page_size = (210.0, 297.0)

        params = SnapResizeParams(
            position=position, size=size, dx=dx, dy=dy, resize_handle=resize_handle, page_size=page_size, dpi=300
        )
        new_pos, new_size = system.snap_resize(params)

        # Should enforce minimum size of 10 pixels
        assert new_size[0] >= 10
        assert new_size[1] >= 10

    def test_snap_resize_all_handles(self):
        """Test snap_resize works with all handle types"""
        from pyPhotoAlbum.snapping import SnapResizeParams

        system = SnappingSystem(snap_threshold_mm=5.0)
        system.snap_to_edges = False

        position = (100.0, 100.0)
        size = (200.0, 200.0)
        dx = 10.0
        dy = 10.0
        page_size = (210.0, 297.0)

        handles = ["nw", "n", "ne", "e", "se", "s", "sw", "w"]

        for handle in handles:
            params = SnapResizeParams(
                position=position, size=size, dx=dx, dy=dy, resize_handle=handle, page_size=page_size, dpi=300
            )
            new_pos, new_size = system.snap_resize(params)
            # Should return valid position and size
            assert isinstance(new_pos, tuple)
            assert len(new_pos) == 2
            assert isinstance(new_size, tuple)
            assert len(new_size) == 2
            assert new_size[0] >= 10  # Minimum size
            assert new_size[1] >= 10

    def test_get_snap_lines_empty(self):
        """Test get_snap_lines with no snapping enabled"""
        system = SnappingSystem()
        system.snap_to_grid = False
        system.snap_to_edges = False
        system.snap_to_guides = False

        page_size = (210.0, 297.0)
        lines = system.get_snap_lines(page_size, dpi=300)

        assert lines["grid"] == []
        assert lines["edges"] == []
        assert lines["guides"] == []

    def test_get_snap_lines_with_grid(self):
        """Test get_snap_lines with grid enabled"""
        system = SnappingSystem()
        system.snap_to_grid = True
        system.grid_size_mm = 10.0

        page_size = (30.0, 30.0)  # Small page for easier testing
        lines = system.get_snap_lines(page_size, dpi=300)

        # Should have grid lines
        assert len(lines["grid"]) > 0

        # Should have both vertical and horizontal grid lines
        vertical_lines = [line for line in lines["grid"] if line[0] == "vertical"]
        horizontal_lines = [line for line in lines["grid"] if line[0] == "horizontal"]
        assert len(vertical_lines) > 0
        assert len(horizontal_lines) > 0

    def test_get_snap_lines_with_edges(self):
        """Test get_snap_lines with edge snapping enabled"""
        system = SnappingSystem()
        system.snap_to_edges = True

        page_size = (210.0, 297.0)
        lines = system.get_snap_lines(page_size, dpi=300)

        # Should have exactly 4 edge lines (left, right, top, bottom)
        assert len(lines["edges"]) == 4

        # Check for vertical edges
        vertical_edges = [line for line in lines["edges"] if line[0] == "vertical"]
        assert len(vertical_edges) == 2

        # Check for horizontal edges
        horizontal_edges = [line for line in lines["edges"] if line[0] == "horizontal"]
        assert len(horizontal_edges) == 2

    def test_get_snap_lines_with_guides(self):
        """Test get_snap_lines with guides"""
        system = SnappingSystem()
        system.snap_to_guides = True

        system.add_guide(position=50.0, orientation="vertical")
        system.add_guide(position=100.0, orientation="horizontal")
        system.add_guide(position=150.0, orientation="vertical")

        page_size = (210.0, 297.0)
        lines = system.get_snap_lines(page_size, dpi=300)

        # Should have guide lines
        assert len(lines["guides"]) == 3

        # Check orientations
        vertical_guides = [line for line in lines["guides"] if line[0] == "vertical"]
        horizontal_guides = [line for line in lines["guides"] if line[0] == "horizontal"]
        assert len(vertical_guides) == 2
        assert len(horizontal_guides) == 1

    def test_serialization(self):
        """Test SnappingSystem serialization to dictionary"""
        system = SnappingSystem(snap_threshold_mm=3.0)
        system.grid_size_mm = 15.0
        system.snap_to_grid = True
        system.snap_to_edges = False
        system.snap_to_guides = True

        system.add_guide(position=50.0, orientation="vertical")
        system.add_guide(position=100.0, orientation="horizontal")

        data = system.serialize()

        assert data["snap_threshold_mm"] == 3.0
        assert data["grid_size_mm"] == 15.0
        assert data["snap_to_grid"] == True
        assert data["snap_to_edges"] == False
        assert data["snap_to_guides"] == True
        assert len(data["guides"]) == 2

    def test_deserialization(self):
        """Test SnappingSystem deserialization from dictionary"""
        system = SnappingSystem()

        data = {
            "snap_threshold_mm": 4.0,
            "grid_size_mm": 20.0,
            "snap_to_grid": True,
            "snap_to_edges": False,
            "snap_to_guides": True,
            "guides": [{"position": 50.0, "orientation": "vertical"}, {"position": 100.0, "orientation": "horizontal"}],
        }

        system.deserialize(data)

        assert system.snap_threshold_mm == 4.0
        assert system.grid_size_mm == 20.0
        assert system.snap_to_grid == True
        assert system.snap_to_edges == False
        assert system.snap_to_guides == True
        assert len(system.guides) == 2
        assert system.guides[0].position == 50.0
        assert system.guides[0].orientation == "vertical"
        assert system.guides[1].position == 100.0
        assert system.guides[1].orientation == "horizontal"

    def test_deserialization_with_defaults(self):
        """Test SnappingSystem deserialization with missing fields uses defaults"""
        system = SnappingSystem()
        data = {}

        system.deserialize(data)

        assert system.snap_threshold_mm == 5.0
        assert system.grid_size_mm == 10.0
        assert system.snap_to_grid == False
        assert system.snap_to_edges == True
        assert system.snap_to_guides == True
        assert len(system.guides) == 0

    def test_serialize_deserialize_roundtrip(self):
        """Test that serialize and deserialize are inverse operations"""
        original = SnappingSystem(snap_threshold_mm=7.5)
        original.grid_size_mm = 12.5
        original.snap_to_grid = True
        original.snap_to_edges = True
        original.snap_to_guides = False

        original.add_guide(position=25.5, orientation="vertical")
        original.add_guide(position=75.5, orientation="horizontal")
        original.add_guide(position=125.5, orientation="vertical")

        data = original.serialize()
        restored = SnappingSystem()
        restored.deserialize(data)

        assert restored.snap_threshold_mm == original.snap_threshold_mm
        assert restored.grid_size_mm == original.grid_size_mm
        assert restored.snap_to_grid == original.snap_to_grid
        assert restored.snap_to_edges == original.snap_to_edges
        assert restored.snap_to_guides == original.snap_to_guides
        assert len(restored.guides) == len(original.guides)

        for orig_guide, rest_guide in zip(original.guides, restored.guides):
            assert rest_guide.position == orig_guide.position
            assert rest_guide.orientation == orig_guide.orientation
