"""
Unit tests for ElementMaximizer class.
Tests each atomic method independently for better test coverage and debugging.
"""

import pytest
from unittest.mock import Mock
from pyPhotoAlbum.alignment import ElementMaximizer
from pyPhotoAlbum.models import BaseLayoutElement


class TestElementMaximizer:
    """Test suite for ElementMaximizer class."""

    @pytest.fixture
    def mock_element(self):
        """Create a mock element for testing."""
        elem = Mock(spec=BaseLayoutElement)
        elem.position = (10.0, 10.0)
        elem.size = (50.0, 50.0)
        return elem

    @pytest.fixture
    def simple_elements(self):
        """Create a simple list of mock elements."""
        elem1 = Mock(spec=BaseLayoutElement)
        elem1.position = (10.0, 10.0)
        elem1.size = (50.0, 50.0)

        elem2 = Mock(spec=BaseLayoutElement)
        elem2.position = (70.0, 10.0)
        elem2.size = (50.0, 50.0)

        return [elem1, elem2]

    @pytest.fixture
    def maximizer(self, simple_elements):
        """Create an ElementMaximizer instance with simple elements."""
        page_size = (200.0, 200.0)
        min_gap = 5.0
        return ElementMaximizer(simple_elements, page_size, min_gap)

    def test_init_records_initial_states(self, simple_elements):
        """Test that __init__ records initial states correctly."""
        page_size = (200.0, 200.0)
        min_gap = 5.0
        maximizer = ElementMaximizer(simple_elements, page_size, min_gap)

        assert len(maximizer.changes) == 2
        assert maximizer.changes[0][0] is simple_elements[0]
        assert maximizer.changes[0][1] == (10.0, 10.0)  # position
        assert maximizer.changes[0][2] == (50.0, 50.0)  # size

    def test_check_collision_with_left_boundary(self, maximizer):
        """Test collision detection with left page boundary."""
        # Position element too close to left edge
        maximizer.elements[0].position = (2.0, 10.0)
        new_size = (50.0, 50.0)

        assert maximizer.check_collision(0, new_size) is True

    def test_check_collision_with_top_boundary(self, maximizer):
        """Test collision detection with top page boundary."""
        # Position element too close to top edge
        maximizer.elements[0].position = (10.0, 2.0)
        new_size = (50.0, 50.0)

        assert maximizer.check_collision(0, new_size) is True

    def test_check_collision_with_right_boundary(self, maximizer):
        """Test collision detection with right page boundary."""
        # Element would extend beyond right boundary
        maximizer.elements[0].position = (150.0, 10.0)
        new_size = (50.0, 50.0)  # 150 + 50 = 200, needs min_gap

        assert maximizer.check_collision(0, new_size) is True

    def test_check_collision_with_bottom_boundary(self, maximizer):
        """Test collision detection with bottom page boundary."""
        # Element would extend beyond bottom boundary
        maximizer.elements[0].position = (10.0, 150.0)
        new_size = (50.0, 50.0)  # 150 + 50 = 200, needs min_gap

        assert maximizer.check_collision(0, new_size) is True

    def test_check_collision_with_other_element(self, maximizer):
        """Test collision detection with other elements."""
        # Make elem1 grow into elem2's space
        new_size = (65.0, 50.0)  # Would overlap with elem2 at x=70

        assert maximizer.check_collision(0, new_size) is True

    def test_check_collision_no_collision(self, maximizer):
        """Test that valid sizes don't trigger collision."""
        # Element has plenty of space
        new_size = (55.0, 55.0)

        assert maximizer.check_collision(0, new_size) is False

    def test_find_max_scale_basic(self, maximizer):
        """Test binary search finds maximum scale factor."""
        current_scale = 1.0
        max_scale = maximizer.find_max_scale(0, current_scale)

        # Should find a scale larger than 1.0 since there's room to grow
        assert max_scale > current_scale

    def test_find_max_scale_constrained_by_boundary(self):
        """Test scaling is constrained by page boundaries."""
        elem = Mock(spec=BaseLayoutElement)
        elem.position = (10.0, 10.0)
        elem.size = (80.0, 80.0)

        maximizer = ElementMaximizer([elem], (100.0, 100.0), 5.0)
        current_scale = 1.0
        max_scale = maximizer.find_max_scale(0, current_scale)

        # Element at (10,10) with size (80,80) reaches (90,90)
        # With min_gap=5, max is (95,95), so max_scale should be around 1.0625
        assert 1.0 < max_scale < 1.1

    def test_find_max_scale_constrained_by_element(self, maximizer):
        """Test scaling is constrained by nearby elements."""
        # Elements are close together, limited growth
        current_scale = 1.0
        max_scale = maximizer.find_max_scale(0, current_scale)

        # There's a gap of 10mm between elements (70-60), with min_gap=5
        # So limited growth is possible
        assert max_scale > 1.0
        assert max_scale < 1.2  # Won't grow too much

    def test_grow_iteration_with_space(self, maximizer):
        """Test grow_iteration when elements have space to grow."""
        scales = [1.0, 1.0]
        growth_rate = 0.05

        result = maximizer.grow_iteration(scales, growth_rate)

        assert result is True  # Some growth occurred
        assert scales[0] > 1.0
        assert scales[1] > 1.0

    def test_grow_iteration_no_space(self):
        """Test grow_iteration when elements have no space to grow."""
        # Create elements that fill the entire page
        elem1 = Mock(spec=BaseLayoutElement)
        elem1.position = (5.0, 5.0)
        elem1.size = (190.0, 190.0)

        maximizer = ElementMaximizer([elem1], (200.0, 200.0), 5.0)
        scales = [1.0]
        growth_rate = 0.05

        result = maximizer.grow_iteration(scales, growth_rate)

        # Should return False since no growth is possible
        assert result is False
        assert scales[0] == 1.0

    def test_check_element_collision_with_overlap(self, maximizer):
        """Test element collision detection with overlap."""
        elem = maximizer.elements[0]
        new_pos = (65.0, 10.0)  # Would overlap with elem2 at (70, 10)

        assert maximizer.check_element_collision(elem, new_pos) is True

    def test_check_element_collision_no_overlap(self, maximizer):
        """Test element collision detection without overlap."""
        elem = maximizer.elements[0]
        new_pos = (15.0, 15.0)  # Safe position

        assert maximizer.check_element_collision(elem, new_pos) is False

    def test_center_element_horizontally_centering(self):
        """Test horizontal centering when space is available."""
        elem = Mock(spec=BaseLayoutElement)
        elem.position = (20.0, 50.0)  # Off-center
        elem.size = (50.0, 50.0)

        maximizer = ElementMaximizer([elem], (200.0, 200.0), 5.0)
        maximizer.center_element_horizontally(elem)

        # Element should move towards center
        # space_left = 20 - 5 = 15
        # space_right = (200 - 5) - (20 + 50) = 125
        # adjust_x = (125 - 15) / 4 = 27.5
        # new_x should be around 47.5
        new_x = elem.position[0]
        assert new_x > 20.0  # Moved right towards center

    def test_center_element_horizontally_already_centered(self):
        """Test horizontal centering when already centered."""
        elem = Mock(spec=BaseLayoutElement)
        # Centered position: (200 - 50) / 2 = 75
        elem.position = (75.0, 50.0)
        elem.size = (50.0, 50.0)

        maximizer = ElementMaximizer([elem], (200.0, 200.0), 5.0)
        original_x = elem.position[0]
        maximizer.center_element_horizontally(elem)

        # Should stay approximately the same
        assert abs(elem.position[0] - original_x) < 1.0

    def test_center_element_vertically_centering(self):
        """Test vertical centering when space is available."""
        elem = Mock(spec=BaseLayoutElement)
        elem.position = (50.0, 20.0)  # Off-center vertically
        elem.size = (50.0, 50.0)

        maximizer = ElementMaximizer([elem], (200.0, 200.0), 5.0)
        maximizer.center_element_vertically(elem)

        # Element should move towards vertical center
        new_y = elem.position[1]
        assert new_y > 20.0  # Moved down towards center

    def test_center_element_vertically_already_centered(self):
        """Test vertical centering when already centered."""
        elem = Mock(spec=BaseLayoutElement)
        # Centered position: (200 - 50) / 2 = 75
        elem.position = (50.0, 75.0)
        elem.size = (50.0, 50.0)

        maximizer = ElementMaximizer([elem], (200.0, 200.0), 5.0)
        original_y = elem.position[1]
        maximizer.center_element_vertically(elem)

        # Should stay approximately the same
        assert abs(elem.position[1] - original_y) < 1.0

    def test_center_elements_calls_both_directions(self, maximizer):
        """Test that center_elements processes both horizontal and vertical."""
        initial_positions = [elem.position for elem in maximizer.elements]
        maximizer.center_elements()

        # At least some elements should potentially move
        # (or stay same if already centered)
        assert len(maximizer.elements) == 2

    def test_maximize_integration(self, maximizer):
        """Test the full maximize method integration."""
        initial_sizes = [elem.size for elem in maximizer.elements]

        changes = maximizer.maximize(max_iterations=50, growth_rate=0.05)

        # Should return changes for undo
        assert len(changes) == 2
        assert changes[0][1] == (10.0, 10.0)  # old position
        assert changes[0][2] == (50.0, 50.0)  # old size

        # Elements should have grown
        final_sizes = [elem.size for elem in maximizer.elements]
        assert final_sizes[0][0] >= initial_sizes[0][0]
        assert final_sizes[0][1] >= initial_sizes[0][1]

    def test_maximize_empty_elements(self):
        """Test maximize with empty element list."""
        from pyPhotoAlbum.alignment import AlignmentManager

        result = AlignmentManager.maximize_pattern([], (200.0, 200.0))
        assert result == []

    def test_maximize_single_element_grows_to_fill_page(self):
        """Test that a single element grows to fill the available page."""
        elem = Mock(spec=BaseLayoutElement)
        elem.position = (50.0, 50.0)
        elem.size = (10.0, 10.0)  # Small initial size

        maximizer = ElementMaximizer([elem], (200.0, 200.0), 5.0)
        maximizer.maximize(max_iterations=100, growth_rate=0.1)

        # Element should grow significantly
        final_width, final_height = elem.size
        assert final_width > 50.0  # Much larger than initial 10.0
        assert final_height > 50.0


