"""Unit tests for generation_ops.py - Blender operator tests"""

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

from types import SimpleNamespace

from src.operators.generation_ops import (
    TEXT_TEXTURE_OT_generate,
    TEXT_TEXTURE_OT_preview,
    TEXT_TEXTURE_OT_generate_shader,
    TEXT_TEXTURE_OT_generate_plane,
    TEXT_TEXTURE_OT_reset_settings,
    _sanitize_text_for_name,
    _resolve_material_name,
)


# Helper functions for common mock setups

def create_mock_props(text="test", width=512, height=512, **kwargs):
    """Create mock texture properties with sensible defaults.
    
    Args:
        text: Text content for texture generation
        width: Texture width in pixels
        height: Texture height in pixels
        **kwargs: Additional property overrides
    
    Returns:
        Mock object with standard texture properties
    """
    props = Mock()
    props.text = text
    props.texture_width = width
    props.texture_height = height
    props.generate_normal_map = kwargs.get('generate_normal_map', False)
    props.preview_size = kwargs.get('preview_size', 512)
    props.shader_type = kwargs.get('shader_type', 'PRINCIPLED')
    props.shader_connection = kwargs.get('shader_connection', 'BASE_COLOR')
    props.use_alpha = kwargs.get('use_alpha', False)
    props.disable_shadows = kwargs.get('disable_shadows', False)
    props.disable_reflections = kwargs.get('disable_reflections', False)
    props.disable_backfacing = kwargs.get('disable_backfacing', False)
    props.custom_material_name = kwargs.get('custom_material_name', "")
    props.auto_assign_material = kwargs.get('auto_assign_material', False)
    return props


class DummyModifiers:
    """Simple stand-in for Blender's modifier collection."""

    def __init__(self):
        self.created = []

    def new(self, name, mod_type=None, **kwargs):
        mod_type = mod_type or kwargs.get('type')
        modifier = SimpleNamespace(
            name=name,
            type=mod_type,
            subdivision_type='CATMULL_CLARK',
            levels=0,
            render_levels=0,
        )
        self.created.append(modifier)
        return modifier


class DummyCollection:
    """Simple collection helper with clear tracking."""

    def __init__(self):
        self.cleared = False

    def clear(self):
        self.cleared = True


def create_mock_context(props=None, **kwargs):
    """Create mock Blender context with scene properties.
    
    Args:
        props: Texture properties (creates default if None)
        **kwargs: Context attribute overrides
    
    Returns:
        Mock context object with scene.text_texture_props
    """
    context = Mock()
    context.scene.text_texture_props = props if props else create_mock_props()
    context.space_data = kwargs.get('space_data', None)
    context.active_object = kwargs.get('active_object', None)
    context.screen.areas = kwargs.get('screen_areas', [])
    return context


def create_mock_image(width=512, height=512):
    """Create mock PIL Image with pixel data.
    
    Args:
        width: Image width in pixels
        height: Image height in pixels
    
    Returns:
        Mock image with pixels attribute
    """
    img = Mock()
    img.pixels = [0.5] * (width * height * 4)
    return img


def create_mock_blender_image(width=512, height=512):
    """Create mock Blender image with pack method.
    
    Args:
        width: Image width in pixels
        height: Image height in pixels
    
    Returns:
        Mock Blender image ready for bpy.data.images
    """
    img = Mock()
    img.pixels = [0.0] * (width * height * 4)
    img.pack = Mock()
    return img


def create_mock_shader_nodes():
    """Create mock shader node tree components.
    
    Returns:
        Tuple of (output_node, shader_node, texture_node) mocks
    """
    output_node = MagicMock()
    output_node.inputs = {'Surface': Mock()}
    
    shader_node = MagicMock()
    shader_node.inputs = {
        'Base Color': Mock(),
        'Emission Color': Mock(),
        'Emission Strength': Mock(),
        'Alpha': Mock(),
        'Normal': Mock()
    }
    shader_node.outputs = {'BSDF': Mock()}
    
    tex_node = MagicMock()
    tex_node.outputs = {'Color': Mock(), 'Alpha': Mock()}
    tex_node.image = None
    tex_node.location = (0, 0)
    
    return output_node, shader_node, tex_node


