"""
Unit tests for text_fitting.py functions.
Tests the core text fitting algorithms in isolation.
"""

import pytest
from unittest.mock import Mock, patch, MagicMock
import sys
import os

# Add src to path for importing modules
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../src'))

from fixtures.sample_data import *
from src.core.text_fitting import (
    calculate_available_text_area,
    largest_rectangle_in_histogram,
    calculate_text_position,
    find_optimal_font_size,
    calculate_text_scaling,
    check_text_overlap,
    resolve_text_conflicts
)


class TestLargestRectangleInHistogram:
    """Test the largest_rectangle_in_histogram function"""
    
    @pytest.mark.unit
    def test_simple_histogram(self):
        """Test with simple histogram data"""
        heights = [3, 1, 3, 2, 2]
        result = largest_rectangle_in_histogram(heights)
        
        assert len(result) == 4  # x, y, width, height
        assert result[2] > 0  # width should be positive
        assert result[3] > 0  # height should be positive

    @pytest.mark.unit
    def test_increasing_histogram(self):
        """Test with increasing height values"""
        heights = [1, 2, 3, 4, 5]
        result = largest_rectangle_in_histogram(heights)
        
        x, y, width, height = result
        assert width > 0 and height > 0
        # The area should be reasonable
        area = width * height
        assert area > 0

    @pytest.mark.unit
    def test_empty_histogram(self):
        """Test with empty histogram"""
        heights = []
        result = largest_rectangle_in_histogram(heights)
        
        x, y, width, height = result
        assert width == 0 and height == 0

    @pytest.mark.unit
    def test_single_bar_histogram(self):
        """Test with single bar"""
        heights = [5]
        result = largest_rectangle_in_histogram(heights)
        
        x, y, width, height = result
        assert width == 1 and height == 5

    @pytest.mark.unit
    def test_zero_height_bars(self):
        """Test histogram with zero height bars"""
        heights = [0, 0, 3, 3, 0, 0]
        result = largest_rectangle_in_histogram(heights)
        
        x, y, width, height = result
        # Should find the rectangle in the middle
        assert width == 2 and height == 3

    @pytest.mark.unit
    def test_pyramid_histogram(self):
        """Test with pyramid-shaped histogram"""
        heights = [1, 2, 3, 4, 3, 2, 1]
        result = largest_rectangle_in_histogram(heights)
        
        x, y, width, height = result
        assert width > 0 and height > 0
        # Should find a reasonable rectangle


class TestCalculateAvailableTextArea:
    """Test the calculate_available_text_area function"""
    
    @pytest.mark.unit
    def test_no_overlays(self):
        """Test area calculation with no overlays"""
        width, height = 500, 400
        overlays = []
        
        result = calculate_available_text_area(width, height, overlays)
        
        assert result['width'] == width
        assert result['height'] == height
        assert result['x'] == 0
        assert result['y'] == 0
        assert result['area'] == width * height

    @pytest.mark.unit
    def test_single_overlay(self):
        """Test area calculation with single overlay"""
        width, height = 500, 400
        overlays = [{'x': 100, 'y': 100, 'width': 50, 'height': 50}]
        
        result = calculate_available_text_area(width, height, overlays)
        
        assert result['width'] > 0
        assert result['height'] > 0
        assert result['x'] >= 0
        assert result['y'] >= 0
        assert result['area'] > 0

    @pytest.mark.unit
    def test_multiple_overlays(self):
        """Test area calculation with multiple overlays"""
        width, height = 500, 400
        overlays = [
            {'x': 50, 'y': 50, 'width': 100, 'height': 100},
            {'x': 300, 'y': 200, 'width': 80, 'height': 80}
        ]
        
        result = calculate_available_text_area(width, height, overlays)
        
        assert result['width'] >= 0
        assert result['height'] >= 0
        assert result['area'] >= 0

    @pytest.mark.unit
    def test_overlays_covering_entire_area(self):
        """Test when overlays cover most of the area"""
        width, height = 100, 100
        overlays = [{'x': 0, 'y': 0, 'width': 100, 'height': 100}]
        
        result = calculate_available_text_area(width, height, overlays)
        
        # Should still return something, even if very small
        assert result['width'] >= 0
        assert result['height'] >= 0


