"""
Unit tests for text_processor.py functions.
Tests the core text processing logic in isolation.
"""

import pytest
from unittest.mock import Mock, patch
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_processor import (
    wrap_text_to_width,
    render_text_with_stroke,
    calculate_text_metrics,
    apply_text_effects,
    optimize_text_layout,
    rectangles_overlap,
    find_non_overlapping_position,
    validate_text_rendering
)


class TestWrapTextToWidth:
    """Test the wrap_text_to_width function"""
    
    @pytest.mark.unit
    def test_wrap_text_short_text_fits(self, sample_font, mock_image_draw):
        """Test that short text that fits is returned as-is"""
        # Mock textbbox to return small dimensions
        mock_image_draw.textbbox.return_value = (0, 0, 50, 20)
        
        result = wrap_text_to_width(SHORT_TEXT, sample_font, 100)
        
        assert result == [SHORT_TEXT]
        assert len(result) == 1

    @pytest.mark.unit
    def test_wrap_text_empty_string(self, sample_font):
        """Test empty string input"""
        result = wrap_text_to_width("", sample_font, 100)
        assert result == [""]

    @pytest.mark.unit
    def test_wrap_text_whitespace_only(self, sample_font):
        """Test whitespace-only string"""
        result = wrap_text_to_width("   ", sample_font, 100)
        assert result == [""]

    @pytest.mark.unit
    @patch('PIL.Image.new')
    @patch('PIL.ImageDraw.Draw')
    def test_wrap_text_needs_wrapping(self, mock_draw_class, mock_image_new, sample_font):
        """Test text that needs to be wrapped"""
        # Setup mocks
        mock_img = Mock()
        mock_draw = Mock()
        mock_image_new.return_value = mock_img
        mock_draw_class.return_value = mock_draw
        
        # Mock textbbox to simulate text getting too wide
        def textbbox_side_effect(pos, text, font):
            if text == LONG_TEXT:
                return (0, 0, 300, 20)  # Too wide for max_width=100
            elif "This is a much longer" in text:
                return (0, 0, 120, 20)  # Still too wide
            elif "This is a much" in text:
                return (0, 0, 80, 20)   # Fits
            elif "longer piece of text" in text:
                return (0, 0, 90, 20)   # Fits
            else:
                return (0, 0, 50, 20)   # Default small size
                
        mock_draw.textbbox.side_effect = textbbox_side_effect
        
        result = wrap_text_to_width(LONG_TEXT, sample_font, 100)
        
        assert len(result) > 1  # Should be split into multiple lines
        assert all(isinstance(line, str) for line in result)

    @pytest.mark.unit
    def test_wrap_text_single_word_too_long(self, sample_font):
        """Test handling of single word that's too long"""
        with patch('PIL.Image.new'), patch('PIL.ImageDraw.Draw') as mock_draw_class:
            mock_draw = Mock()
            mock_draw_class.return_value = mock_draw
            mock_draw.textbbox.return_value = (0, 0, 200, 20)  # Too wide
            
            result = wrap_text_to_width("SuperLongWordThatCannotFit", sample_font, 50)
            
            # Should still return the word, even if it doesn't fit
            assert len(result) >= 1
            assert "SuperLongWordThatCannotFit" in result



class TestCalculateTextMetrics:
    """Test the calculate_text_metrics function"""
    
    @pytest.mark.unit
    def test_calculate_basic_metrics(self):
        """Test basic text metrics calculation"""
        with patch('PIL.Image.new'), patch('PIL.ImageDraw.Draw') as mock_draw_class:
            mock_draw = Mock()
            mock_draw_class.return_value = mock_draw
            mock_draw.textbbox.return_value = (2, 1, 102, 21)  # x1, y1, x2, y2
            
            # Create a mock font with getmetrics method
            mock_font = Mock()
            mock_font.getmetrics.return_value = (18, 4)  # ascent, descent
            
            result = calculate_text_metrics(SHORT_TEXT, mock_font)
            
            assert result['width'] == 100  # 102 - 2
            assert result['height'] == 20  # 21 - 1
            assert result['left_bearing'] == 2
            assert result['top_bearing'] == 1
            assert result['ascent'] == 18
            assert result['descent'] == 4

    @pytest.mark.unit
    def test_calculate_metrics_font_error(self):
        """Test metrics calculation when font.getmetrics fails"""
        with patch('PIL.Image.new'), patch('PIL.ImageDraw.Draw') as mock_draw_class:
            mock_draw = Mock()
            mock_draw_class.return_value = mock_draw
            mock_draw.textbbox.return_value = (0, 0, 100, 20)
            
            # Create a mock font with getmetrics that raises exception
            mock_font = Mock()
            mock_font.getmetrics.side_effect = Exception("Font error")
            
            result = calculate_text_metrics(SHORT_TEXT, mock_font)
            
            assert result['width'] == 100
            assert result['height'] == 20
            assert result['ascent'] == 20  # Should default to height
            assert result['descent'] == 0