class TestGenerateOperator:
    """Test suite for TEXT_TEXTURE_OT_generate operator"""
    
    def test_execute_success(self):
        """Successful generation returns {'FINISHED'}"""
        op = TEXT_TEXTURE_OT_generate()
        op.report = Mock()
        
        props = create_mock_props(text="test texture")
        context = create_mock_context(props)
        mock_img = create_mock_image()
        
        with patch('src.operators.generation_ops.generate_texture_image') as mock_gen, \
             patch('src.operators.generation_ops.bpy') as mock_bpy:
            
            mock_gen.return_value = (mock_img, None)
            mock_blender_img = create_mock_blender_image()
            mock_bpy.data.images.new.return_value = mock_blender_img
            mock_bpy.data.images.__contains__.return_value = False
            
            result = op.execute(context)
            
            assert result == {'FINISHED'}
            mock_gen.assert_called_once_with(props, 512, 512)
            assert op.report.call_count >= 1
    
    def test_execute_creates_blender_image(self):
        """Image added to bpy.data.images"""
        op = TEXT_TEXTURE_OT_generate()
        op.report = Mock()
        
        context = Mock()
        mock_props = Mock()
        mock_props.text = "test"
        mock_props.texture_width = 256
        mock_props.texture_height = 256
        mock_props.generate_normal_map = False
        context.scene.text_texture_props = mock_props
        context.space_data = None
        
        mock_img = Mock()
        mock_img.pixels = [0.5] * (256 * 256 * 4)
        
        with patch('src.operators.generation_ops.generate_texture_image') as mock_gen, \
             patch('src.operators.generation_ops.bpy') as mock_bpy:
            
            mock_gen.return_value = (mock_img, None)
            
            mock_blender_img = Mock()
            mock_blender_img.pixels = [0.0] * (256 * 256 * 4)
            mock_blender_img.pack = Mock()
            mock_bpy.data.images.new.return_value = mock_blender_img
            mock_bpy.data.images.__contains__.return_value = False
            
            result = op.execute(context)
            
            mock_bpy.data.images.new.assert_called_once()
            call_args = mock_bpy.data.images.new.call_args
            assert call_args[0][1] == 256
            assert call_args[0][2] == 256
            assert call_args[1]['alpha'] is True
            
            mock_blender_img.pack.assert_called_once()
            assert result == {'FINISHED'}
    
    def test_execute_replaces_existing_image(self):
        """Old image removed before new creation"""
        op = TEXT_TEXTURE_OT_generate()
        op.report = Mock()
        
        context = Mock()
        mock_props = Mock()
        mock_props.text = "existing"
        mock_props.texture_width = 512
        mock_props.texture_height = 512
        mock_props.generate_normal_map = False
        context.scene.text_texture_props = mock_props
        context.space_data = None
        
        mock_img = Mock()
        mock_img.pixels = [0.5] * (512 * 512 * 4)
        
        mock_old_image = Mock()
        
        with patch('src.operators.generation_ops.generate_texture_image') as mock_gen, \
             patch('src.operators.generation_ops.bpy') as mock_bpy:
            
            mock_gen.return_value = (mock_img, None)
            
            mock_bpy.data.images.__contains__.return_value = True
            mock_bpy.data.images.__getitem__.return_value = mock_old_image
            mock_bpy.data.images.remove = Mock()
            
            mock_blender_img = Mock()
            mock_blender_img.pixels = [0.0] * (512 * 512 * 4)
            mock_blender_img.pack = Mock()
            mock_bpy.data.images.new.return_value = mock_blender_img
            
            result = op.execute(context)
            
            mock_bpy.data.images.remove.assert_called_once_with(mock_old_image)
            assert result == {'FINISHED'}
    
    def test_execute_error_handling(self):
        """Generation failure returns {'CANCELLED'} with error report"""
        op = TEXT_TEXTURE_OT_generate()
        op.report = Mock()
        
        context = Mock()
        mock_props = Mock()
        mock_props.text = "test"
        mock_props.texture_width = 512
        mock_props.texture_height = 512
        context.scene.text_texture_props = mock_props
        
        with patch('src.operators.generation_ops.generate_texture_image') as mock_gen:
            mock_gen.return_value = (None, None)
            
            result = op.execute(context)
            
            assert result == {'CANCELLED'}
            op.report.assert_called_once()
            assert 'ERROR' in op.report.call_args[0][0]
    
    def test_invoke_opens_file_browser(self):
        """Invoke context opens file browser"""
        # TEXT_TEXTURE_OT_generate doesn't implement a custom invoke method
        # It uses the default Blender operator behavior (direct execute)
        # This test verifies that behavior is as expected
        pytest.skip("Operator uses default Blender invoke (no file browser)")
    
    def test_poll_requires_context(self):
        """Poll method validates context availability"""
        if hasattr(TEXT_TEXTURE_OT_generate, 'poll'):
            valid_context = Mock()
            valid_context.scene = Mock()
            assert TEXT_TEXTURE_OT_generate.poll(valid_context) is True
            
            invalid_context = Mock()
            invalid_context.scene = None
            assert TEXT_TEXTURE_OT_generate.poll(invalid_context) is False
        else:
            pytest.skip("Operator does not implement poll method")