class TestCalculateTextPosition:
    """Test the calculate_text_position function"""
    
    @pytest.mark.unit
    @patch('PIL.Image.new')
    @patch('PIL.ImageDraw.Draw')
    def test_center_alignment(self, mock_draw_class, mock_image_new, sample_font):
        """Test center alignment positioning"""
        mock_draw = Mock()
        mock_draw_class.return_value = mock_draw
        mock_draw.textbbox.return_value = (0, 0, 100, 20)  # 100x20 text
        
        available_area = {'x': 0, 'y': 0, 'width': 300, 'height': 100}
        
        result = calculate_text_position("Test Text", sample_font, available_area, 'center')
        
        assert result['x'] == 100  # (300 - 100) / 2
        assert result['y'] == 40   # (100 - 20) / 2
        assert result['text_width'] == 100
        assert result['text_height'] == 20

    @pytest.mark.unit
    @patch('PIL.Image.new')
    @patch('PIL.ImageDraw.Draw')
    def test_top_left_alignment(self, mock_draw_class, mock_image_new, sample_font):
        """Test top-left alignment positioning"""
        mock_draw = Mock()
        mock_draw_class.return_value = mock_draw
        mock_draw.textbbox.return_value = (0, 0, 100, 20)
        
        available_area = {'x': 10, 'y': 10, 'width': 200, 'height': 100}
        
        result = calculate_text_position("Test Text", sample_font, available_area, 'top-left')
        
        assert result['x'] == 10  # area x
        assert result['y'] == 10  # area y

    @pytest.mark.unit
    @patch('PIL.Image.new')
    @patch('PIL.ImageDraw.Draw')
    def test_bottom_right_alignment(self, mock_draw_class, mock_image_new, sample_font):
        """Test bottom-right alignment positioning"""
        mock_draw = Mock()
        mock_draw_class.return_value = mock_draw
        mock_draw.textbbox.return_value = (0, 0, 100, 20)
        
        available_area = {'x': 0, 'y': 0, 'width': 300, 'height': 100}
        
        result = calculate_text_position("Test Text", sample_font, available_area, 'bottom-right')
        
        assert result['x'] == 200  # 300 - 100
        assert result['y'] == 80   # 100 - 20

    @pytest.mark.unit
    @patch('PIL.Image.new')
    @patch('PIL.ImageDraw.Draw')
    def test_invalid_alignment_defaults_to_center(self, mock_draw_class, mock_image_new, sample_font):
        """Test invalid alignment defaults to center"""
        mock_draw = Mock()
        mock_draw_class.return_value = mock_draw
        mock_draw.textbbox.return_value = (0, 0, 100, 20)
        
        available_area = {'x': 0, 'y': 0, 'width': 300, 'height': 100}
        
        result = calculate_text_position("Test Text", sample_font, available_area, 'invalid_alignment')
        
        assert result['x'] == 100  # Center alignment
        assert result['y'] == 40


class TestFindOptimalFontSize:
    """Test the find_optimal_font_size function"""
    
    @pytest.mark.unit
    @patch('PIL.ImageFont.truetype')
    @patch('PIL.Image.new')
    @patch('PIL.ImageDraw.Draw')
    def test_find_optimal_size_basic(self, mock_draw_class, mock_image_new, mock_truetype):
        """Test basic optimal font size finding"""
        mock_draw = Mock()
        mock_draw_class.return_value = mock_draw
        
        # Mock font that gets larger as size increases
        def textbbox_side_effect(pos, text, font):
            size = font.size if hasattr(font, 'size') else 24
            return (0, 0, size * 4, size)  # width = size * 4, height = size
        
        mock_draw.textbbox.side_effect = textbbox_side_effect
        
        # Mock fonts with size attribute
        def mock_font_factory(path, size):
            font = Mock()
            font.size = size
            return font
        
        mock_truetype.side_effect = mock_font_factory
        
        result = find_optimal_font_size("Test Text", "/fake/font.ttf", 200, 50, 8, 100)
        
        assert result >= 8
        assert result <= 100
        assert result > 0

    @pytest.mark.unit
    @patch('PIL.ImageFont.truetype')
    def test_find_optimal_size_font_load_error(self, mock_truetype):
        """Test handling of font loading errors"""
        mock_truetype.side_effect = OSError("Font not found")
        
        result = find_optimal_font_size("Test Text", "/fake/font.ttf", 200, 50, 8, 100)
        
        assert result == 8  # Should return min_size on error

    @pytest.mark.unit
    @patch('PIL.ImageFont.truetype')
    @patch('PIL.Image.new')
    @patch('PIL.ImageDraw.Draw')
    def test_find_optimal_size_tight_constraints(self, mock_draw_class, mock_image_new, mock_truetype):
        """Test with very tight size constraints"""
        mock_draw = Mock()
        mock_draw_class.return_value = mock_draw
        mock_draw.textbbox.return_value = (0, 0, 300, 100)  # Very large text
        
        mock_font = Mock()
        mock_truetype.return_value = mock_font
        
        result = find_optimal_font_size("Test Text", "/fake/font.ttf", 50, 20, 8, 100)
        
        # Should return minimum size when text is too large
        assert result == 8


