"""
Unit tests for the thumbnail browser functionality.
"""
import unittest
from pathlib import Path
from unittest.mock import Mock, MagicMock, patch
import tempfile
import os
import time

from PyQt6.QtWidgets import QApplication
from PyQt6.QtCore import Qt, QTimer
from PyQt6.QtTest import QTest

from pyPhotoAlbum.thumbnail_browser import ThumbnailItem, ThumbnailGLWidget, ThumbnailBrowserDock
from pyPhotoAlbum.models import ImageData
from pyPhotoAlbum.page_layout import PageLayout
from pyPhotoAlbum.project import Project, Page

try:
    from PIL import Image
    PILLOW_AVAILABLE = True
except ImportError:
    PILLOW_AVAILABLE = False


class TestThumbnailItem(unittest.TestCase):
    """Test ThumbnailItem class."""

    def test_thumbnail_item_initialization(self):
        """Test ThumbnailItem initializes correctly."""
        item = ThumbnailItem("/path/to/image.jpg", (0, 0), 100.0)

        self.assertEqual(item.image_path, "/path/to/image.jpg")
        self.assertEqual(item.grid_row, 0)
        self.assertEqual(item.grid_col, 0)
        self.assertEqual(item.thumbnail_size, 100.0)
        self.assertFalse(item.is_used_in_project)

    def test_thumbnail_item_position_calculation(self):
        """Test that thumbnail position is calculated correctly based on grid."""
        # Position (0, 0)
        item1 = ThumbnailItem("/path/1.jpg", (0, 0), 100.0)
        self.assertEqual(item1.x, 10.0)  # spacing
        self.assertEqual(item1.y, 10.0)  # spacing

        # Position (0, 1) - second column
        item2 = ThumbnailItem("/path/2.jpg", (0, 1), 100.0)
        self.assertEqual(item2.x, 120.0)  # 10 + (100 + 10) * 1
        self.assertEqual(item2.y, 10.0)

        # Position (1, 0) - second row
        item3 = ThumbnailItem("/path/3.jpg", (1, 0), 100.0)
        self.assertEqual(item3.x, 10.0)
        self.assertEqual(item3.y, 120.0)  # 10 + (100 + 10) * 1

    def test_thumbnail_item_bounds(self):
        """Test get_bounds returns correct values."""
        item = ThumbnailItem("/path/to/image.jpg", (0, 0), 100.0)
        bounds = item.get_bounds()

        self.assertEqual(bounds, (10.0, 10.0, 100.0, 100.0))

    def test_thumbnail_item_contains_point(self):
        """Test contains_point correctly detects if point is inside thumbnail."""
        item = ThumbnailItem("/path/to/image.jpg", (0, 0), 100.0)

        # Point inside
        self.assertTrue(item.contains_point(50.0, 50.0))
        self.assertTrue(item.contains_point(10.0, 10.0))  # Top-left corner
        self.assertTrue(item.contains_point(110.0, 110.0))  # Bottom-right corner

        # Points outside
        self.assertFalse(item.contains_point(5.0, 5.0))
        self.assertFalse(item.contains_point(120.0, 120.0))
        self.assertFalse(item.contains_point(50.0, 150.0))