class TestPreviewOperator:
    """Test suite for TEXT_TEXTURE_OT_preview operator"""
    
    def test_execute_generates_preview(self):
        """Preview created at preview resolution"""
        op = TEXT_TEXTURE_OT_preview()
        op.report = Mock()
        
        context = Mock()
        mock_props = Mock()
        mock_props.text = "preview test"
        mock_props.preview_size = 512
        context.scene.text_texture_props = mock_props
        context.screen.areas = []
        
        mock_preview_img = Mock()
        mock_preview_img.name = "Preview_Image"
        
        # Patch at the core module level since that's where generate_preview lives
        with patch('src.core.generation_engine.generate_preview') as mock_preview:
            mock_preview.return_value = mock_preview_img
            
            result = op.execute(context)
            
            assert result == {'FINISHED'}
            mock_preview.assert_called_once()
            call_args = mock_preview.call_args
            assert call_args[0][0] == mock_props
            assert call_args[0][1] == 512
            assert call_args[0][2] == 512
    
    def test_execute_preview_error_handling(self):
        """Preview failure returns {'CANCELLED'}"""
        op = TEXT_TEXTURE_OT_preview()
        op.report = Mock()
        
        context = Mock()
        mock_props = Mock()
        mock_props.text = "test"
        mock_props.preview_size = 512
        context.scene.text_texture_props = mock_props
        
        with patch('src.operators.generation_ops.generate_preview') as mock_preview:
            mock_preview.return_value = None
            
            result = op.execute(context)
            
            assert result == {'CANCELLED'}
            op.report.assert_called()
            assert 'ERROR' in op.report.call_args[0][0]
    
    def test_poll_requires_context(self):
        """Poll validates context"""
        if hasattr(TEXT_TEXTURE_OT_preview, 'poll'):
            valid_context = Mock()
            valid_context.scene = Mock()
            assert TEXT_TEXTURE_OT_preview.poll(valid_context) is True
            
            invalid_context = Mock()
            invalid_context.scene = None
            assert TEXT_TEXTURE_OT_preview.poll(invalid_context) is False
        else:
            pytest.skip("Operator does not implement poll method")
    
    def test_preview_uses_correct_resolution(self):
        """Uses preview_width and preview_height"""
        op = TEXT_TEXTURE_OT_preview()
        op.report = Mock()
        
        context = Mock()
        mock_props = Mock()
        mock_props.text = "test"
        mock_props.preview_size = 256
        mock_props.texture_width = 2048
        mock_props.texture_height = 2048
        context.scene.text_texture_props = mock_props
        context.screen.areas = []
        
        mock_result = Mock()
        mock_result.name = "Preview"
        
        with patch('src.core.generation_engine.generate_preview') as mock_preview:
            mock_preview.return_value = mock_result
            
            result = op.execute(context)
            
            assert result == {'FINISHED'}
            call_args = mock_preview.call_args
            assert call_args[0][1] == 256
            assert call_args[0][2] == 256