class TestCalculateTextScaling:
    """Test the calculate_text_scaling function"""
    
    @pytest.mark.unit
    @patch('PIL.Image.new')
    @patch('PIL.ImageDraw.Draw')
    def test_calculate_scaling_basic(self, mock_draw_class, mock_image_new, sample_font):
        """Test basic scaling calculation"""
        mock_draw = Mock()
        mock_draw_class.return_value = mock_draw
        mock_draw.textbbox.return_value = (0, 0, 100, 50)  # Current size 100x50
        
        width_scale, height_scale, uniform_scale = calculate_text_scaling(
            "Test Text", sample_font, 200, 100  # Target size 200x100
        )
        
        assert width_scale == 2.0   # 200 / 100
        assert height_scale == 2.0  # 100 / 50  
        assert uniform_scale == 2.0 # min(2.0, 2.0)

    @pytest.mark.unit
    @patch('PIL.Image.new')
    @patch('PIL.ImageDraw.Draw')
    def test_calculate_scaling_different_ratios(self, mock_draw_class, mock_image_new, sample_font):
        """Test scaling with different width/height ratios"""
        mock_draw = Mock()
        mock_draw_class.return_value = mock_draw
        mock_draw.textbbox.return_value = (0, 0, 100, 50)  # Current size 100x50
        
        width_scale, height_scale, uniform_scale = calculate_text_scaling(
            "Test Text", sample_font, 150, 200  # Target size 150x200
        )
        
        assert width_scale == 1.5   # 150 / 100
        assert height_scale == 4.0  # 200 / 50
        assert uniform_scale == 1.5 # min(1.5, 4.0) - use smaller to ensure fit

    @pytest.mark.unit
    @patch('PIL.Image.new')
    @patch('PIL.ImageDraw.Draw')
    def test_calculate_scaling_zero_dimensions(self, mock_draw_class, mock_image_new, sample_font):
        """Test scaling with zero current dimensions"""
        mock_draw = Mock()
        mock_draw_class.return_value = mock_draw
        mock_draw.textbbox.return_value = (0, 0, 0, 0)  # Zero size
        
        width_scale, height_scale, uniform_scale = calculate_text_scaling(
            "Test Text", sample_font, 100, 50
        )
        
        # Should return 1.0 for all when current dimensions are zero
        assert width_scale == 1.0
        assert height_scale == 1.0
        assert uniform_scale == 1.0