class TestThumbnailGLWidget(unittest.TestCase):
    """Test ThumbnailGLWidget class."""

    @classmethod
    def setUpClass(cls):
        """Set up QApplication for tests."""
        if not QApplication.instance():
            cls.app = QApplication([])
        else:
            cls.app = QApplication.instance()

    def setUp(self):
        """Set up test fixtures."""
        self.widget = ThumbnailGLWidget(main_window=None)

    def test_widget_initialization(self):
        """Test widget initializes with correct defaults."""
        self.assertEqual(len(self.widget.thumbnails), 0)
        self.assertIsNone(self.widget.current_folder)
        self.assertEqual(self.widget.zoom_level, 1.0)
        self.assertEqual(self.widget.pan_offset, (0, 0))

    def test_screen_to_viewport_conversion(self):
        """Test screen to viewport coordinate conversion."""
        self.widget.zoom_level = 2.0
        self.widget.pan_offset = (10, 20)

        vp_x, vp_y = self.widget.screen_to_viewport(50, 60)

        # (50 - 10) / 2.0 = 20.0
        # (60 - 20) / 2.0 = 20.0
        self.assertEqual(vp_x, 20.0)
        self.assertEqual(vp_y, 20.0)

    def test_load_folder_with_no_images(self):
        """Test loading a folder with no images."""
        with tempfile.TemporaryDirectory() as tmpdir:
            self.widget.load_folder(Path(tmpdir))

            self.assertEqual(self.widget.current_folder, Path(tmpdir))
            self.assertEqual(len(self.widget.thumbnails), 0)

    @patch('pyPhotoAlbum.thumbnail_browser.ThumbnailGLWidget._request_thumbnail_load')
    def test_load_folder_with_images(self, mock_request_load):
        """Test loading a folder with image files."""
        with tempfile.TemporaryDirectory() as tmpdir:
            # Create some dummy image files
            img1 = Path(tmpdir) / "image1.jpg"
            img2 = Path(tmpdir) / "image2.png"
            img3 = Path(tmpdir) / "image3.gif"

            img1.touch()
            img2.touch()
            img3.touch()

            self.widget.load_folder(Path(tmpdir))

            self.assertEqual(self.widget.current_folder, Path(tmpdir))
            self.assertEqual(len(self.widget.thumbnails), 3)
            self.assertEqual(len(self.widget.image_files), 3)

            # Check that all thumbnails have valid grid positions
            for thumb in self.widget.thumbnails:
                self.assertGreaterEqual(thumb.grid_row, 0)
                self.assertGreaterEqual(thumb.grid_col, 0)

            # Verify load was requested for each thumbnail
            self.assertEqual(mock_request_load.call_count, 3)

    def test_get_thumbnail_at_position(self):
        """Test getting thumbnail at a specific screen position."""
        # Manually add some thumbnails
        thumb1 = ThumbnailItem("/path/1.jpg", (0, 0), 100.0)
        thumb2 = ThumbnailItem("/path/2.jpg", (0, 1), 100.0)
        self.widget.thumbnails = [thumb1, thumb2]

        # No zoom or pan
        self.widget.zoom_level = 1.0
        self.widget.pan_offset = (0, 0)

        # Point inside first thumbnail
        result = self.widget.get_thumbnail_at(50, 50)
        self.assertEqual(result, thumb1)

        # Point inside second thumbnail
        result = self.widget.get_thumbnail_at(130, 50)
        self.assertEqual(result, thumb2)

        # Point outside both thumbnails
        result = self.widget.get_thumbnail_at(300, 300)
        self.assertIsNone(result)

    def test_update_used_images(self):
        """Test that used images are correctly marked."""
        # Create mock main window with project
        mock_main_window = Mock()
        mock_project = Mock(spec=Project)

        # Create mock pages with image elements
        mock_layout = Mock(spec=PageLayout)
        mock_page = Mock(spec=Page)
        mock_page.layout = mock_layout

        # Create image element that uses /path/to/used.jpg
        mock_image = Mock(spec=ImageData)
        mock_image.image_path = "assets/used.jpg"
        mock_image.resolve_image_path.return_value = "/path/to/used.jpg"

        mock_layout.elements = [mock_image]
        mock_project.pages = [mock_page]
        mock_main_window.project = mock_project

        # Mock the window() method to return our mock main window
        with patch.object(self.widget, 'window', return_value=mock_main_window):
            # Add thumbnails
            thumb1 = ThumbnailItem("/path/to/used.jpg", (0, 0))
            thumb2 = ThumbnailItem("/path/to/unused.jpg", (0, 1))
            self.widget.thumbnails = [thumb1, thumb2]

            # Update used images
            self.widget.update_used_images()

            # Check results
            self.assertTrue(thumb1.is_used_in_project)
            self.assertFalse(thumb2.is_used_in_project)


