"""Unit tests for generation_engine.py - Core texture generation logic"""

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

from src.core.generation_engine import generate_texture_image, generate_preview
from src.utils.overlay_assets import RasterizationResult
# Test configuration constants
DEFAULT_WIDTH = 512
DEFAULT_HEIGHT = 512
SMALL_WIDTH = 256
SMALL_HEIGHT = 256
PREVIEW_WIDTH = 128
PREVIEW_HEIGHT = 128
TINY_WIDTH = 64
TINY_HEIGHT = 64


def setup_pil_mocks(mock_pil_image_module, mock_pil_imagedraw_module, 
                    mock_pil_imagefont_module, width, height, 
                    textbbox_return=(0, 0, 100, 50), font_type='default'):
    """Helper to setup PIL mocks with consistent behavior.
    
    Args:
        mock_pil_image_module: Mocked PIL.Image module
        mock_pil_imagedraw_module: Mocked PIL.ImageDraw module
        mock_pil_imagefont_module: Mocked PIL.ImageFont module
        width: Image width in pixels
        height: Image height in pixels
        textbbox_return: Return value for textbbox (default: (0, 0, 100, 50))
        font_type: 'default', 'custom', or 'error' for font loading behavior
    
    Returns:
        tuple: (mock_pil_image, mock_draw, mock_font)
    """
    # Setup PIL Image mock
    mock_pil_image = MagicMock()
    mock_pil_image.size = (width, height)
    mock_pil_image.getdata.return_value = [(255, 255, 255, 255)] * (width * height)
    mock_pil_image.transpose.return_value = mock_pil_image
    mock_pil_image_module.new.return_value = mock_pil_image
    mock_pil_image_module.FLIP_TOP_BOTTOM = 1
    
    # Setup ImageDraw mock
    mock_draw = MagicMock()
    mock_draw.textbbox.return_value = textbbox_return
    mock_pil_imagedraw_module.Draw.return_value = mock_draw
    
    # Setup ImageFont mock based on font_type
    if font_type == 'default':
        mock_font = MagicMock()
        mock_pil_imagefont_module.truetype.return_value = mock_font
        mock_pil_imagefont_module.load_default.return_value = mock_font
    elif font_type == 'custom':
        mock_font = MagicMock()
        mock_pil_imagefont_module.truetype.return_value = mock_font
    elif font_type == 'error':
        mock_pil_imagefont_module.truetype.side_effect = OSError("Font not found")
        mock_font = MagicMock()
        mock_pil_imagefont_module.load_default.return_value = mock_font
    else:
        mock_font = MagicMock()
        mock_pil_imagefont_module.load_default.return_value = mock_font
    
    return mock_pil_image, mock_draw, mock_font


def setup_bpy_mocks(mock_bpy, width, height, raise_error=None):
    """Helper to setup bpy mocks with consistent behavior.
    
    Args:
        mock_bpy: Mocked bpy module
        width: Image width in pixels
        height: Image height in pixels
        raise_error: Optional exception to raise on image creation
    
    Returns:
        MagicMock: The mocked Blender image object (or None if error)
    """
    mock_bpy.data.images.__contains__.return_value = False
    
    if raise_error:
        mock_bpy.data.images.new.side_effect = raise_error
        return None
    
    mock_blender_image = MagicMock()
    mock_blender_image.pixels = [0.0] * (width * height * 4)
    mock_bpy.data.images.new.return_value = mock_blender_image
    return mock_blender_image




def create_mock_props(**kwargs):
    """Helper to create a mock props object with sensible defaults."""
    props = MagicMock()
    
    # Set default values
    props.image_overlays = kwargs.get('image_overlays', [])
    props.text = kwargs.get('text', '')
    props.prepend_text = kwargs.get('prepend_text', '')
    props.append_text = kwargs.get('append_text', '')
    props.prepend_margin = kwargs.get('prepend_margin', 50.0)
    props.append_margin = kwargs.get('append_margin', 50.0)
    props.prepend_append_layout = kwargs.get('prepend_append_layout', 'HORIZONTAL')
    props.background_transparent = kwargs.get('background_transparent', False)
    props.background_color = kwargs.get('background_color', (0.0, 0.0, 0.0, 1.0))
    props.text_color = kwargs.get('text_color', (1.0, 1.0, 1.0, 1.0))
    props.font_size = kwargs.get('font_size', 96)
    props.font_path = kwargs.get('font_path', 'default')
    props.custom_font_path = kwargs.get('custom_font_path', '')
    props.use_custom_font = kwargs.get('use_custom_font', False)
    props.enable_text_fitting = kwargs.get('enable_text_fitting', False)
    props.min_font_size = kwargs.get('min_font_size', 8)
    props.max_font_size = kwargs.get('max_font_size', 500)
    props.text_fitting_margin = kwargs.get('text_fitting_margin', 10.0)
    
    # Effect flags and properties
    props.enable_stroke = kwargs.get('enable_stroke', False)
    props.stroke_width = kwargs.get('stroke_width', 0)
    props.stroke_color = kwargs.get('stroke_color', (0.0, 0.0, 0.0, 1.0))
    
    props.enable_shadow = kwargs.get('enable_shadow', False)
    props.shadow_blur = kwargs.get('shadow_blur', 0.0)
    props.shadow_color = kwargs.get('shadow_color', (0.0, 0.0, 0.0, 1.0))
    props.shadow_offset_x = kwargs.get('shadow_offset_x', 0.0)
    props.shadow_offset_y = kwargs.get('shadow_offset_y', 0.0)
    
    props.enable_glow = kwargs.get('enable_glow', False)
    props.glow_blur = kwargs.get('glow_blur', 0.0)
    props.glow_color = kwargs.get('glow_color', (1.0, 1.0, 1.0, 1.0))
    
    props.enable_blur = kwargs.get('enable_blur', False)
    props.blur_radius = kwargs.get('blur_radius', 0.0)
    
    return props