class TestElementMaximizerEdgeCases:
    """Test edge cases and boundary conditions."""

    def test_zero_min_gap(self):
        """Test with zero minimum gap."""
        elem = Mock(spec=BaseLayoutElement)
        elem.position = (0.0, 0.0)
        elem.size = (100.0, 100.0)

        maximizer = ElementMaximizer([elem], (100.0, 100.0), 0.0)

        # Should not collide with boundaries at exact edges
        assert maximizer.check_collision(0, (100.0, 100.0)) is False

    def test_very_large_min_gap(self):
        """Test with very large minimum gap."""
        elem = Mock(spec=BaseLayoutElement)
        elem.position = (50.0, 50.0)
        elem.size = (10.0, 10.0)

        maximizer = ElementMaximizer([elem], (200.0, 200.0), 50.0)

        # Element at (50,50) with size (10,10) is OK since:
        # - left edge at 50 > min_gap (50)
        # - top edge at 50 > min_gap (50)
        # - right edge at 60 < page_width (200) - min_gap (50) = 150
        # Current size should NOT collide
        assert maximizer.check_collision(0, (10.0, 10.0)) is False

        # But if we try to position too close to an edge, it should collide
        elem.position = (40.0, 50.0)  # Left edge at 40 < min_gap
        assert maximizer.check_collision(0, (10.0, 10.0)) is True

    def test_elements_touching_with_exact_min_gap(self):
        """Test elements that are exactly min_gap apart."""
        elem1 = Mock(spec=BaseLayoutElement)
        elem1.position = (10.0, 10.0)
        elem1.size = (50.0, 50.0)

        elem2 = Mock(spec=BaseLayoutElement)
        elem2.position = (65.0, 10.0)  # Exactly 5mm gap (60 + 5 = 65)
        elem2.size = (50.0, 50.0)

        maximizer = ElementMaximizer([elem1, elem2], (200.0, 200.0), 5.0)

        # Should not grow since they're at minimum gap
        result = maximizer.check_collision(0, (50.0, 50.0))
        assert result is False  # Current size is OK

        # But slightly larger would collide
        result = maximizer.check_collision(0, (51.0, 50.0))
        assert result is True

    def test_find_max_scale_tolerance(self):
        """Test that binary search respects tolerance parameter."""
        elem = Mock(spec=BaseLayoutElement)
        elem.position = (10.0, 10.0)
        elem.size = (50.0, 50.0)

        maximizer = ElementMaximizer([elem], (200.0, 200.0), 5.0)

        # Test with different tolerances
        scale_loose = maximizer.find_max_scale(0, 1.0, tolerance=0.1)
        scale_tight = maximizer.find_max_scale(0, 1.0, tolerance=0.0001)

        # Tighter tolerance might find slightly different result
        # Both should be greater than 1.0
        assert scale_loose > 1.0
        assert scale_tight > 1.0
        assert abs(scale_loose - scale_tight) < 0.15  # Should be similar

    def test_grow_iteration_alternating_growth(self):
        """Test that elements can alternate growth in tight spaces."""
        # Create two elements side by side with limited space
        elem1 = Mock(spec=BaseLayoutElement)
        elem1.position = (10.0, 10.0)
        elem1.size = (40.0, 40.0)

        elem2 = Mock(spec=BaseLayoutElement)
        elem2.position = (60.0, 10.0)
        elem2.size = (40.0, 40.0)

        maximizer = ElementMaximizer([elem1, elem2], (200.0, 100.0), 5.0)
        scales = [1.0, 1.0]

        # First iteration should allow growth
        result1 = maximizer.grow_iteration(scales, 0.05)
        assert result1 is True

        # Continue growing until no more growth
        for _ in range(50):
            if not maximizer.grow_iteration(scales, 0.05):
                break

        # Both should have grown
        assert scales[0] > 1.0
        assert scales[1] > 1.0