class TestThumbnailBrowserDock(unittest.TestCase):
    """Test ThumbnailBrowserDock class."""

    @classmethod
    def setUpClass(cls):
        """Set up QApplication for tests."""
        if not QApplication.instance():
            cls.app = QApplication([])
        else:
            cls.app = QApplication.instance()

    def setUp(self):
        """Set up test fixtures."""
        self.dock = ThumbnailBrowserDock()

    def test_dock_initialization(self):
        """Test dock widget initializes correctly."""
        self.assertEqual(self.dock.windowTitle(), "Image Browser")
        self.assertIsNotNone(self.dock.gl_widget)
        self.assertIsNotNone(self.dock.folder_label)
        self.assertIsNotNone(self.dock.select_folder_btn)

    def test_initial_folder_label(self):
        """Test initial folder label text."""
        self.assertEqual(self.dock.folder_label.text(), "No folder selected")

    @patch('pyPhotoAlbum.thumbnail_browser.QFileDialog.getExistingDirectory')
    @patch('pyPhotoAlbum.thumbnail_browser.ThumbnailGLWidget.load_folder')
    def test_select_folder(self, mock_load_folder, mock_dialog):
        """Test folder selection updates the widget."""
        # Mock the dialog to return a path
        test_path = "/test/folder"
        mock_dialog.return_value = test_path

        # Trigger folder selection
        self.dock._select_folder()

        # Verify dialog was called
        mock_dialog.assert_called_once()

        # Verify load_folder was called with the path
        mock_load_folder.assert_called_once_with(Path(test_path))

    @patch('pyPhotoAlbum.thumbnail_browser.QFileDialog.getExistingDirectory')
    @patch('pyPhotoAlbum.thumbnail_browser.ThumbnailGLWidget.load_folder')
    def test_select_folder_cancel(self, mock_load_folder, mock_dialog):
        """Test folder selection handles cancel."""
        # Mock the dialog to return empty (cancel)
        mock_dialog.return_value = ""

        # Trigger folder selection
        self.dock._select_folder()

        # Verify load_folder was NOT called
        mock_load_folder.assert_not_called()

    def test_load_folder_updates_label(self):
        """Test that loading a folder updates the label."""
        with tempfile.TemporaryDirectory() as tmpdir:
            folder_path = Path(tmpdir)
            folder_name = folder_path.name

            self.dock.load_folder(folder_path)

            self.assertEqual(self.dock.folder_label.text(), f"Folder: {folder_name}")


