"""
Unit tests for pyWebLayout.concrete.image module.
Tests the RenderableImage class for image loading, scaling, and rendering functionality.
"""

import unittest
import os
import tempfile
import numpy as np
from PIL import Image as PILImage, ImageDraw
from unittest.mock import Mock, patch

from pyWebLayout.concrete.image import RenderableImage
from pyWebLayout.abstract.block import Image as AbstractImage
from pyWebLayout.style import Alignment


class TestRenderableImage(unittest.TestCase):
    """Test cases for the RenderableImage class"""

    def setUp(self):
        """Set up test fixtures"""
        # Create a temporary test image
        self.temp_dir = tempfile.mkdtemp()
        self.test_image_path = os.path.join(self.temp_dir, "test_image.png")

        # Create a simple test image
        test_img = PILImage.new('RGB', (100, 80), (255, 0, 0))  # Red image
        test_img.save(self.test_image_path)

        # Create abstract image objects
        self.abstract_image = AbstractImage(self.test_image_path, "Test Image", 100, 80)
        self.abstract_image_no_dims = AbstractImage(self.test_image_path, "Test Image")

        # Create a canvas and draw object for testing
        self.canvas = PILImage.new('RGBA', (400, 300), (255, 255, 255, 255))
        self.draw = ImageDraw.Draw(self.canvas)

    def tearDown(self):
        """Clean up test fixtures"""
        # Clean up temporary files
        try:
            os.unlink(self.test_image_path)
            os.rmdir(self.temp_dir)
        except BaseException:
            pass

    def test_renderable_image_initialization_basic(self):
        """Test basic image initialization"""
        renderable = RenderableImage(self.abstract_image, self.canvas)

        self.assertEqual(renderable._abstract_image, self.abstract_image)
        self.assertEqual(renderable._canvas, self.canvas)
        self.assertIsNotNone(renderable._pil_image)
        self.assertIsNone(renderable._error_message)
        self.assertEqual(renderable._halign, Alignment.CENTER)
        self.assertEqual(renderable._valign, Alignment.CENTER)

    def test_renderable_image_initialization_with_constraints(self):
        """Test image initialization with size constraints"""
        max_width = 50
        max_height = 40

        renderable = RenderableImage(
            self.abstract_image,
            self.draw,
            max_width=max_width,
            max_height=max_height
        )

        self.assertEqual(renderable._abstract_image, self.abstract_image)
        # Size should be constrained
        self.assertLessEqual(renderable._size[0], max_width)
        self.assertLessEqual(renderable._size[1], max_height)

    def test_renderable_image_initialization_with_custom_params(self):
        """Test image initialization with custom parameters"""
        custom_origin = (20, 30)
        custom_size = (120, 90)

        renderable = RenderableImage(
            self.abstract_image,
            self.draw,
            origin=custom_origin,
            size=custom_size,
            halign=Alignment.LEFT,
            valign=Alignment.TOP
        )

        np.testing.assert_array_equal(renderable._origin, np.array(custom_origin))
        np.testing.assert_array_equal(renderable._size, np.array(custom_size))
        self.assertEqual(renderable._halign, Alignment.LEFT)
        self.assertEqual(renderable._valign, Alignment.TOP)

    def test_load_image_local_file(self):
        """Test loading image from local file"""
        renderable = RenderableImage(self.abstract_image, self.draw)

        # Image should be loaded
        self.assertIsNotNone(renderable._pil_image)
        self.assertIsNone(renderable._error_message)
        self.assertEqual(renderable._pil_image.size, (100, 80))

    def test_load_image_nonexistent_file(self):
        """Test loading image from nonexistent file"""
        bad_abstract = AbstractImage("/nonexistent/path.png", "Bad Image")
        renderable = RenderableImage(bad_abstract, self.draw)

        # Should have error message, no PIL image
        self.assertIsNone(renderable._pil_image)
        self.assertIsNotNone(renderable._error_message)

    @patch('requests.get')
    def test_load_image_url_success(self, mock_get):
        """Test loading image from URL (success)"""
        # Create a mock response
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.content = open(self.test_image_path, 'rb').read()
        mock_get.return_value = mock_response

        url_abstract = AbstractImage("https://example.com/image.png", "URL Image")
        renderable = RenderableImage(url_abstract, self.draw)

        # Should successfully load image
        self.assertIsNotNone(renderable._pil_image)
        self.assertIsNone(renderable._error_message)

    @patch('requests.get')
    def test_load_image_url_failure(self, mock_get):
        """Test loading image from URL (failure)"""
        # Mock a failed request
        mock_response = Mock()
        mock_response.status_code = 404
        mock_get.return_value = mock_response

        url_abstract = AbstractImage(
            "https://example.com/notfound.png",
            "Bad URL Image")
        renderable = RenderableImage(url_abstract, self.draw)

        # Should have error message
        self.assertIsNone(renderable._pil_image)
        self.assertIsNotNone(renderable._error_message)

    def test_load_image_no_requests_library(self):
        """Test loading URL image when requests library is not available"""
        # Mock the import to raise ImportError for requests
        def mock_import(name, *args, **kwargs):
            if name == 'requests':
                raise ImportError("No module named 'requests'")
            return __import__(name, *args, **kwargs)

        with patch('builtins.__import__', side_effect=mock_import):
            url_abstract = AbstractImage("https://example.com/image.png", "URL Image")
            renderable = RenderableImage(url_abstract, self.draw)

            # Should have error message about missing requests
            self.assertIsNone(renderable._pil_image)
            self.assertIsNotNone(renderable._error_message)
            self.assertIn("Requests library not available", renderable._error_message)

    def test_resize_image_fit_within_bounds(self):
        """Test image resizing to fit within bounds"""
        renderable = RenderableImage(self.abstract_image, self.draw)

        # Original image is 100x80, resize to fit in 50x50
        renderable._size = np.array([50, 50])
        resized = renderable._resize_image()

        self.assertIsInstance(resized, PILImage.Image)
        # Should maintain aspect ratio and fit within bounds
        self.assertLessEqual(resized.width, 50)
        self.assertLessEqual(resized.height, 50)
        # Check aspect ratio is maintained (approximately)
        original_ratio = 100 / 80
        new_ratio = resized.width / resized.height
        self.assertAlmostEqual(original_ratio, new_ratio, delta=0.1)

    def test_resize_image_larger_target(self):
        """Test image resizing when target is larger than original"""
        renderable = RenderableImage(self.abstract_image, self.draw)

        # Target size larger than original
        renderable._size = np.array([200, 160])
        resized = renderable._resize_image()

        self.assertIsInstance(resized, PILImage.Image)
        # Should scale up to fill the space while maintaining aspect ratio
        self.assertGreater(resized.width, 100)
        self.assertGreater(resized.height, 80)

    def test_resize_image_no_image(self):
        """Test resize when no image is loaded"""
        bad_abstract = AbstractImage("/nonexistent/path.png", "Bad Image")
        renderable = RenderableImage(bad_abstract, self.draw)

        resized = renderable._resize_image()

        # Should return a placeholder image
        self.assertIsInstance(resized, PILImage.Image)
        self.assertEqual(resized.mode, 'RGBA')

    def test_draw_error_placeholder(self):
        """Test drawing error placeholder"""
        bad_abstract = AbstractImage("/nonexistent/path.png", "Bad Image")
        renderable = RenderableImage(bad_abstract, self.canvas)
        renderable._error_message = "File not found"

        # Set origin for the placeholder
        renderable.set_origin(np.array([10, 20]))

        # Call the error placeholder method
        renderable._draw_error_placeholder()

        # We can't easily test the actual drawing without complex mocking,
        # but we can verify the method doesn't raise an exception
        self.assertIsNotNone(renderable._error_message)

    def test_draw_error_placeholder_with_text(self):
        """Test drawing error placeholder with error message"""
        bad_abstract = AbstractImage("/nonexistent/path.png", "Bad Image")
        renderable = RenderableImage(bad_abstract, self.canvas)
        renderable._error_message = "File not found"

        # Set origin for the placeholder
        renderable.set_origin(np.array([10, 20]))

        # Call the error placeholder method
        renderable._draw_error_placeholder()

        # Verify error message is set
        self.assertIsNotNone(renderable._error_message)
        self.assertIn("File not found", renderable._error_message)

    def test_render_successful_image(self):
        """Test rendering successfully loaded image"""
        renderable = RenderableImage(self.abstract_image, self.canvas)
        renderable.set_origin(np.array([10, 20]))

        # Render returns nothing (draws directly into canvas)
        result = renderable.render()

        # Result should be None as it draws directly
        self.assertIsNone(result)

        # Verify image was loaded
        self.assertIsNotNone(renderable._pil_image)

    def test_render_failed_image(self):
        """Test rendering when image failed to load"""
        bad_abstract = AbstractImage("/nonexistent/path.png", "Bad Image")
        renderable = RenderableImage(bad_abstract, self.canvas)
        renderable.set_origin(np.array([10, 20]))

        with patch.object(renderable, '_draw_error_placeholder') as mock_draw_error:
            result = renderable.render()

            # Result should be None as it draws directly
            self.assertIsNone(result)
            mock_draw_error.assert_called_once()

    def test_render_with_left_alignment(self):
        """Test rendering with left alignment"""
        renderable = RenderableImage(
            self.abstract_image,
            self.canvas,
            halign=Alignment.LEFT,
            valign=Alignment.TOP
        )
        renderable.set_origin(np.array([10, 20]))

        result = renderable.render()

        # Result should be None as it draws directly
        self.assertIsNone(result)
        self.assertEqual(renderable._halign, Alignment.LEFT)
        self.assertEqual(renderable._valign, Alignment.TOP)

    def test_render_with_right_alignment(self):
        """Test rendering with right alignment"""
        renderable = RenderableImage(
            self.abstract_image,
            self.canvas,
            halign=Alignment.RIGHT,
            valign=Alignment.BOTTOM
        )
        renderable.set_origin(np.array([10, 20]))

        result = renderable.render()

        # Result should be None as it draws directly
        self.assertIsNone(result)
        self.assertEqual(renderable._halign, Alignment.RIGHT)
        self.assertEqual(renderable._valign, Alignment.BOTTOM)

    def test_render_rgb_image_conversion(self):
        """Test rendering RGB image (should be converted to RGBA)"""
        # Our test image is RGB, so this should test the conversion path
        renderable = RenderableImage(self.abstract_image, self.canvas)
        renderable.set_origin(np.array([10, 20]))

        result = renderable.render()

        # Result should be None as it draws directly
        self.assertIsNone(result)
        self.assertIsNotNone(renderable._pil_image)

    def test_in_object(self):
        """Test in_object method"""
        renderable = RenderableImage(self.abstract_image, self.draw, origin=(10, 20))

        # Point inside image
        self.assertTrue(renderable.in_object((15, 25)))

        # Point outside image
        self.assertFalse(renderable.in_object((200, 200)))

    def test_in_object_with_numpy_array(self):
        """Test in_object with numpy array point"""
        renderable = RenderableImage(self.abstract_image, self.draw, origin=(10, 20))

        # Point inside image as numpy array
        point = np.array([15, 25])
        self.assertTrue(renderable.in_object(point))

        # Point outside image as numpy array
        point = np.array([200, 200])
        self.assertFalse(renderable.in_object(point))

    def test_image_size_calculation_with_abstract_image_dimensions(self):
        """Test that size is calculated from abstract image when available"""
        # Abstract image has dimensions 100x80
        renderable = RenderableImage(self.abstract_image, self.draw)

        # Size should match the calculated scaled dimensions
        expected_size = self.abstract_image.calculate_scaled_dimensions()
        np.testing.assert_array_equal(renderable._size, np.array(expected_size))

    def test_image_size_calculation_with_constraints(self):
        """Test size calculation with max constraints"""
        max_width = 60
        max_height = 50

        renderable = RenderableImage(
            self.abstract_image,
            self.draw,
            max_width=max_width,
            max_height=max_height
        )

        # Size should respect constraints
        self.assertLessEqual(renderable._size[0], max_width)
        self.assertLessEqual(renderable._size[1], max_height)

    def test_image_without_initial_dimensions(self):
        """Test image without initial dimensions in abstract image"""
        renderable = RenderableImage(self.abstract_image_no_dims, self.draw)

        # Should still work, using default or calculated size
        self.assertIsInstance(renderable._size, np.ndarray)
        self.assertEqual(len(renderable._size), 2)

    def test_set_origin_method(self):
        """Test the set_origin method"""
        renderable = RenderableImage(self.abstract_image, self.draw)

        new_origin = np.array([50, 60])
        renderable.set_origin(new_origin)

        np.testing.assert_array_equal(renderable.origin, new_origin)

    def test_properties(self):
        """Test the property methods"""
        renderable = RenderableImage(
            self.abstract_image, self.draw, origin=(
                10, 20), size=(
                100, 80))

        np.testing.assert_array_equal(renderable.origin, np.array([10, 20]))
        np.testing.assert_array_equal(renderable.size, np.array([100, 80]))
        self.assertEqual(renderable.width, 100)


if __name__ == '__main__':
    unittest.main()
