"""
Comprehensive tests for SnappingSystem
"""

import pytest
import math
from unittest.mock import Mock

from pyPhotoAlbum.snapping import SnappingSystem, Guide, SnapResizeParams


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

    def test_guide_creation(self):
        """Test creating a Guide"""
        guide = Guide(position=100.0, orientation="vertical")
        assert guide.position == 100.0
        assert guide.orientation == "vertical"

    def test_guide_serialize(self):
        """Test Guide serialization"""
        guide = Guide(position=50.5, orientation="horizontal")
        data = guide.serialize()

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

    def test_guide_deserialize(self):
        """Test Guide deserialization"""
        data = {"position": 75.0, "orientation": "vertical"}
        guide = Guide.deserialize(data)

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

    def test_guide_deserialize_defaults(self):
        """Test Guide deserialization with missing fields"""
        guide = Guide.deserialize({})

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


class TestSnappingSystemInit:
    """Tests for SnappingSystem initialization"""

    def test_default_init(self):
        """Test default initialization"""
        snap = SnappingSystem()

        assert snap.snap_threshold_mm == 5.0
        assert snap.grid_size_mm == 10.0
        assert snap.snap_to_grid is False
        assert snap.snap_to_edges is True
        assert snap.snap_to_guides is True
        assert snap.guides == []

    def test_custom_threshold(self):
        """Test initialization with custom threshold"""
        snap = SnappingSystem(snap_threshold_mm=10.0)
        assert snap.snap_threshold_mm == 10.0


class TestGuideManagement:
    """Tests for guide management methods"""

    def test_add_guide(self):
        """Test adding a guide"""
        snap = SnappingSystem()
        guide = snap.add_guide(100.0, "vertical")

        assert len(snap.guides) == 1
        assert snap.guides[0].position == 100.0
        assert snap.guides[0].orientation == "vertical"

    def test_add_multiple_guides(self):
        """Test adding multiple guides"""
        snap = SnappingSystem()
        snap.add_guide(50.0, "horizontal")
        snap.add_guide(100.0, "vertical")
        snap.add_guide(150.0, "horizontal")

        assert len(snap.guides) == 3

    def test_remove_guide(self):
        """Test removing a guide"""
        snap = SnappingSystem()
        guide = snap.add_guide(100.0, "vertical")

        snap.remove_guide(guide)

        assert len(snap.guides) == 0

    def test_remove_nonexistent_guide(self):
        """Test removing a guide that doesn't exist"""
        snap = SnappingSystem()
        guide = Guide(position=100.0, orientation="vertical")

        # Should not raise exception
        snap.remove_guide(guide)
        assert len(snap.guides) == 0

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

        snap.clear_guides()

        assert len(snap.guides) == 0