class TestCheckTextOverlap:
    """Test the check_text_overlap function"""
    
    @pytest.mark.unit
    def test_no_overlaps(self):
        """Test positions with no overlaps"""
        positions = [
            {'x': 0, 'y': 0, 'width': 100, 'height': 50},
            {'x': 150, 'y': 0, 'width': 100, 'height': 50},
            {'x': 0, 'y': 100, 'width': 100, 'height': 50}
        ]
        
        overlaps = check_text_overlap(positions)
        
        assert len(overlaps) == 0

    @pytest.mark.unit
    def test_with_overlaps(self):
        """Test positions with overlaps"""
        positions = [
            {'x': 0, 'y': 0, 'width': 100, 'height': 50},
            {'x': 50, 'y': 25, 'width': 100, 'height': 50},  # Overlaps first
            {'x': 200, 'y': 0, 'width': 100, 'height': 50}
        ]
        
        overlaps = check_text_overlap(positions)
        
        assert len(overlaps) == 1
        assert (0, 1) in overlaps

    @pytest.mark.unit
    def test_multiple_overlaps(self):
        """Test positions with multiple overlaps"""
        positions = [
            {'x': 0, 'y': 0, 'width': 100, 'height': 100},
            {'x': 50, 'y': 50, 'width': 100, 'height': 100},  # Overlaps 0
            {'x': 25, 'y': 25, 'width': 50, 'height': 50}     # Overlaps 0 and 1
        ]
        
        overlaps = check_text_overlap(positions)
        
        assert len(overlaps) >= 2  # Should detect multiple overlaps
        overlap_pairs = set(overlaps)
        assert (0, 1) in overlap_pairs or (1, 0) in overlap_pairs
        assert (0, 2) in overlap_pairs or (2, 0) in overlap_pairs

    @pytest.mark.unit
    def test_empty_positions_list(self):
        """Test with empty positions list"""
        overlaps = check_text_overlap([])
        assert len(overlaps) == 0

    @pytest.mark.unit
    def test_single_position(self):
        """Test with single position"""
        positions = [{'x': 0, 'y': 0, 'width': 100, 'height': 50}]
        overlaps = check_text_overlap(positions)
        assert len(overlaps) == 0


class TestResolveTextConflicts:
    """Test the resolve_text_conflicts function"""
    
    @pytest.mark.unit
    @patch('src.core.text_fitting.check_text_overlap')
    def test_resolve_no_conflicts(self, mock_check_overlap):
        """Test conflict resolution when no conflicts exist"""
        mock_check_overlap.return_value = []  # No overlaps
        
        positions = [
            {'x': 0, 'y': 0, 'width': 100, 'height': 50},
            {'x': 150, 'y': 0, 'width': 100, 'height': 50}
        ]
        
        result = resolve_text_conflicts(positions, 500, 400)
        
        # Positions should remain unchanged
        assert len(result) == 2
        assert result[0] == positions[0]
        assert result[1] == positions[1]

    @pytest.mark.unit
    @patch('src.core.text_fitting.check_text_overlap')
    def test_resolve_vertical_move(self, mock_check_overlap):
        """Test conflict resolution by moving vertically"""
        mock_check_overlap.return_value = [(0, 1)]  # First two positions overlap
        
        positions = [
            {'x': 0, 'y': 0, 'width': 100, 'height': 50},
            {'x': 50, 'y': 25, 'width': 100, 'height': 50}  # Overlaps first
        ]
        
        result = resolve_text_conflicts(positions, 500, 400)
        
        assert len(result) == 2
        # First position should remain unchanged
        assert result[0] == positions[0]
        # Second position should be moved down
        assert result[1]['y'] == 60  # 0 + 50 + 10 (position + height + margin)
        assert result[1]['x'] == 50  # x should remain the same

    @pytest.mark.unit
    @patch('src.core.text_fitting.check_text_overlap')
    def test_resolve_horizontal_move(self, mock_check_overlap):
        """Test conflict resolution by moving horizontally when vertical doesn't fit"""
        mock_check_overlap.return_value = [(0, 1)]
        
        positions = [
            {'x': 0, 'y': 300, 'width': 100, 'height': 50},   # Near bottom
            {'x': 50, 'y': 325, 'width': 100, 'height': 50}   # Overlaps, can't move down
        ]
        
        result = resolve_text_conflicts(positions, 500, 400)
        
        assert len(result) == 2
        # First position unchanged
        assert result[0] == positions[0]
        # Second position should be moved right since vertical move won't fit
        assert result[1]['x'] == 110  # 0 + 100 + 10 (position + width + margin)

    @pytest.mark.unit
    def test_resolve_empty_list(self):
        """Test conflict resolution with empty positions list"""
        result = resolve_text_conflicts([], 500, 400)
        assert result == []