@unittest.skipUnless(PILLOW_AVAILABLE, "Pillow not available")
class TestThumbnailBrowserIntegration(unittest.TestCase):
    """Integration tests for thumbnail browser with actual image files."""

    @classmethod
    def setUpClass(cls):
        """Set up QApplication for tests."""
        if not QApplication.instance():
            cls.app = QApplication([])
        else:
            cls.app = QApplication.instance()

    def setUp(self):
        """Set up test fixtures."""
        self.widget = ThumbnailGLWidget(main_window=None)

    def tearDown(self):
        """Clean up after tests."""
        if hasattr(self.widget, 'thumbnails'):
            # Clean up any GL textures
            for thumb in self.widget.thumbnails:
                if hasattr(thumb, '_texture_id') and thumb._texture_id:
                    try:
                        from pyPhotoAlbum.gl_imports import glDeleteTextures
                        glDeleteTextures([thumb._texture_id])
                    except:
                        pass

    def _create_test_jpeg(self, path: Path, width: int = 100, height: int = 100, color: tuple = (255, 0, 0)):
        """Create a test JPEG file with the specified dimensions and color."""
        img = Image.new('RGB', (width, height), color=color)
        img.save(path, 'JPEG', quality=85)

    def test_load_folder_with_real_jpegs(self):
        """Integration test: Load a folder with real JPEG files."""
        with tempfile.TemporaryDirectory() as tmpdir:
            folder = Path(tmpdir)

            # Create test JPEG files with different colors
            colors = [
                (255, 0, 0),    # Red
                (0, 255, 0),    # Green
                (0, 0, 255),    # Blue
                (255, 255, 0),  # Yellow
                (255, 0, 255),  # Magenta
            ]

            created_files = []
            for i, color in enumerate(colors):
                img_path = folder / f"test_image_{i:02d}.jpg"
                self._create_test_jpeg(img_path, 200, 150, color)
                created_files.append(img_path)

            # Load the folder
            self.widget.load_folder(folder)

            # Verify folder was set
            self.assertEqual(self.widget.current_folder, folder)

            # Verify image files were found
            self.assertEqual(len(self.widget.image_files), 5)
            self.assertEqual(len(self.widget.thumbnails), 5)

            # Verify all created files are in the list
            found_paths = [str(f) for f in self.widget.image_files]
            for created_file in created_files:
                self.assertIn(str(created_file), found_paths)

            # Verify grid positions are valid
            for thumb in self.widget.thumbnails:
                self.assertGreaterEqual(thumb.grid_row, 0)
                self.assertGreaterEqual(thumb.grid_col, 0)
                self.assertTrue(thumb.image_path.endswith('.jpg'))

    def test_thumbnail_async_loading_with_mock_loader(self):
        """Test thumbnail loading with a mock async loader."""
        with tempfile.TemporaryDirectory() as tmpdir:
            folder = Path(tmpdir)

            # Create 3 test images
            for i in range(3):
                img_path = folder / f"image_{i}.jpg"
                self._create_test_jpeg(img_path, 150, 150, (100 + i * 50, 100, 100))

            # Create a mock main window with async loader and project
            mock_main_window = Mock()
            mock_gl_widget = Mock()
            mock_async_loader = Mock()
            mock_project = Mock()
            mock_project.pages = []  # Empty pages list

            # Track requested loads
            requested_loads = []

            def mock_request_load(path, priority, target_size, user_data):
                requested_loads.append({
                    'path': path,
                    'user_data': user_data
                })
                # Simulate immediate load by loading the image
                try:
                    img = Image.open(path)
                    img = img.convert('RGBA')
                    img.thumbnail(target_size, Image.Resampling.LANCZOS)
                    # Call the callback directly
                    user_data._pending_pil_image = img
                    user_data._img_width = img.width
                    user_data._img_height = img.height
                except Exception as e:
                    print(f"Error in mock load: {e}")

            mock_async_loader.request_load = mock_request_load
            mock_gl_widget.async_image_loader = mock_async_loader
            mock_main_window._gl_widget = mock_gl_widget
            mock_main_window.project = mock_project

            # Patch the widget's window() method
            with patch.object(self.widget, 'window', return_value=mock_main_window):
                # Load the folder
                self.widget.load_folder(folder)

                # Verify load was requested for each image
                self.assertEqual(len(requested_loads), 3)

                # Verify images were "loaded" (pending images set)
                loaded_count = sum(1 for thumb in self.widget.thumbnails
                                  if hasattr(thumb, '_pending_pil_image') and thumb._pending_pil_image)
                self.assertEqual(loaded_count, 3)

                # Verify image dimensions were set
                for thumb in self.widget.thumbnails:
                    if hasattr(thumb, '_img_width'):
                        self.assertGreater(thumb._img_width, 0)
                        self.assertGreater(thumb._img_height, 0)

    def test_large_folder_loading(self):
        """Test loading a folder with many images."""
        with tempfile.TemporaryDirectory() as tmpdir:
            folder = Path(tmpdir)

            # Create 50 test images
            num_images = 50
            for i in range(num_images):
                img_path = folder / f"img_{i:03d}.jpg"
                # Use smaller images for speed
                color = (i * 5 % 256, (i * 7) % 256, (i * 11) % 256)
                self._create_test_jpeg(img_path, 50, 50, color)

            # Load the folder
            self.widget.load_folder(folder)

            # Verify all images were found
            self.assertEqual(len(self.widget.image_files), num_images)
            self.assertEqual(len(self.widget.thumbnails), num_images)

            # Verify grid layout exists and all positions are valid
            for thumb in self.widget.thumbnails:
                self.assertGreaterEqual(thumb.grid_row, 0)
                self.assertGreaterEqual(thumb.grid_col, 0)

    def test_mixed_file_extensions(self):
        """Test loading folder with mixed image extensions."""
        with tempfile.TemporaryDirectory() as tmpdir:
            folder = Path(tmpdir)

            # Create files with different extensions
            extensions = ['jpg', 'jpeg', 'JPG', 'JPEG', 'png', 'PNG']
            for i, ext in enumerate(extensions):
                img_path = folder / f"image_{i}.{ext}"
                self._create_test_jpeg(img_path, 100, 100, (i * 40, 100, 100))

            # Also create a non-image file that should be ignored
            text_file = folder / "readme.txt"
            text_file.write_text("This should be ignored")

            # Load the folder
            self.widget.load_folder(folder)

            # Should find all image files (6) but not the text file
            self.assertEqual(len(self.widget.image_files), 6)

            # Verify text file is not in the list
            found_names = [f.name for f in self.widget.image_files]
            self.assertNotIn("readme.txt", found_names)

    def test_empty_folder(self):
        """Test loading an empty folder."""
        with tempfile.TemporaryDirectory() as tmpdir:
            folder = Path(tmpdir)

            # Load empty folder
            self.widget.load_folder(folder)

            # Should have no images
            self.assertEqual(len(self.widget.image_files), 0)
            self.assertEqual(len(self.widget.thumbnails), 0)
            self.assertEqual(self.widget.current_folder, folder)


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