class TestSnapPosition:
    """Tests for snap_position method"""

    def test_no_snapping_when_disabled(self):
        """Test that no snapping occurs when all snapping is disabled"""
        snap = SnappingSystem()
        snap.snap_to_grid = False
        snap.snap_to_edges = False
        snap.snap_to_guides = False

        position = (100, 100)
        size = (50, 50)
        page_size = (210, 297)  # A4 in mm

        result = snap.snap_position(position, size, page_size)

        assert result == position

    def test_snap_to_left_edge(self):
        """Test snapping to left page edge"""
        snap = SnappingSystem(snap_threshold_mm=10.0)
        snap.snap_to_edges = True
        snap.snap_to_grid = False
        snap.snap_to_guides = False

        # Position close to left edge (within threshold)
        # At 300 DPI, 10mm = ~118 pixels
        position = (5, 100)  # Very close to left edge (0)
        size = (50, 50)
        page_size = (210, 297)

        result = snap.snap_position(position, size, page_size)

        # Should snap to 0 (left edge)
        assert result[0] == 0

    def test_snap_to_right_edge(self):
        """Test snapping to right page edge"""
        snap = SnappingSystem(snap_threshold_mm=10.0)
        snap.snap_to_edges = True
        snap.snap_to_grid = False
        snap.snap_to_guides = False

        dpi = 300
        page_width_mm = 210
        page_width_px = page_width_mm * dpi / 25.4
        element_width = 50

        # Position close to right edge
        position = (page_width_px - element_width - 5, 100)
        size = (element_width, 50)
        page_size = (page_width_mm, 297)

        result = snap.snap_position(position, size, page_size, dpi)

        # Should snap so element's right edge aligns with page right edge
        expected_x = page_width_px - element_width
        assert abs(result[0] - expected_x) < 1

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

        dpi = 300
        grid_size_px = 10.0 * dpi / 25.4  # ~118 pixels

        # Position slightly off grid
        position = (grid_size_px + 10, grid_size_px + 10)
        size = (50, 50)
        page_size = (210, 297)

        result = snap.snap_position(position, size, page_size, dpi)

        # Should snap to nearest grid intersection
        assert abs(result[0] - grid_size_px) < 1 or abs(result[0] - 2 * grid_size_px) < 1

    def test_snap_to_guides(self):
        """Test snapping to guides"""
        snap = SnappingSystem(snap_threshold_mm=10.0)
        snap.snap_to_edges = False
        snap.snap_to_grid = False
        snap.snap_to_guides = True

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

        snap.add_guide(guide_pos_mm, "vertical")
        snap.add_guide(guide_pos_mm, "horizontal")

        # Position close to guide intersection
        position = (guide_pos_px + 5, guide_pos_px + 5)
        size = (50, 50)
        page_size = (210, 297)

        result = snap.snap_position(position, size, page_size, dpi)

        # Should snap to guide intersection
        assert abs(result[0] - guide_pos_px) < 1 or abs(result[1] - guide_pos_px) < 1

    def test_snap_uses_euclidean_distance(self):
        """Test that snapping uses Euclidean distance for point selection"""
        snap = SnappingSystem(snap_threshold_mm=20.0)
        snap.snap_to_edges = True
        snap.snap_to_grid = False
        snap.snap_to_guides = False

        # Position close to origin - should snap to (0, 0)
        # At 300 DPI, 20mm threshold = ~236 pixels
        # Position (50, 50) has euclidean distance ~70.7 from (0, 0)
        # which is well within the threshold
        position = (50, 50)
        size = (50, 50)
        page_size = (210, 297)
        dpi = 300

        result = snap.snap_position(position, size, page_size, dpi)

        # Should snap to (0, 0) corner as it's closest and within threshold
        # Note: snap_position considers multiple snap points; check we got one of them
        assert result[0] == 0 or result[1] == 0, f"Expected at least one axis to snap to 0, got {result}"

    def test_snap_with_project_settings(self):
        """Test snapping with project settings override"""
        snap = SnappingSystem()
        snap.snap_to_grid = False  # Local setting

        mock_project = Mock()
        mock_project.snap_to_grid = True
        mock_project.snap_to_edges = False
        mock_project.snap_to_guides = False
        mock_project.grid_size_mm = 10.0
        mock_project.snap_threshold_mm = 5.0

        dpi = 300
        grid_size_px = 10.0 * dpi / 25.4

        # Position near grid line
        position = (grid_size_px + 5, grid_size_px + 5)
        size = (50, 50)
        page_size = (210, 297)

        result = snap.snap_position(position, size, page_size, dpi, mock_project)

        # Should use project settings and snap to grid
        # The result should be different from input (snapped)
        assert result != position