class TestGenerateTextureImage:
    """Test suite for generate_texture_image() function"""
    
    def test_generate_simple_text(self):
        """Test basic text generation with default settings."""
        with patch('PIL.Image') as mock_pil_image_module, \
             patch('PIL.ImageDraw') as mock_pil_imagedraw_module, \
             patch('PIL.ImageFont') as mock_pil_imagefont_module, \
             patch('src.core.generation_engine.bpy') as mock_bpy:
            
            # Setup mocks using helper
            mock_pil_image, mock_draw, mock_font = setup_pil_mocks(
                mock_pil_image_module, mock_pil_imagedraw_module,
                mock_pil_imagefont_module, DEFAULT_WIDTH, DEFAULT_HEIGHT
            )
            mock_blender_image = setup_bpy_mocks(mock_bpy, DEFAULT_WIDTH, DEFAULT_HEIGHT)
            
            # Create props
            props = create_mock_props(
                text="Hello",
                background_transparent=False,
                background_color=(1.0, 1.0, 1.0, 1.0)
            )
            
            # Call function
            result, normal_map = generate_texture_image(props, DEFAULT_WIDTH, DEFAULT_HEIGHT)
            
            # Assertions
            assert result is not None
            assert normal_map is None  # Normal maps not enabled
            mock_pil_image_module.new.assert_called_once()
            assert mock_pil_imagedraw_module.Draw.call_count >= 1
            mock_draw.text.assert_called()
            mock_bpy.data.images.new.assert_called_once()
            mock_pil_image.transpose.assert_called_once()
    
    def test_generate_transparent_background(self):
        """Test generation with transparent background."""
        with patch('PIL.Image') as mock_pil_image_module, \
             patch('PIL.ImageDraw') as mock_pil_imagedraw_module, \
             patch('PIL.ImageFont') as mock_pil_imagefont_module, \
             patch('src.core.generation_engine.bpy') as mock_bpy:
            
            # Setup PIL mocks
            mock_pil_image = MagicMock()
            mock_pil_image.size = (256, 256)
            mock_pil_image.getdata.return_value = [(0, 0, 0, 0)] * (256 * 256)
            mock_pil_image.transpose.return_value = mock_pil_image
            mock_pil_image_module.new.return_value = mock_pil_image
            mock_pil_image_module.FLIP_TOP_BOTTOM = 1
            
            mock_draw = MagicMock()
            mock_draw.textbbox.return_value = (0, 0, 80, 40)
            mock_pil_imagedraw_module.Draw.return_value = mock_draw
            
            mock_font = MagicMock()
            mock_pil_imagefont_module.load_default.return_value = mock_font
            
            # Setup bpy mocks
            mock_blender_image = MagicMock()
            mock_blender_image.pixels = [0.0] * (256 * 256 * 4)
            mock_bpy.data.images.new.return_value = mock_blender_image
            mock_bpy.data.images.__contains__.return_value = False
            
            # Create props with transparent background
            props = create_mock_props(
                text="Test",
                background_transparent=True
            )
            
            # Call function
            result, normal_map = generate_texture_image(props, 256, 256)
            
            # Assertions
            assert result is not None
            # Verify RGBA mode with transparent background
            call_args = mock_pil_image_module.new.call_args
            assert call_args[0][0] == "RGBA"
            assert call_args[0][2] == (0, 0, 0, 0)

    def test_default_font_uses_scalable_size(self):
        """Ensure default font configuration respects requested font size."""
        with patch('PIL.Image') as mock_pil_image_module, \
             patch('PIL.ImageDraw') as mock_pil_imagedraw_module, \
             patch('PIL.ImageFont') as mock_pil_imagefont_module, \
             patch('src.core.generation_engine.bpy') as mock_bpy, \
             patch('src.core.generation_engine.find_default_truetype_font', return_value="/fonts/fallback.ttf", create=True):

            setup_pil_mocks(
                mock_pil_image_module,
                mock_pil_imagedraw_module,
                mock_pil_imagefont_module,
                DEFAULT_WIDTH,
                DEFAULT_HEIGHT,
                font_type='default'
            )
            setup_bpy_mocks(mock_bpy, DEFAULT_WIDTH, DEFAULT_HEIGHT)

            fallback_font = MagicMock()

            def truetype_side_effect(font_path_arg, size_arg):
                if font_path_arg is None:
                    raise TypeError("file must be a path, not None")
                return fallback_font

            mock_pil_imagefont_module.truetype.side_effect = truetype_side_effect
            mock_pil_imagefont_module.load_default.return_value = MagicMock()

            props = create_mock_props(
                text="Size Probe",
                font_path='default',
                use_custom_font=False,
                font_size=42
            )

            generate_texture_image(props, DEFAULT_WIDTH, DEFAULT_HEIGHT)

            mock_pil_imagefont_module.truetype.assert_called_with("/fonts/fallback.ttf", 42)
            mock_pil_imagefont_module.load_default.assert_not_called()

    def test_generate_colored_background(self):
        """Test generation with custom background color."""
        with patch('PIL.Image') as mock_pil_image_module, \
             patch('PIL.ImageDraw') as mock_pil_imagedraw_module, \
             patch('PIL.ImageFont') as mock_pil_imagefont_module, \
             patch('src.core.generation_engine.bpy') as mock_bpy:
            
            # Setup PIL mocks
            mock_pil_image = MagicMock()
            mock_pil_image.size = (128, 128)
            mock_pil_image.getdata.return_value = [(255, 0, 0, 255)] * (128 * 128)
            mock_pil_image.transpose.return_value = mock_pil_image
            mock_pil_image_module.new.return_value = mock_pil_image
            mock_pil_image_module.FLIP_TOP_BOTTOM = 1
            
            mock_draw = MagicMock()
            mock_draw.textbbox.return_value = (0, 0, 60, 30)
            mock_pil_imagedraw_module.Draw.return_value = mock_draw
            
            mock_font = MagicMock()
            mock_pil_imagefont_module.load_default.return_value = mock_font
            
            # Setup bpy mocks
            mock_blender_image = MagicMock()
            mock_blender_image.pixels = [0.0] * (128 * 128 * 4)
            mock_bpy.data.images.new.return_value = mock_blender_image
            mock_bpy.data.images.__contains__.return_value = False
            
            # Create props with red background
            props = create_mock_props(
                text="Red",
                background_transparent=False,
                background_color=(1.0, 0.0, 0.0, 1.0),  # Red in 0-1 range
                text_color=(1.0, 1.0, 1.0, 1.0)
            )
            
            # Call function
            result, normal_map = generate_texture_image(props, 128, 128)
            
            # Assertions
            assert result is not None
            call_args = mock_pil_image_module.new.call_args
            # Background color should be converted to 0-255 range
            assert call_args[0][2] == (255, 0, 0, 255)

    def test_empty_text(self):
        """Test handling of empty text input."""
        with patch('PIL.Image') as mock_pil_image_module, \
             patch('PIL.ImageDraw') as mock_pil_imagedraw_module, \
             patch('PIL.ImageFont') as mock_pil_imagefont_module, \
             patch('src.core.generation_engine.bpy') as mock_bpy:
            
            # Setup PIL mocks
            mock_pil_image = MagicMock()
            mock_pil_image.size = (512, 512)
            mock_pil_image.getdata.return_value = [(255, 255, 255, 255)] * (512 * 512)
            mock_pil_image.transpose.return_value = mock_pil_image
            mock_pil_image_module.new.return_value = mock_pil_image
            mock_pil_image_module.FLIP_TOP_BOTTOM = 1
            
            mock_draw = MagicMock()
            mock_pil_imagedraw_module.Draw.return_value = mock_draw
            
            # Setup bpy mocks
            mock_blender_image = MagicMock()
            mock_blender_image.pixels = [0.0] * (512 * 512 * 4)
            mock_bpy.data.images.new.return_value = mock_blender_image
            mock_bpy.data.images.__contains__.return_value = False
            
            # Create props with empty text
            props = create_mock_props(text="")
            
            # Call function
            result, normal_map = generate_texture_image(props, 512, 512)
            
            # Should still generate an image (just background)
            assert result is not None
            # text.draw should not be called for empty text
            mock_draw.text.assert_not_called()

    def test_invalid_dimensions(self):
        """Test error handling for invalid dimensions."""
        with patch('PIL.Image') as mock_pil_image_module, \
             patch('PIL.ImageDraw') as mock_pil_imagedraw_module, \
             patch('PIL.ImageFont') as mock_pil_imagefont_module, \
             patch('src.core.generation_engine.bpy') as mock_bpy:
            
            # Setup to raise an error on invalid dimensions
            mock_bpy.data.images.new.side_effect = ValueError("Invalid dimensions")
            mock_bpy.data.images.__contains__.return_value = False
            
            # Create props
            props = create_mock_props(text="Test")
            
            # Call with invalid dimensions
            result, normal_map = generate_texture_image(props, 0, 0)
            
            # Should handle error gracefully
            assert result is None
            assert normal_map is None
    
    def test_generate_with_prepend_append(self):
        """Test text generation with prepend and append text."""
        with patch('PIL.Image') as mock_pil_image_module, \
             patch('PIL.ImageDraw') as mock_pil_imagedraw_module, \
             patch('PIL.ImageFont') as mock_pil_imagefont_module, \
             patch('src.core.generation_engine.bpy') as mock_bpy:
            
            # Setup PIL mocks
            mock_pil_image = MagicMock()
            mock_pil_image.size = (512, 512)
            mock_pil_image.getdata.return_value = [(255, 255, 255, 255)] * (512 * 512)
            mock_pil_image.transpose.return_value = mock_pil_image
            mock_pil_image_module.new.return_value = mock_pil_image
            mock_pil_image_module.FLIP_TOP_BOTTOM = 1
            
            mock_draw = MagicMock()
            mock_draw.textbbox.return_value = (0, 0, 150, 50)
            mock_pil_imagedraw_module.Draw.return_value = mock_draw
            
            mock_font = MagicMock()
            mock_pil_imagefont_module.load_default.return_value = mock_font
            
            # Setup bpy mocks
            mock_blender_image = MagicMock()
            mock_blender_image.pixels = [0.0] * (512 * 512 * 4)
            mock_bpy.data.images.new.return_value = mock_blender_image
            mock_bpy.data.images.__contains__.return_value = False
            
            # Create props with prepend and append
            props = create_mock_props(
                text="Main",
                prepend_text="Pre-",
                append_text="-Post"
            )
            
            # Call function
            result, normal_map = generate_texture_image(props, 512, 512)
            
            # Assertions
            assert result is not None
            # Verify text.draw was called with combined text
            mock_draw.text.assert_called()
    
    def test_horizontal_layout(self):
        """Test horizontal text layout."""
        with patch('PIL.Image') as mock_pil_image_module, \
             patch('PIL.ImageDraw') as mock_pil_imagedraw_module, \
             patch('PIL.ImageFont') as mock_pil_imagefont_module, \
             patch('src.core.generation_engine.bpy') as mock_bpy:
            
            # Setup PIL mocks
            mock_pil_image = MagicMock()
            mock_pil_image.size = (512, 256)
            mock_pil_image.getdata.return_value = [(255, 255, 255, 255)] * (512 * 256)
            mock_pil_image.transpose.return_value = mock_pil_image
            mock_pil_image_module.new.return_value = mock_pil_image
            mock_pil_image_module.FLIP_TOP_BOTTOM = 1
            
            mock_draw = MagicMock()
            mock_draw.textbbox.return_value = (0, 0, 100, 50)
            mock_pil_imagedraw_module.Draw.return_value = mock_draw
            
            mock_font = MagicMock()
            mock_pil_imagefont_module.load_default.return_value = mock_font
            
            # Setup bpy mocks
            mock_blender_image = MagicMock()
            mock_blender_image.pixels = [0.0] * (512 * 256 * 4)
            mock_bpy.data.images.new.return_value = mock_blender_image
            mock_bpy.data.images.__contains__.return_value = False
            
            # Create props with horizontal layout
            props = create_mock_props(
                text="Horizontal",
                prepend_append_layout='HORIZONTAL'
            )
            
            # Call function
            result, normal_map = generate_texture_image(props, 512, 256)
            
            # Assertions
            assert result is not None
    
    def test_vertical_layout(self):
        """Test vertical text layout."""
        with patch('PIL.Image') as mock_pil_image_module, \
             patch('PIL.ImageDraw') as mock_pil_imagedraw_module, \
             patch('PIL.ImageFont') as mock_pil_imagefont_module, \
             patch('src.core.generation_engine.bpy') as mock_bpy:
            
            # Setup PIL mocks
            mock_pil_image = MagicMock()
            mock_pil_image.size = (256, 512)
            mock_pil_image.getdata.return_value = [(255, 255, 255, 255)] * (256 * 512)
            mock_pil_image.transpose.return_value = mock_pil_image
            mock_pil_image_module.new.return_value = mock_pil_image
            mock_pil_image_module.FLIP_TOP_BOTTOM = 1
            
            mock_draw = MagicMock()
            mock_draw.textbbox.return_value = (0, 0, 50, 50)
            mock_pil_imagedraw_module.Draw.return_value = mock_draw
            
            mock_font = MagicMock()
            mock_pil_imagefont_module.load_default.return_value = mock_font
            
            # Setup bpy mocks
            mock_blender_image = MagicMock()
            mock_blender_image.pixels = [0.0] * (256 * 512 * 4)
            mock_bpy.data.images.new.return_value = mock_blender_image
            mock_bpy.data.images.__contains__.return_value = False
            
            # Create props with vertical layout and multiple text parts
            props = create_mock_props(
                prepend_text="Top",
                text="Middle",
                append_text="Bottom",
                prepend_append_layout='VERTICAL'
            )
            
            # Call function
            result, normal_map = generate_texture_image(props, 256, 512)

    def test_applies_absolute_overlay_to_texture(self, tmp_path):
        """Image overlays should be composited onto the generated texture."""
        Image = pytest.importorskip("PIL.Image")
        with patch('src.core.generation_engine.bpy') as mock_bpy:
            width = height = 64
            overlay_path = tmp_path / "overlay.png"
            # Create a solid red overlay image
            overlay_source = Image.new("RGBA", (16, 16), (255, 0, 0, 255))
            overlay_source.save(overlay_path)

            mock_blender_image = MagicMock()
            mock_blender_image.pixels = [0.0] * (width * height * 4)
            mock_bpy.data.images.new.return_value = mock_blender_image
            mock_bpy.data.images.__contains__.return_value = False

            props = create_mock_props(
                text="",
                background_transparent=False,
                background_color=(1.0, 1.0, 1.0, 1.0)
            )

            overlay = MagicMock()
            overlay.enabled = True
            overlay.image_path = str(overlay_path)
            overlay.component_type = 'IMAGE'
            overlay.placement_mode = 'ABSOLUTE'
            overlay.x_position = 0.5
            overlay.y_position = 0.5
            overlay.scale = 1.0
            overlay.rotation = 0.0
            overlay.z_index = 0
            overlay.text_spacing = 0.0
            overlay.image_spacing = 0.0
            props.image_overlays = [overlay]

            result, normal_map = generate_texture_image(props, width, height)

            assert result is mock_blender_image
            assert normal_map is None

            center_index = ((height // 2) * width + (width // 2)) * 4
            center_pixel = mock_blender_image.pixels[center_index:center_index + 4]
            # Center pixel should now be red from the overlay
            assert center_pixel[0] == pytest.approx(1.0, rel=1e-3)
            assert center_pixel[1] == pytest.approx(0.0, abs=1e-3)
            assert center_pixel[2] == pytest.approx(0.0, abs=1e-3)
            assert center_pixel[3] == pytest.approx(1.0, rel=1e-3)

    def test_svg_overlay_is_rasterized_and_applied(self, tmp_path):
        """SVG overlays should be rasterized before being composited."""
        Image = pytest.importorskip("PIL.Image")
        with patch('src.core.generation_engine.bpy') as mock_bpy, \
             patch('src.core.generation_engine.ensure_svg_raster') as mock_rasterize:
            width = height = 64
            svg_path = tmp_path / "overlay.svg"
            svg_path.write_text('<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"></svg>')

            raster_path = tmp_path / "overlay_raster.png"
            overlay_source = Image.new("RGBA", (16, 16), (0, 255, 0, 255))
            overlay_source.save(raster_path)

            mock_rasterize.return_value = RasterizationResult(path=str(raster_path), applied_scale=1.0)

            mock_blender_image = MagicMock()
            mock_blender_image.pixels = [0.0] * (width * height * 4)
            mock_bpy.data.images.new.return_value = mock_blender_image
            mock_bpy.data.images.__contains__.return_value = False

            props = create_mock_props(
                text="",
                background_transparent=False,
                background_color=(1.0, 1.0, 1.0, 1.0)
            )

            overlay = MagicMock()
            overlay.enabled = True
            overlay.image_path = str(svg_path)
            overlay.component_type = 'IMAGE'
            overlay.placement_mode = 'ABSOLUTE'
            overlay.x_position = 0.5
            overlay.y_position = 0.5
            overlay.scale = 1.0
            overlay.rotation = 0.0
            overlay.z_index = 0
            overlay.text_spacing = 0.0
            overlay.image_spacing = 0.0
            props.image_overlays = [overlay]

            result, normal_map = generate_texture_image(props, width, height)

            assert result is mock_blender_image
            assert normal_map is None
            mock_rasterize.assert_called_once_with(str(svg_path), overlay.scale)

            center_index = ((height // 2) * width + (width // 2)) * 4
            center_pixel = mock_blender_image.pixels[center_index:center_index + 4]
            assert center_pixel[0] == pytest.approx(0.0, abs=1e-3)
            assert center_pixel[1] == pytest.approx(1.0, rel=1e-3)
            assert center_pixel[2] == pytest.approx(0.0, abs=1e-3)
            assert center_pixel[3] == pytest.approx(1.0, rel=1e-3)

    def test_skips_overlays_without_valid_path(self, tmp_path):
        """Overlays without a valid image path should be ignored."""
        pytest.importorskip("PIL.Image")
        with patch('PIL.Image.open') as mock_image_open, \
             patch('src.core.generation_engine.bpy') as mock_bpy:
            width = height = 32

            mock_blender_image = MagicMock()
            mock_blender_image.pixels = [0.0] * (width * height * 4)
            mock_bpy.data.images.new.return_value = mock_blender_image
            mock_bpy.data.images.__contains__.return_value = False

            props = create_mock_props(
                text="",
                background_transparent=True
            )

            overlay = MagicMock()
            overlay.enabled = True
            overlay.image_path = ""
            overlay.component_type = 'IMAGE'
            overlay.placement_mode = 'ABSOLUTE'
            overlay.x_position = 0.5
            overlay.y_position = 0.5
            overlay.scale = 1.0
            overlay.rotation = 0.0
            overlay.z_index = 0
            overlay.text_spacing = 0.0
            overlay.image_spacing = 0.0
            props.image_overlays = [overlay]

            result, normal_map = generate_texture_image(props, width, height)

            assert result is mock_blender_image
            assert normal_map is None
            mock_image_open.assert_not_called()

    def test_append_overlay_positions_to_right(self, tmp_path):
        """Append overlays should be positioned to the right side with spacing applied."""
        Image = pytest.importorskip("PIL.Image")
        with patch('src.core.generation_engine.bpy') as mock_bpy:
            width = height = 64
            overlay_path = tmp_path / "append.png"
            overlay_source = Image.new("RGBA", (10, 10), (0, 255, 0, 255))
            overlay_source.save(overlay_path)

            mock_blender_image = MagicMock()
            mock_blender_image.pixels = [0.0] * (width * height * 4)
            mock_bpy.data.images.new.return_value = mock_blender_image
            mock_bpy.data.images.__contains__.return_value = False

            props = create_mock_props(
                text="",
                background_transparent=False,
                background_color=(1.0, 1.0, 1.0, 1.0)
            )

            overlay = MagicMock()
            overlay.enabled = True
            overlay.image_path = str(overlay_path)
            overlay.component_type = 'IMAGE'
            overlay.placement_mode = 'APPEND'
            overlay.text_spacing = 10.0  # push further right
            overlay.image_spacing = 0.0
            overlay.scale = 1.0
            overlay.rotation = 0.0
            overlay.z_index = 0
            overlay.x_position = 0.5
            overlay.y_position = 0.5
            props.image_overlays = [overlay]

            result, normal_map = generate_texture_image(props, width, height)

            assert result is mock_blender_image
            assert normal_map is None

            spacing_ratio = overlay.text_spacing / 100.0
            center_x = (width * 0.5) + (spacing_ratio * width) + (overlay_source.width / 2.0)
            expected_x = int(round(center_x - overlay_source.width / 2.0))
            expected_y = int(round((height * overlay.y_position) - (overlay_source.height / 2.0)))
            expected_y = max(0, min(height - overlay_source.height, expected_y))

            sample_row = expected_y + overlay_source.height // 2
            green_columns = []
            for x in range(width):
                idx = (sample_row * width + x) * 4
                pixel = mock_blender_image.pixels[idx:idx + 4]
                if pixel and pixel[1] >= 0.95 and pixel[0] <= 0.1 and pixel[2] <= 0.1:
                    green_columns.append(x)

            assert green_columns, "Append overlay pixels not found"
            assert max(green_columns) >= expected_x + overlay_source.width - 2
            assert min(green_columns) >= expected_x - 1
            assert max(green_columns) > width // 2  # should be on right half

    def test_prepend_overlay_positions_to_left(self, tmp_path):
        """Prepend overlays should be positioned to the left side with spacing applied."""
        Image = pytest.importorskip("PIL.Image")
        with patch('src.core.generation_engine.bpy') as mock_bpy:
            width = height = 64
            overlay_path = tmp_path / "prepend.png"
            overlay_source = Image.new("RGBA", (10, 10), (0, 0, 255, 255))
            overlay_source.save(overlay_path)

            mock_blender_image = MagicMock()
            mock_blender_image.pixels = [0.0] * (width * height * 4)
            mock_bpy.data.images.new.return_value = mock_blender_image
            mock_bpy.data.images.__contains__.return_value = False

            props = create_mock_props(
                text="",
                background_transparent=False,
                background_color=(1.0, 1.0, 1.0, 1.0)
            )

            overlay = MagicMock()
            overlay.enabled = True
            overlay.image_path = str(overlay_path)
            overlay.component_type = 'IMAGE'
            overlay.placement_mode = 'PREPEND'
            overlay.text_spacing = 10.0  # push further left
            overlay.image_spacing = 0.0
            overlay.scale = 1.0
            overlay.rotation = 0.0
            overlay.z_index = 0
            overlay.x_position = 0.5
            overlay.y_position = 0.5
            props.image_overlays = [overlay]

            result, normal_map = generate_texture_image(props, width, height)

            assert result is mock_blender_image
            assert normal_map is None

            spacing_ratio = overlay.text_spacing / 100.0
            center_x = (width * 0.5) - (spacing_ratio * width) - (overlay_source.width / 2.0)
            expected_x = int(round(center_x - overlay_source.width / 2.0))
            expected_y = int(round((height * overlay.y_position) - (overlay_source.height / 2.0)))
            expected_y = max(0, min(height - overlay_source.height, expected_y))

            sample_row = expected_y + overlay_source.height // 2
            blue_columns = []
            for x in range(width):
                idx = (sample_row * width + x) * 4
                pixel = mock_blender_image.pixels[idx:idx + 4]
                if pixel and pixel[2] >= 0.95 and pixel[0] <= 0.1 and pixel[1] <= 0.1:
                    blue_columns.append(x)

            assert blue_columns, "Prepend overlay pixels not found"
            assert min(blue_columns) <= expected_x + 1
            assert max(blue_columns) <= expected_x + overlay_source.width
            assert max(blue_columns) < width // 2  # should remain on left half

    def test_prepend_text_component_shares_baseline(self):
        """Prepend text components should align exactly with the main text baseline."""
        Image = pytest.importorskip("PIL.Image")
        with patch('src.core.generation_engine.bpy') as mock_bpy:
            width = height = 256

            mock_blender_image = MagicMock()
            mock_blender_image.pixels = [0.0] * (width * height * 4)
            mock_bpy.data.images.new.return_value = mock_blender_image
            mock_bpy.data.images.__contains__.return_value = False

            props = create_mock_props(
                text="World",
                background_transparent=True,
                background_color=(0.0, 0.0, 0.0, 0.0),
                text_color=(1.0, 1.0, 1.0, 1.0),
                font_size=72,
            )

            component = MagicMock()
            component.enabled = True
            component.component_type = 'TEXT'
            component.text_content = "Test"
            component.placement_mode = 'PREPEND'
            component.scale = 1.0
            component.rotation = 0.0
            component.z_index = 0
            component.text_spacing = 5.0
            component.image_spacing = 0.0
            props.image_overlays = [component]

            result, _ = generate_texture_image(props, width, height)

            assert result is mock_blender_image

            pixels = mock_blender_image.pixels

            def collect_bounds(x_start, x_end):
                ys = []
                for y in range(height):
                    for x in range(x_start, x_end):
                        idx = (y * width + x) * 4
                        alpha = pixels[idx + 3]
                        if alpha > 0.5:
                            ys.append(y)
                            break
                if not ys:
                    return None
                return min(ys), max(ys)

            left_bounds = collect_bounds(0, width // 2)
            right_bounds = collect_bounds(width // 2, width)

            assert left_bounds and right_bounds, "Expected both text regions to be present"

        left_top, left_bottom = left_bounds
        right_top, right_bottom = right_bounds

        assert abs(left_top - right_top) <= 1, (
            f"Top alignment mismatch: prepend top={left_top}, main top={right_top}"
        )

    def test_absolute_component_negative_z_draws_behind_text(self, tmp_path):
        """Absolute components with negative z-index should render behind the main text."""
        Image = pytest.importorskip("PIL.Image")
        with patch('src.core.generation_engine.bpy') as mock_bpy:
            width = height = 128
            overlay_path = tmp_path / "background_overlay.png"
            overlay_source = Image.new("RGBA", (128, 128), (255, 0, 0, 255))
            overlay_source.save(overlay_path)

            mock_blender_image = MagicMock()
            mock_blender_image.pixels = [0.0] * (width * height * 4)
            mock_bpy.data.images.new.return_value = mock_blender_image
            mock_bpy.data.images.__contains__.return_value = False

            props = create_mock_props(
                text="T",
                background_transparent=False,
                background_color=(0.0, 0.0, 0.0, 1.0),
            )

            overlay = MagicMock()
            overlay.enabled = True
            overlay.component_type = 'IMAGE'
            overlay.image_path = str(overlay_path)
            overlay.placement_mode = 'ABSOLUTE'
            overlay.x_position = 0.5
            overlay.y_position = 0.5
            overlay.scale = 1.0
            overlay.rotation = 0.0
            overlay.z_index = -2
            overlay.text_spacing = 0.0
            overlay.image_spacing = 0.0
            props.image_overlays = [overlay]

            result, normal_map = generate_texture_image(props, width, height)

            assert result is mock_blender_image
            assert normal_map is None

            pixels = mock_blender_image.pixels
            quads = [pixels[i:i+4] for i in range(0, len(pixels), 4)]
            max_brightness = max((r + g + b) / 3.0 for r, g, b, _ in quads)
            red_pixels = sum(1 for r, g, b, _ in quads if r >= 0.9 and g <= 0.3 and b <= 0.3)

            assert max_brightness > 0.45, "Text should introduce bright pixels above the overlay"
            assert red_pixels > 0, "Red overlay pixels should still be present"

    def test_text_fitting_enabled(self):
        """Test text fitting integration."""
        with patch('PIL.Image') as mock_pil_image_module, \
             patch('PIL.ImageDraw') as mock_pil_imagedraw_module, \
             patch('PIL.ImageFont') as mock_pil_imagefont_module, \
             patch('src.core.generation_engine.bpy') as mock_bpy, \
             patch('src.core.generation_engine.has_text_fitting') as mock_has_fitting:
            
            # Enable text fitting
            mock_has_fitting.return_value = True
            
            # Setup PIL mocks
            mock_pil_image = MagicMock()
            mock_pil_image.size = (512, 512)
            mock_pil_image.getdata.return_value = [(255, 255, 255, 255)] * (512 * 512)
            mock_pil_image.transpose.return_value = mock_pil_image
            mock_pil_image_module.new.return_value = mock_pil_image
            mock_pil_image_module.FLIP_TOP_BOTTOM = 1
            
            mock_draw = MagicMock()
            mock_draw.textbbox.return_value = (0, 0, 600, 50)  # Text too wide
            mock_pil_imagedraw_module.Draw.return_value = mock_draw
            
            mock_font = MagicMock()
            mock_pil_imagefont_module.load_default.return_value = mock_font
            
            # Setup bpy mocks
            mock_blender_image = MagicMock()
            mock_blender_image.pixels = [0.0] * (512 * 512 * 4)
            mock_bpy.data.images.new.return_value = mock_blender_image
            mock_bpy.data.images.__contains__.return_value = False
            
            # Create props with text fitting enabled
            props = create_mock_props(
                text="Very Long Text That Needs Fitting",
                font_size=50,
                enable_text_fitting=True
            )
            
            # Call function
            result, normal_map = generate_texture_image(props, 512, 512)
            
            # Assertions
            assert result is not None
            # Text fitting should call textbbox multiple times to find right size
            assert mock_draw.textbbox.call_count > 1
    
    def test_custom_font_loading(self):
        """Test loading a custom font file."""
        with patch('PIL.Image') as mock_pil_image_module, \
             patch('PIL.ImageDraw') as mock_pil_imagedraw_module, \
             patch('PIL.ImageFont') as mock_pil_imagefont_module, \
             patch('src.core.generation_engine.bpy') as mock_bpy:
            
            # Setup PIL mocks
            mock_pil_image = MagicMock()
            mock_pil_image.size = (512, 512)
            mock_pil_image.getdata.return_value = [(255, 255, 255, 255)] * (512 * 512)
            mock_pil_image.transpose.return_value = mock_pil_image
            mock_pil_image_module.new.return_value = mock_pil_image
            mock_pil_image_module.FLIP_TOP_BOTTOM = 1
            
            mock_draw = MagicMock()
            mock_draw.textbbox.return_value = (0, 0, 100, 50)
            mock_pil_imagedraw_module.Draw.return_value = mock_draw
            
            mock_custom_font = MagicMock()
            mock_pil_imagefont_module.truetype.return_value = mock_custom_font
            
            # Setup bpy mocks
            mock_blender_image = MagicMock()
            mock_blender_image.pixels = [0.0] * (512 * 512 * 4)
            mock_bpy.data.images.new.return_value = mock_blender_image
            mock_bpy.data.images.__contains__.return_value = False
            
            # Create props with custom font
            props = create_mock_props(
                text="Custom Font",
                use_custom_font=True,
                custom_font_path="/path/to/custom/font.ttf",
                font_size=30
            )
            
            # Call function
            result, normal_map = generate_texture_image(props, 512, 512)
            
            # Assertions
            assert result is not None
            # Verify truetype was called with the custom font path
            mock_pil_imagefont_module.truetype.assert_called_with("/path/to/custom/font.ttf", 30)
    
    def test_font_loading_fallback(self):
        """Test fallback to default font when custom font fails to load."""
        with patch('PIL.Image') as mock_pil_image_module, \
             patch('PIL.ImageDraw') as mock_pil_imagedraw_module, \
             patch('PIL.ImageFont') as mock_pil_imagefont_module, \
             patch('src.core.generation_engine.bpy') as mock_bpy:
            
            # Setup PIL mocks
            mock_pil_image = MagicMock()
            mock_pil_image.size = (512, 512)
            mock_pil_image.getdata.return_value = [(255, 255, 255, 255)] * (512 * 512)
            mock_pil_image.transpose.return_value = mock_pil_image
            mock_pil_image_module.new.return_value = mock_pil_image
            mock_pil_image_module.FLIP_TOP_BOTTOM = 1
            
            mock_draw = MagicMock()
            mock_draw.textbbox.return_value = (0, 0, 100, 50)
            mock_pil_imagedraw_module.Draw.return_value = mock_draw
            
            # Make truetype raise OSError, should fallback to default
            mock_pil_imagefont_module.truetype.side_effect = OSError("Font not found")
            mock_default_font = MagicMock()
            mock_pil_imagefont_module.load_default.return_value = mock_default_font
            
            # Setup bpy mocks
            mock_blender_image = MagicMock()
            mock_blender_image.pixels = [0.0] * (512 * 512 * 4)
            mock_bpy.data.images.new.return_value = mock_blender_image
            mock_bpy.data.images.__contains__.return_value = False
            
            # Create props with invalid font path
            props = create_mock_props(
                text="Fallback Font",
                use_custom_font=True,
                custom_font_path="/invalid/path/to/font.ttf",
                font_size=30
            )
            
            # Call function
            result, normal_map = generate_texture_image(props, 512, 512)
            
            # Assertions
            assert result is not None
            # Verify fallback to default font
            mock_pil_imagefont_module.load_default.assert_called_once()
    
    def test_pil_import_error(self):
        """Test handling of PIL import error."""
        with patch('PIL.Image', None), \
             patch('PIL.ImageDraw', None), \
             patch('PIL.ImageFont', None):
            # Create a mock for bpy that returns an image even without PIL
            with patch('src.core.generation_engine.bpy') as mock_bpy:
                mock_blender_image = MagicMock()
                mock_blender_image.pixels = [0.0] * (512 * 512 * 4)
                mock_bpy.data.images.new.return_value = mock_blender_image
                mock_bpy.data.images.__contains__.return_value = False
                
                # Create props
                props = create_mock_props(text="No PIL")
                
                # Call when PIL is not available - should return None
                result, normal_map = generate_texture_image(props, 512, 512)
                
                # Should return None when PIL is unavailable
                assert result is None
                assert normal_map is None


class TestGeneratePreview:
    """Test suite for generate_preview() function"""
    
    def test_preview_generates_at_resolution(self):
        """Test that preview generates at specified resolution."""
        with patch('PIL.Image') as mock_pil_image_module, \
             patch('PIL.ImageDraw') as mock_pil_imagedraw_module, \
             patch('PIL.ImageFont') as mock_pil_imagefont_module, \
             patch('src.core.generation_engine.bpy') as mock_bpy:
            
            # Setup PIL mocks
            mock_pil_image = MagicMock()
            mock_pil_image.size = (128, 128)
            mock_pil_image.getdata.return_value = [(255, 255, 255, 255)] * (128 * 128)
            mock_pil_image.transpose.return_value = mock_pil_image
            mock_pil_image_module.new.return_value = mock_pil_image
            mock_pil_image_module.FLIP_TOP_BOTTOM = 1
            
            mock_draw = MagicMock()
            mock_draw.textbbox.return_value = (0, 0, 50, 25)
            mock_pil_imagedraw_module.Draw.return_value = mock_draw
            
            mock_font = MagicMock()
            mock_pil_imagefont_module.load_default.return_value = mock_font
            
            # Setup bpy mocks
            mock_blender_image = MagicMock()
            mock_blender_image.pixels = [0.0] * (128 * 128 * 4)
            mock_bpy.data.images.new.return_value = mock_blender_image
            mock_bpy.data.images.__contains__.return_value = False
            
            # Create props
            props = create_mock_props(text="Preview")
            
            # Call generate_preview with specific preview dimensions
            result = generate_preview(props, preview_width=128, preview_height=128)
            
            # Assertions
            assert result is not None
            # Verify image was created at preview resolution  
            # The image name contains dimensions
            call_args = mock_bpy.data.images.new.call_args[0]
            assert "128x128" in call_args[0]

    def test_preview_defaults_to_texture_resolution(self):
        """Preview falls back to full texture dimensions when none provided."""
        with patch('PIL.Image') as mock_pil_image_module, \
             patch('PIL.ImageDraw') as mock_pil_imagedraw_module, \
             patch('PIL.ImageFont') as mock_pil_imagefont_module, \
             patch('src.core.generation_engine.bpy') as mock_bpy:

            mock_pil_image = MagicMock()
            mock_pil_image.size = (256, 128)
            mock_pil_image.getdata.return_value = [(255, 255, 255, 255)] * (256 * 128)
            mock_pil_image.transpose.return_value = mock_pil_image
            mock_pil_image_module.new.return_value = mock_pil_image
            mock_pil_image_module.FLIP_TOP_BOTTOM = 1

            mock_draw = MagicMock()
            mock_draw.textbbox.return_value = (0, 0, 50, 25)
            mock_pil_imagedraw_module.Draw.return_value = mock_draw

            mock_font = MagicMock()
            mock_pil_imagefont_module.load_default.return_value = mock_font

            mock_blender_image = MagicMock()
            mock_blender_image.pixels = [0.0] * (256 * 128 * 4)
            mock_bpy.data.images.new.return_value = mock_blender_image
            mock_bpy.data.images.__contains__.return_value = False

            props = create_mock_props(text="AutoSize")
            props.texture_width = 256
            props.texture_height = 128

            result = generate_preview(props)

            assert result is not None
            call_args = mock_bpy.data.images.new.call_args[0]
            assert "256x128" in call_args[0]
    
    def test_preview_uses_same_props(self):
        """Test that preview uses same properties as main generation."""
        with patch('PIL.Image') as mock_pil_image_module, \
             patch('PIL.ImageDraw') as mock_pil_imagedraw_module, \
             patch('PIL.ImageFont') as mock_pil_imagefont_module, \
             patch('src.core.generation_engine.bpy') as mock_bpy:
            
            # Setup PIL mocks
            mock_pil_image = MagicMock()
            mock_pil_image.size = (64, 64)
            mock_pil_image.getdata.return_value = [(255, 255, 255, 255)] * (64 * 64)
            mock_pil_image.transpose.return_value = mock_pil_image
            mock_pil_image_module.new.return_value = mock_pil_image
            mock_pil_image_module.FLIP_TOP_BOTTOM = 1
            
            mock_draw = MagicMock()
            mock_draw.textbbox.return_value = (0, 0, 30, 15)
            mock_pil_imagedraw_module.Draw.return_value = mock_draw
            
            mock_font = MagicMock()
            mock_pil_imagefont_module.truetype.return_value = mock_font
            
            # Setup bpy mocks
            mock_blender_image = MagicMock()
            mock_blender_image.pixels = [0.0] * (64 * 64 * 4)
            mock_bpy.data.images.new.return_value = mock_blender_image
            mock_bpy.data.images.__contains__.return_value = False
            
            # Create props with various properties
            props = create_mock_props(
                text="Props",
                background_transparent=True,
                text_color=(1.0, 0.0, 0.0, 1.0),
                prepend_text="[",
                append_text="]",
                use_custom_font=True,
                custom_font_path="/path/to/font.ttf",
                font_size=20,
                prepend_append_layout='VERTICAL'
            )
            
            # Call preview
            result = generate_preview(props, preview_width=64, preview_height=64)
            
            # Assertions
            assert result is not None
            # Verify props were used (transparent, custom font, etc.)
            call_args = mock_pil_image_module.new.call_args
            assert call_args[0][0] == "RGBA"  # Transparent
            mock_pil_imagefont_module.truetype.assert_called_with("/path/to/font.ttf", 20)
    
    def test_preview_error_handling(self):
        """Test preview error handling."""
        with patch('PIL.Image') as mock_pil_image_module, \
             patch('PIL.ImageDraw') as mock_pil_imagedraw_module, \
             patch('PIL.ImageFont') as mock_pil_imagefont_module, \
             patch('src.core.generation_engine.bpy') as mock_bpy:
            
            # Setup PIL to raise an error
            mock_bpy.data.images.new.side_effect = RuntimeError("Preview generation failed")
            mock_bpy.data.images.__contains__.return_value = False
            
            # Create props
            props = create_mock_props(text="Error")
            
            # Call preview
            result = generate_preview(props, preview_width=128, preview_height=128)
            
            # Should handle error gracefully
            assert result is None