class TestGenerateShaderOperator:
    """Test suite for TEXT_TEXTURE_OT_generate_shader operator"""
    
    def test_execute_creates_shader_nodes(self):
        """Shader nodes created and linked"""
        op = TEXT_TEXTURE_OT_generate_shader()
        op.report = Mock()
        
        context = Mock()
        mock_props = Mock()
        mock_props.text = "shader test"
        mock_props.texture_width = 512
        mock_props.texture_height = 512
        mock_props.generate_normal_map = False
        mock_props.shader_type = 'PRINCIPLED'
        mock_props.shader_connection = 'BASE_COLOR'
        mock_props.use_alpha = False
        mock_props.disable_shadows = False
        mock_props.disable_reflections = False
        mock_props.disable_backfacing = False
        mock_props.custom_material_name = ""
        mock_props.auto_assign_material = False
        context.scene.text_texture_props = mock_props
        context.active_object = None
        context.screen.areas = []
        
        mock_img = Mock()
        mock_img.pixels = [0.5] * (512 * 512 * 4)
        
        with patch('src.operators.generation_ops.generate_texture_image') as mock_gen, \
             patch('src.operators.generation_ops.bpy') as mock_bpy:
            
            mock_gen.return_value = (mock_img, None)
            
            # Create mock nodes with subscriptable outputs/inputs
            mock_output_node = MagicMock()
            mock_output_node.inputs = {'Surface': Mock()}
            
            mock_shader_node = MagicMock()
            mock_shader_node.inputs = {
                'Base Color': Mock(),
                'Emission Color': Mock(),
                'Emission Strength': Mock(),
                'Alpha': Mock(),
                'Normal': Mock()
            }
            mock_shader_node.outputs = {'BSDF': Mock()}
            
            mock_tex_node = MagicMock()
            mock_tex_node.outputs = {'Color': Mock(), 'Alpha': Mock()}
            mock_tex_node.image = None
            mock_tex_node.location = (0, 0)
            
            # Mock material and node tree
            mock_material = Mock()
            mock_nodes = Mock()
            mock_links = Mock()
            mock_material.node_tree.nodes = mock_nodes
            mock_material.node_tree.links = mock_links
            mock_material.node_tree.nodes.clear = Mock()
            mock_material.use_nodes = True
            
            # Return different nodes on each call
            mock_nodes.new.side_effect = [mock_output_node, mock_shader_node, mock_tex_node]
            
            mock_bpy.data.materials.new.return_value = mock_material
            mock_bpy.data.materials.__contains__.return_value = False
            
            mock_blender_img = Mock()
            mock_blender_img.pixels = [0.0] * (512 * 512 * 4)
            mock_blender_img.pack = Mock()
            mock_bpy.data.images.new.return_value = mock_blender_img
            mock_bpy.data.images.__contains__.return_value = False
            
            result = op.execute(context)
            
            assert result == {'FINISHED'}
            assert mock_nodes.new.call_count >= 3
            assert mock_links.new.call_count >= 1
    
    def test_execute_requires_material(self):
        """Material must exist in context"""
        op = TEXT_TEXTURE_OT_generate_shader()
        op.report = Mock()
        
        context = Mock()
        mock_props = Mock()
        mock_props.text = "test"
        mock_props.texture_width = 512
        mock_props.texture_height = 512
        mock_props.generate_normal_map = False
        mock_props.shader_type = 'PRINCIPLED'
        mock_props.shader_connection = 'BASE_COLOR'
        mock_props.use_alpha = False
        mock_props.disable_shadows = False
        mock_props.disable_reflections = False
        mock_props.disable_backfacing = False
        mock_props.custom_material_name = ""
        mock_props.auto_assign_material = False
        context.scene.text_texture_props = mock_props
        context.active_object = None
        context.screen.areas = []
        
        mock_img = Mock()
        mock_img.pixels = [0.5] * (512 * 512 * 4)
        
        with patch('src.operators.generation_ops.generate_texture_image') as mock_gen, \
             patch('src.operators.generation_ops.bpy') as mock_bpy:
            
            mock_gen.return_value = (mock_img, None)
            
            # Create mock nodes with subscriptable outputs/inputs
            mock_output_node = MagicMock()
            mock_output_node.inputs = {'Surface': Mock()}
            
            mock_shader_node = MagicMock()
            mock_shader_node.inputs = {
                'Base Color': Mock(),
                'Alpha': Mock(),
                'Normal': Mock()
            }
            mock_shader_node.outputs = {'BSDF': Mock()}
            
            mock_tex_node = MagicMock()
            mock_tex_node.outputs = {'Color': Mock(), 'Alpha': Mock()}
            
            mock_material = Mock()
            mock_material.node_tree.nodes = Mock()
            mock_material.node_tree.links = Mock()
            mock_material.node_tree.nodes.clear = Mock()
            mock_material.node_tree.nodes.new.side_effect = [mock_output_node, mock_shader_node, mock_tex_node]
            mock_material.use_nodes = True
            
            mock_bpy.data.materials.__contains__.return_value = False
            mock_bpy.data.materials.new.return_value = mock_material
            
            mock_blender_img = Mock()
            mock_blender_img.pixels = [0.0] * (512 * 512 * 4)
            mock_blender_img.pack = Mock()
            mock_bpy.data.images.new.return_value = mock_blender_img
            mock_bpy.data.images.__contains__.return_value = False
            
            result = op.execute(context)
            
            assert result == {'FINISHED'}
            mock_bpy.data.materials.new.assert_called_once()
    
    def test_execute_error_handling(self):
        """Missing material returns {'CANCELLED'}"""
        op = TEXT_TEXTURE_OT_generate_shader()
        op.report = Mock()
        
        context = Mock()
        mock_props = Mock()
        mock_props.text = "test"
        mock_props.texture_width = 512
        mock_props.texture_height = 512
        context.scene.text_texture_props = mock_props
        
        with patch('src.operators.generation_ops.generate_texture_image') as mock_gen:
            mock_gen.return_value = (None, None)
            
            result = op.execute(context)
            
            assert result == {'CANCELLED'}
            op.report.assert_called()
            assert 'ERROR' in op.report.call_args[0][0]
    
    def test_poll_validates_material_context(self):
        """Poll checks material availability"""
        if hasattr(TEXT_TEXTURE_OT_generate_shader, 'poll'):
            valid_context = Mock()
            valid_context.scene = Mock()
            assert TEXT_TEXTURE_OT_generate_shader.poll(valid_context) is True
            
            invalid_context = Mock()
            invalid_context.scene = None
            assert TEXT_TEXTURE_OT_generate_shader.poll(invalid_context) is False
        else:
            pytest.skip("Operator does not implement poll method")