class TestSnapResize:
    """Tests for snap_resize method"""

    def test_resize_southeast_handle(self):
        """Test resizing from SE corner"""
        snap = SnappingSystem()
        snap.snap_to_edges = False
        snap.snap_to_grid = False
        snap.snap_to_guides = False

        params = SnapResizeParams(
            position=(100, 100), size=(100, 100), dx=50, dy=50, resize_handle="se", page_size=(210, 297)
        )

        new_pos, new_size = snap.snap_resize(params)

        # Position should stay same for SE resize
        assert new_pos == (100, 100)
        # Size should increase
        assert new_size == (150, 150)

    def test_resize_northwest_handle(self):
        """Test resizing from NW corner"""
        snap = SnappingSystem()
        snap.snap_to_edges = False
        snap.snap_to_grid = False
        snap.snap_to_guides = False

        params = SnapResizeParams(
            position=(100, 100), size=(100, 100), dx=-20, dy=-20, resize_handle="nw", page_size=(210, 297)
        )

        new_pos, new_size = snap.snap_resize(params)

        # Position should move for NW resize
        assert new_pos == (80, 80)
        # Size should increase
        assert new_size == (120, 120)

    def test_resize_minimum_size(self):
        """Test that resize enforces minimum size"""
        snap = SnappingSystem()
        snap.snap_to_edges = False
        snap.snap_to_grid = False
        snap.snap_to_guides = False

        params = SnapResizeParams(
            position=(100, 100),
            size=(50, 50),
            dx=-100,  # Would make width negative
            dy=-100,  # Would make height negative
            resize_handle="se",
            page_size=(210, 297),
        )

        new_pos, new_size = snap.snap_resize(params)

        # Size should be clamped to minimum
        assert new_size[0] >= 10
        assert new_size[1] >= 10

    def test_resize_snap_to_edge(self):
        """Test that resize snaps edges to page boundaries"""
        snap = SnappingSystem(snap_threshold_mm=10.0)
        snap.snap_to_edges = True
        snap.snap_to_grid = False
        snap.snap_to_guides = False

        dpi = 300
        page_width_px = 210 * dpi / 25.4

        params = SnapResizeParams(
            position=(100, 100),
            size=(100, 100),
            dx=page_width_px - 200 - 5,  # Almost to right edge
            dy=0,
            resize_handle="e",
            page_size=(210, 297),
            dpi=dpi,
        )

        new_pos, new_size = snap.snap_resize(params)

        # Right edge should snap to page edge
        right_edge = new_pos[0] + new_size[0]
        assert abs(right_edge - page_width_px) < 20  # Within snap threshold


class TestSnapEdgeToTargets:
    """Tests for _snap_edge_to_targets method"""

    def test_snap_to_page_start_edge(self):
        """Test snapping to page start edge (0)"""
        snap = SnappingSystem()
        snap.snap_to_edges = True
        snap.snap_to_grid = False
        snap.snap_to_guides = False

        dpi = 300
        threshold_px = 50

        result = snap._snap_edge_to_targets(
            edge_position=10, page_size_mm=210, dpi=dpi, snap_threshold_px=threshold_px, orientation="vertical"
        )

        assert result == 0

    def test_snap_to_page_end_edge(self):
        """Test snapping to page end edge"""
        snap = SnappingSystem()
        snap.snap_to_edges = True
        snap.snap_to_grid = False
        snap.snap_to_guides = False

        dpi = 300
        page_size_mm = 210
        page_size_px = page_size_mm * dpi / 25.4
        threshold_px = 50

        result = snap._snap_edge_to_targets(
            edge_position=page_size_px - 10,
            page_size_mm=page_size_mm,
            dpi=dpi,
            snap_threshold_px=threshold_px,
            orientation="vertical",
        )

        assert result == page_size_px

    def test_snap_to_grid_line(self):
        """Test snapping to grid line"""
        snap = SnappingSystem()
        snap.snap_to_edges = False
        snap.snap_to_grid = True
        snap.snap_to_guides = False
        snap.grid_size_mm = 10.0

        dpi = 300
        grid_size_px = 10.0 * dpi / 25.4
        threshold_px = 50

        result = snap._snap_edge_to_targets(
            edge_position=grid_size_px + 5,
            page_size_mm=210,
            dpi=dpi,
            snap_threshold_px=threshold_px,
            orientation="vertical",
        )

        assert result == grid_size_px

    def test_snap_to_guide(self):
        """Test snapping to guide"""
        snap = SnappingSystem()
        snap.snap_to_edges = False
        snap.snap_to_grid = False
        snap.snap_to_guides = True

        guide_pos_mm = 50.0
        snap.add_guide(guide_pos_mm, "vertical")

        dpi = 300
        guide_pos_px = guide_pos_mm * dpi / 25.4
        threshold_px = 50

        result = snap._snap_edge_to_targets(
            edge_position=guide_pos_px + 5,
            page_size_mm=210,
            dpi=dpi,
            snap_threshold_px=threshold_px,
            orientation="vertical",
        )

        assert result == guide_pos_px

    def test_no_snap_when_out_of_threshold(self):
        """Test no snap when edge is outside threshold"""
        snap = SnappingSystem()
        snap.snap_to_edges = True
        snap.snap_to_grid = False
        snap.snap_to_guides = False

        dpi = 300
        threshold_px = 10

        result = snap._snap_edge_to_targets(
            edge_position=500,  # Far from any edge
            page_size_mm=210,
            dpi=dpi,
            snap_threshold_px=threshold_px,
            orientation="vertical",
        )

        assert result is None