class TestRectanglesOverlap:
    """Test the rectangles_overlap function"""
    
    @pytest.mark.unit
    def test_no_overlap_separate_rects(self):
        """Test rectangles that don't overlap"""
        rect1 = (0, 0, 50, 50)      # Top-left
        rect2 = (100, 100, 50, 50)  # Bottom-right, separate
        
        assert not rectangles_overlap(rect1, rect2)

    @pytest.mark.unit
    def test_clear_overlap(self):
        """Test rectangles that clearly overlap"""
        rect1 = (0, 0, 100, 100)
        rect2 = (50, 50, 100, 100)  # Overlaps in center
        
        assert rectangles_overlap(rect1, rect2)

    @pytest.mark.unit
    def test_touching_edges_no_overlap(self):
        """Test rectangles that touch but don't overlap"""
        rect1 = (0, 0, 50, 50)
        rect2 = (50, 0, 50, 50)  # Touching right edge
        
        assert not rectangles_overlap(rect1, rect2)

    @pytest.mark.unit
    def test_identical_rectangles(self):
        """Test identical rectangles (complete overlap)"""
        rect1 = (10, 10, 50, 50)
        rect2 = (10, 10, 50, 50)
        
        assert rectangles_overlap(rect1, rect2)

    @pytest.mark.unit
    def test_one_inside_other(self):
        """Test one rectangle completely inside another"""
        rect1 = (0, 0, 100, 100)    # Outer
        rect2 = (25, 25, 50, 50)    # Inner
        
        assert rectangles_overlap(rect1, rect2)


class TestFindNonOverlappingPosition:
    """Test the find_non_overlapping_position function"""
    
    @pytest.mark.unit
    def test_find_position_no_existing_blocks(self):
        """Test finding position when no existing blocks"""
        x, y = find_non_overlapping_position(100, 50, [], 500, 400)
        
        assert x == 0
        assert y == 0

    @pytest.mark.unit  
    def test_find_position_with_existing_blocks(self):
        """Test finding position avoiding existing blocks"""
        existing_blocks = [
            {'x': 0, 'y': 0, 'width': 100, 'height': 50}
        ]
        
        x, y = find_non_overlapping_position(100, 50, existing_blocks, 500, 400)
        
        # Should find a position that doesn't overlap
        assert x >= 0 and y >= 0
        # Verify it doesn't overlap with existing block
        new_rect = (x, y, 100, 50)
        existing_rect = (0, 0, 100, 50)
        assert not rectangles_overlap(new_rect, existing_rect)

    @pytest.mark.unit
    def test_find_position_no_space_available(self):
        """Test when no space is available (fallback to 0,0)"""
        # Fill entire canvas with blocks
        existing_blocks = []
        for y in range(0, 400, 20):
            for x in range(0, 500, 20):
                existing_blocks.append({'x': x, 'y': y, 'width': 20, 'height': 20})
        
        x, y = find_non_overlapping_position(100, 50, existing_blocks, 500, 400)
        
        # Should fall back to 0, 0
        assert x == 0 and y == 0