class TestGenerationHelpers:
    """Unit tests for helper utilities in generation_ops."""

    def test_sanitize_text_removes_invalid_characters(self):
        assert _sanitize_text_for_name("Hello @World!!!") == "Hello_World"

    def test_sanitize_text_truncates_and_defaults(self):
        assert _sanitize_text_for_name(" " * 5 + "--") == "Texture"
        assert len(_sanitize_text_for_name("A" * 50)) == 20

    def test_resolve_material_uses_custom_name(self):
        props = create_mock_props()
        props.custom_material_name = "   Custom Mat   "
        assert _resolve_material_name(props, "Ignored") == "Custom Mat"

    def test_resolve_material_defaults_to_sanitized(self):
        props = create_mock_props()
        props.custom_material_name = ""
        assert _resolve_material_name(props, "Sample") == "TextTexture_Mat_Sample"


class TestGeneratePlaneOperator:
    """Tests for the Create Plane operator."""

    def test_generate_plane_success(self):
        op = TEXT_TEXTURE_OT_generate_plane()
        op.report = Mock()

        props = create_mock_props(text="Plane Test", width=800, height=400)
        context = create_mock_context(props)

        plane = SimpleNamespace(
            type='MESH',
            name='Plane',
            scale=SimpleNamespace(x=1.0, y=1.0, z=1.0),
            modifiers=DummyModifiers(),
            data=SimpleNamespace(materials=[]),
        )
        context.active_object = plane

        mat = SimpleNamespace(name="TextTexture_Mat_Plane_Test")

        with patch('src.operators.generation_ops.bpy') as mock_bpy:
            mock_bpy.ops.text_texture.generate_shader.return_value = {'FINISHED'}
            mock_bpy.ops.mesh.primitive_plane_add.return_value = None
            mock_bpy.data.materials.get.return_value = mat

            result = op.execute(context)

        assert result == {'FINISHED'}
        assert pytest.approx(plane.scale.x, rel=1e-6) == 2.0
        assert pytest.approx(plane.scale.y, rel=1e-6) == 1.0
        assert plane.modifiers.created[0].subdivision_type == 'SIMPLE'
        assert plane.modifiers.created[0].levels == 1
        assert plane.data.materials[0] is mat

    def test_generate_plane_handles_shader_failure(self):
        op = TEXT_TEXTURE_OT_generate_plane()
        op.report = Mock()

        props = create_mock_props()
        context = create_mock_context(props)

        with patch('src.operators.generation_ops.bpy') as mock_bpy:
            mock_bpy.ops.text_texture.generate_shader.return_value = {'CANCELLED'}

            result = op.execute(context)

        assert result == {'CANCELLED'}
        op.report.assert_called()


class TestResetSettingsOperator:
    """Tests for the reset settings operator."""

    class DummyProps:
        __annotations__ = {
            'text': None,
            'image_overlays': None,
            'preset_search_query': None,
        }

        def __init__(self):
            self.text = "Custom"
            self.image_overlays = DummyCollection()
            self.preset_search_query = "search"
            self.unset_called = []
            self.bl_rna = SimpleNamespace(properties=[
                SimpleNamespace(identifier='text'),
                SimpleNamespace(identifier='image_overlays'),
                SimpleNamespace(identifier='preset_search_query'),
            ])

        def property_unset(self, name):
            self.unset_called.append(name)

    def test_reset_settings_unsets_all_known_properties(self):
        op = TEXT_TEXTURE_OT_reset_settings()
        op.report = Mock()

        props = self.DummyProps()
        context = create_mock_context(props)

        result = op.execute(context)

        assert result == {'FINISHED'}
        assert set(props.unset_called) >= {'text', 'image_overlays', 'preset_search_query'}
        assert props.image_overlays.cleared is True
        op.report.assert_called()