class TestGetSnapLines:
    """Tests for get_snap_lines method"""

    def test_get_snap_lines_edges_only(self):
        """Test getting snap lines with edges only"""
        snap = SnappingSystem()
        snap.snap_to_edges = True
        snap.snap_to_grid = False
        snap.snap_to_guides = False

        result = snap.get_snap_lines((210, 297))

        assert len(result["edges"]) == 4  # 4 edges
        assert len(result["grid"]) == 0
        assert len(result["guides"]) == 0

    def test_get_snap_lines_with_grid(self):
        """Test getting snap lines with grid enabled"""
        snap = SnappingSystem()
        snap.snap_to_edges = False
        snap.snap_to_grid = True
        snap.snap_to_guides = False
        snap.grid_size_mm = 10.0

        result = snap.get_snap_lines((100, 100), dpi=300)

        # Should have multiple grid lines
        assert len(result["grid"]) > 0
        assert len(result["edges"]) == 0

    def test_get_snap_lines_with_guides(self):
        """Test getting snap lines with guides"""
        snap = SnappingSystem()
        snap.snap_to_edges = False
        snap.snap_to_grid = False
        snap.snap_to_guides = True

        snap.add_guide(50.0, "vertical")
        snap.add_guide(100.0, "horizontal")

        result = snap.get_snap_lines((210, 297))

        assert len(result["guides"]) == 2


class TestSerialization:
    """Tests for serialize/deserialize methods"""

    def test_serialize(self):
        """Test serialization"""
        snap = SnappingSystem(snap_threshold_mm=8.0)
        snap.grid_size_mm = 15.0
        snap.snap_to_grid = True
        snap.snap_to_edges = False
        snap.add_guide(50.0, "vertical")
        snap.add_guide(100.0, "horizontal")

        data = snap.serialize()

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

    def test_deserialize(self):
        """Test deserialization"""
        snap = SnappingSystem()

        data = {
            "snap_threshold_mm": 12.0,
            "grid_size_mm": 20.0,
            "snap_to_grid": True,
            "snap_to_edges": False,
            "snap_to_guides": False,
            "guides": [{"position": 75.0, "orientation": "vertical"}, {"position": 125.0, "orientation": "horizontal"}],
        }

        snap.deserialize(data)

        assert snap.snap_threshold_mm == 12.0
        assert snap.grid_size_mm == 20.0
        assert snap.snap_to_grid is True
        assert snap.snap_to_edges is False
        assert snap.snap_to_guides is False
        assert len(snap.guides) == 2
        assert snap.guides[0].position == 75.0
        assert snap.guides[1].orientation == "horizontal"

    def test_serialize_deserialize_roundtrip(self):
        """Test serialize/deserialize roundtrip"""
        original = SnappingSystem(snap_threshold_mm=7.5)
        original.grid_size_mm = 12.5
        original.snap_to_grid = True
        original.add_guide(33.0, "vertical")
        original.add_guide(66.0, "horizontal")

        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)

    def test_deserialize_defaults(self):
        """Test deserialization with missing fields uses defaults"""
        snap = SnappingSystem()
        snap.deserialize({})

        assert snap.snap_threshold_mm == 5.0
        assert snap.grid_size_mm == 10.0
        assert snap.snap_to_grid is False
        assert snap.snap_to_edges is True
        assert snap.snap_to_guides is True
        assert snap.guides == []