class TestValidateTextRendering:
    """Test the validate_text_rendering function"""
    
    @pytest.mark.unit
    def test_validate_valid_positions(self):
        """Test validation with valid text positions"""
        mock_img = Mock()
        mock_img.width = 500
        mock_img.height = 400
        
        positions = [
            {'x': 10, 'y': 10, 'width': 100, 'height': 50},
            {'x': 200, 'y': 200, 'width': 80, 'height': 40}
        ]
        
        results = validate_text_rendering(mock_img, positions)
        
        assert len(results) == 2
        assert all(r['valid'] for r in results)
        assert all(len(r['issues']) == 0 for r in results)

    @pytest.mark.unit
    def test_validate_negative_positions(self):
        """Test validation with negative positions"""
        mock_img = Mock()
        mock_img.width = 500
        mock_img.height = 400
        
        positions = [
            {'x': -10, 'y': 10, 'width': 100, 'height': 50}
        ]
        
        results = validate_text_rendering(mock_img, positions)
        
        assert len(results) == 1
        assert not results[0]['valid']
        assert 'Negative position' in results[0]['issues']

    @pytest.mark.unit
    def test_validate_exceeds_bounds(self):
        """Test validation with positions exceeding image bounds"""
        mock_img = Mock()
        mock_img.width = 500
        mock_img.height = 400
        
        positions = [
            {'x': 450, 'y': 10, 'width': 100, 'height': 50},  # Exceeds width
            {'x': 10, 'y': 350, 'width': 100, 'height': 100}  # Exceeds height
        ]
        
        results = validate_text_rendering(mock_img, positions)
        
        assert len(results) == 2
        assert not results[0]['valid']
        assert not results[1]['valid']
        assert 'Exceeds image width' in results[0]['issues']
        assert 'Exceeds image height' in results[1]['issues']

    @pytest.mark.unit
    def test_validate_zero_dimensions(self):
        """Test validation with zero dimensions"""
        mock_img = Mock()
        mock_img.width = 500
        mock_img.height = 400
        
        positions = [
            {'x': 10, 'y': 10, 'width': 0, 'height': 50},
            {'x': 10, 'y': 100, 'width': 100, 'height': 0}
        ]
        
        results = validate_text_rendering(mock_img, positions)
        
        assert len(results) == 2
        assert not results[0]['valid']
        assert not results[1]['valid']
        assert 'Zero dimensions' in results[0]['issues']
        assert 'Zero dimensions' in results[1]['issues']


class TestOptimizeTextLayout:
    """Test the optimize_text_layout function"""
    
    @pytest.mark.unit
    @patch('src.core.text_processor.find_non_overlapping_position')
    def test_optimize_no_overlaps(self, mock_find_pos):
        """Test optimization when no overlaps exist"""
        text_blocks = [
            {'x': 0, 'y': 0, 'width': 100, 'height': 50, 'text': 'Block 1'},
            {'x': 200, 'y': 200, 'width': 80, 'height': 40, 'text': 'Block 2'}
        ]
        
        result = optimize_text_layout(text_blocks, 500, 400)
        
        assert len(result) == 2
        # Positions should remain unchanged since no overlaps
        assert result[0]['x'] == 0 and result[0]['y'] == 0
        assert result[1]['x'] == 200 and result[1]['y'] == 200
        # find_non_overlapping_position should not be called
        mock_find_pos.assert_not_called()

    @pytest.mark.unit
    @patch('src.core.text_processor.find_non_overlapping_position')
    def test_optimize_with_overlaps(self, mock_find_pos):
        """Test optimization when overlaps exist"""
        mock_find_pos.return_value = (150, 100)  # New position
        
        text_blocks = [
            {'x': 0, 'y': 0, 'width': 100, 'height': 100, 'text': 'Block 1'},
            {'x': 50, 'y': 50, 'width': 100, 'height': 100, 'text': 'Block 2'}  # Overlaps
        ]
        
        result = optimize_text_layout(text_blocks, 500, 400)
        
        assert len(result) == 2
        # First block should remain unchanged
        assert result[0]['x'] == 0 and result[0]['y'] == 0
        # Second block should be moved to avoid overlap
        assert result[1]['x'] == 150 and result[1]['y'] == 100
        # find_non_overlapping_position should be called
        mock_find_pos.assert_called_once()

    @pytest.mark.unit
    def test_optimize_empty_list(self):
        """Test optimization with empty text blocks list"""
        result = optimize_text_layout([], 500, 400)
        
        assert result == []
