"""
Tests for async_loading mixin module
"""

import pytest
from pathlib import Path
from unittest.mock import Mock, MagicMock, patch, PropertyMock


class TestAsyncLoadingMixinInit:
    """Tests for AsyncLoadingMixin initialization"""

    def test_init_async_loading_creates_cache(self):
        """Test that _init_async_loading creates image cache"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin

        with (
            patch("pyPhotoAlbum.mixins.async_loading.ImageCache") as mock_cache,
            patch("pyPhotoAlbum.mixins.async_loading.AsyncImageLoader") as mock_loader,
            patch("pyPhotoAlbum.mixins.async_loading.AsyncPDFGenerator") as mock_pdf,
        ):

            mock_loader_instance = Mock()
            mock_loader.return_value = mock_loader_instance
            mock_pdf_instance = Mock()
            mock_pdf.return_value = mock_pdf_instance

            class TestClass(AsyncLoadingMixin):
                pass

            obj = TestClass()
            obj._init_async_loading()

            mock_cache.assert_called_once_with(max_memory_mb=512)
            assert hasattr(obj, "image_cache")

    def test_init_async_loading_creates_image_loader(self):
        """Test that _init_async_loading creates async image loader"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin

        with (
            patch("pyPhotoAlbum.mixins.async_loading.ImageCache") as mock_cache,
            patch("pyPhotoAlbum.mixins.async_loading.AsyncImageLoader") as mock_loader,
            patch("pyPhotoAlbum.mixins.async_loading.AsyncPDFGenerator") as mock_pdf,
        ):

            mock_loader_instance = Mock()
            mock_loader.return_value = mock_loader_instance
            mock_pdf_instance = Mock()
            mock_pdf.return_value = mock_pdf_instance

            class TestClass(AsyncLoadingMixin):
                pass

            obj = TestClass()
            obj._init_async_loading()

            mock_loader.assert_called_once()
            assert hasattr(obj, "async_image_loader")
            mock_loader_instance.start.assert_called_once()

    def test_init_async_loading_creates_pdf_generator(self):
        """Test that _init_async_loading creates async PDF generator"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin

        with (
            patch("pyPhotoAlbum.mixins.async_loading.ImageCache") as mock_cache,
            patch("pyPhotoAlbum.mixins.async_loading.AsyncImageLoader") as mock_loader,
            patch("pyPhotoAlbum.mixins.async_loading.AsyncPDFGenerator") as mock_pdf,
        ):

            mock_loader_instance = Mock()
            mock_loader.return_value = mock_loader_instance
            mock_pdf_instance = Mock()
            mock_pdf.return_value = mock_pdf_instance

            class TestClass(AsyncLoadingMixin):
                pass

            obj = TestClass()
            obj._init_async_loading()

            mock_pdf.assert_called_once()
            assert hasattr(obj, "async_pdf_generator")
            mock_pdf_instance.start.assert_called_once()


class TestAsyncLoadingMixinCleanup:
    """Tests for AsyncLoadingMixin cleanup"""

    def test_cleanup_stops_image_loader(self):
        """Test that _cleanup_async_loading stops image loader"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin

        class TestClass(AsyncLoadingMixin):
            pass

        obj = TestClass()
        obj.async_image_loader = Mock()
        obj.async_pdf_generator = Mock()
        obj.image_cache = Mock()

        obj._cleanup_async_loading()

        obj.async_image_loader.stop.assert_called_once()

    def test_cleanup_stops_pdf_generator(self):
        """Test that _cleanup_async_loading stops PDF generator"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin

        class TestClass(AsyncLoadingMixin):
            pass

        obj = TestClass()
        obj.async_image_loader = Mock()
        obj.async_pdf_generator = Mock()
        obj.image_cache = Mock()

        obj._cleanup_async_loading()

        obj.async_pdf_generator.stop.assert_called_once()

    def test_cleanup_clears_cache(self):
        """Test that _cleanup_async_loading clears image cache"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin

        class TestClass(AsyncLoadingMixin):
            pass

        obj = TestClass()
        obj.async_image_loader = Mock()
        obj.async_pdf_generator = Mock()
        obj.image_cache = Mock()

        obj._cleanup_async_loading()

        obj.image_cache.clear.assert_called_once()

    def test_cleanup_handles_missing_components(self):
        """Test that _cleanup_async_loading handles missing components gracefully"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin

        class TestClass(AsyncLoadingMixin):
            pass

        obj = TestClass()
        # Don't set any async components

        # Should not raise
        obj._cleanup_async_loading()


class TestOnImageLoaded:
    """Tests for _on_image_loaded callback"""

    def test_on_image_loaded_calls_element_callback(self):
        """Test that _on_image_loaded calls element's callback"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin

        class TestClass(AsyncLoadingMixin):
            def update(self):
                pass

        obj = TestClass()

        mock_image = Mock()
        mock_user_data = Mock()
        mock_user_data._on_async_image_loaded = Mock()

        obj._on_image_loaded(Path("/test/image.jpg"), mock_image, mock_user_data)

        mock_user_data._on_async_image_loaded.assert_called_once_with(mock_image)

    def test_on_image_loaded_triggers_update(self):
        """Test that _on_image_loaded triggers widget update"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin

        class TestClass(AsyncLoadingMixin):
            def __init__(self):
                self.update_called = False

            def update(self):
                self.update_called = True

        obj = TestClass()

        obj._on_image_loaded(Path("/test/image.jpg"), Mock(), None)

        assert obj.update_called

    def test_on_image_loaded_handles_none_user_data(self):
        """Test that _on_image_loaded handles None user_data"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin

        class TestClass(AsyncLoadingMixin):
            def update(self):
                pass

        obj = TestClass()

        # Should not raise
        obj._on_image_loaded(Path("/test/image.jpg"), Mock(), None)


class TestOnImageLoadFailed:
    """Tests for _on_image_load_failed callback"""

    def test_on_image_load_failed_calls_element_callback(self):
        """Test that _on_image_load_failed calls element's callback"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin

        class TestClass(AsyncLoadingMixin):
            pass

        obj = TestClass()

        mock_user_data = Mock()
        mock_user_data._on_async_image_load_failed = Mock()

        obj._on_image_load_failed(Path("/test/image.jpg"), "Error message", mock_user_data)

        mock_user_data._on_async_image_load_failed.assert_called_once_with("Error message")

    def test_on_image_load_failed_handles_none_user_data(self):
        """Test that _on_image_load_failed handles None user_data"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin

        class TestClass(AsyncLoadingMixin):
            pass

        obj = TestClass()

        # Should not raise
        obj._on_image_load_failed(Path("/test/image.jpg"), "Error", None)


class TestOnPdfProgress:
    """Tests for _on_pdf_progress callback"""

    def test_on_pdf_progress_updates_dialog(self):
        """Test that _on_pdf_progress updates progress dialog"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin

        class TestClass(AsyncLoadingMixin):
            pass

        obj = TestClass()
        obj._pdf_progress_dialog = Mock()

        obj._on_pdf_progress(5, 10, "Processing page 5")

        obj._pdf_progress_dialog.setValue.assert_called_once_with(5)
        obj._pdf_progress_dialog.setLabelText.assert_called_once_with("Processing page 5")

    def test_on_pdf_progress_handles_no_dialog(self):
        """Test that _on_pdf_progress handles missing dialog"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin

        class TestClass(AsyncLoadingMixin):
            pass

        obj = TestClass()
        # No _pdf_progress_dialog attribute

        # Should not raise
        obj._on_pdf_progress(5, 10, "Processing")


class TestOnPdfComplete:
    """Tests for _on_pdf_complete callback"""

    def test_on_pdf_complete_closes_dialog(self):
        """Test that _on_pdf_complete closes progress dialog"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin

        class TestClass(AsyncLoadingMixin):
            def window(self):
                return Mock(spec=[])

        obj = TestClass()
        mock_dialog = Mock()
        obj._pdf_progress_dialog = mock_dialog

        obj._on_pdf_complete(True, [])

        mock_dialog.close.assert_called_once()
        assert obj._pdf_progress_dialog is None

    def test_on_pdf_complete_shows_success_status(self):
        """Test that _on_pdf_complete shows success status"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin

        mock_main_window = Mock()

        class TestClass(AsyncLoadingMixin):
            def window(self):
                return mock_main_window

        obj = TestClass()

        obj._on_pdf_complete(True, [])

        mock_main_window.show_status.assert_called_once()
        call_args = mock_main_window.show_status.call_args[0]
        assert "successfully" in call_args[0]

    def test_on_pdf_complete_shows_warnings(self):
        """Test that _on_pdf_complete shows warning count"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin

        mock_main_window = Mock()

        class TestClass(AsyncLoadingMixin):
            def window(self):
                return mock_main_window

        obj = TestClass()

        obj._on_pdf_complete(True, ["warning1", "warning2"])

        mock_main_window.show_status.assert_called_once()
        call_args = mock_main_window.show_status.call_args[0]
        assert "2 warnings" in call_args[0]

    def test_on_pdf_complete_shows_failure_status(self):
        """Test that _on_pdf_complete shows failure status"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin

        mock_main_window = Mock()

        class TestClass(AsyncLoadingMixin):
            def window(self):
                return mock_main_window

        obj = TestClass()

        obj._on_pdf_complete(False, [])

        mock_main_window.show_status.assert_called_once()
        call_args = mock_main_window.show_status.call_args[0]
        assert "failed" in call_args[0]


class TestOnPdfFailed:
    """Tests for _on_pdf_failed callback"""

    def test_on_pdf_failed_closes_dialog(self):
        """Test that _on_pdf_failed closes progress dialog"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin

        class TestClass(AsyncLoadingMixin):
            def window(self):
                return Mock(spec=[])

        obj = TestClass()
        mock_dialog = Mock()
        obj._pdf_progress_dialog = mock_dialog

        obj._on_pdf_failed("Error occurred")

        mock_dialog.close.assert_called_once()
        assert obj._pdf_progress_dialog is None

    def test_on_pdf_failed_shows_error_status(self):
        """Test that _on_pdf_failed shows error status"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin

        mock_main_window = Mock()

        class TestClass(AsyncLoadingMixin):
            def window(self):
                return mock_main_window

        obj = TestClass()

        obj._on_pdf_failed("Something went wrong")

        mock_main_window.show_status.assert_called_once()
        call_args = mock_main_window.show_status.call_args[0]
        assert "failed" in call_args[0]
        assert "Something went wrong" in call_args[0]


class TestRequestImageLoad:
    """Tests for request_image_load method"""

    def test_request_image_load_no_loader(self):
        """Test request_image_load when loader not initialized"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin

        class TestClass(AsyncLoadingMixin):
            pass

        obj = TestClass()
        mock_image_data = Mock()

        # Should not raise
        obj.request_image_load(mock_image_data)

    def test_request_image_load_empty_path(self):
        """Test request_image_load with empty image path"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin

        class TestClass(AsyncLoadingMixin):
            pass

        obj = TestClass()
        obj.async_image_loader = Mock()

        mock_image_data = Mock()
        mock_image_data.image_path = ""

        obj.request_image_load(mock_image_data)

        obj.async_image_loader.request_load.assert_not_called()

    def test_request_image_load_non_assets_path_skipped(self):
        """Test request_image_load skips paths outside assets folder"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin

        class TestClass(AsyncLoadingMixin):
            pass

        obj = TestClass()
        obj.async_image_loader = Mock()

        mock_image_data = Mock()
        mock_image_data.image_path = "/absolute/path/image.jpg"

        obj.request_image_load(mock_image_data)

        obj.async_image_loader.request_load.assert_not_called()

    def test_request_image_load_path_not_resolved(self):
        """Test request_image_load when path resolution fails"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin

        class TestClass(AsyncLoadingMixin):
            pass

        obj = TestClass()
        obj.async_image_loader = Mock()

        mock_image_data = Mock()
        mock_image_data.image_path = "assets/missing.jpg"
        mock_image_data.resolve_image_path.return_value = None

        obj.request_image_load(mock_image_data)

        obj.async_image_loader.request_load.assert_not_called()

    def test_request_image_load_success(self, tmp_path):
        """Test successful request_image_load"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin, LoadPriority

        class TestClass(AsyncLoadingMixin):
            pass

        obj = TestClass()
        obj.async_image_loader = Mock()

        # Create actual file
        asset_path = tmp_path / "assets" / "photo.jpg"
        asset_path.parent.mkdir(parents=True)
        asset_path.write_text("test")

        mock_image_data = Mock()
        mock_image_data.image_path = "assets/photo.jpg"
        mock_image_data.resolve_image_path.return_value = str(asset_path)

        obj.request_image_load(mock_image_data, priority=LoadPriority.HIGH)

        obj.async_image_loader.request_load.assert_called_once()
        call_kwargs = obj.async_image_loader.request_load.call_args[1]
        assert call_kwargs["priority"] == LoadPriority.HIGH
        assert call_kwargs["user_data"] == mock_image_data


class TestExportPdfAsync:
    """Tests for export_pdf_async method"""

    def test_export_pdf_async_no_generator(self):
        """Test export_pdf_async when generator not initialized"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin

        class TestClass(AsyncLoadingMixin):
            pass

        obj = TestClass()
        mock_project = Mock()

        result = obj.export_pdf_async(mock_project, "/output.pdf")

        assert result is False

    def test_export_pdf_async_creates_progress_dialog(self, qtbot):
        """Test export_pdf_async creates progress dialog"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
        from PyQt6.QtWidgets import QWidget

        class TestWidget(QWidget, AsyncLoadingMixin):
            pass

        widget = TestWidget()
        qtbot.addWidget(widget)

        widget.async_pdf_generator = Mock()
        widget.async_pdf_generator.export_pdf.return_value = True

        mock_project = Mock()
        mock_project.pages = [Mock(is_cover=False, is_double_spread=False)]

        widget.export_pdf_async(mock_project, "/output.pdf")

        assert hasattr(widget, "_pdf_progress_dialog")
        assert widget._pdf_progress_dialog is not None

    def test_export_pdf_async_calls_generator(self, qtbot):
        """Test export_pdf_async calls the PDF generator"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
        from PyQt6.QtWidgets import QWidget

        class TestWidget(QWidget, AsyncLoadingMixin):
            pass

        widget = TestWidget()
        qtbot.addWidget(widget)

        widget.async_pdf_generator = Mock()
        widget.async_pdf_generator.export_pdf.return_value = True

        mock_project = Mock()
        mock_project.pages = []

        result = widget.export_pdf_async(mock_project, "/output.pdf", export_dpi=150)

        widget.async_pdf_generator.export_pdf.assert_called_once_with(mock_project, "/output.pdf", 150)
        assert result is True


class TestOnPdfCancel:
    """Tests for _on_pdf_cancel callback"""

    def test_on_pdf_cancel_cancels_export(self):
        """Test that _on_pdf_cancel cancels the export"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin

        class TestClass(AsyncLoadingMixin):
            pass

        obj = TestClass()
        obj.async_pdf_generator = Mock()

        obj._on_pdf_cancel()

        obj.async_pdf_generator.cancel_export.assert_called_once()

    def test_on_pdf_cancel_handles_no_generator(self):
        """Test that _on_pdf_cancel handles missing generator"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin

        class TestClass(AsyncLoadingMixin):
            pass

        obj = TestClass()
        # No async_pdf_generator

        # Should not raise
        obj._on_pdf_cancel()


class TestGetAsyncStats:
    """Tests for get_async_stats method"""

    def test_get_async_stats_empty(self):
        """Test get_async_stats with no components initialized"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin

        class TestClass(AsyncLoadingMixin):
            pass

        obj = TestClass()
        stats = obj.get_async_stats()

        assert stats == {}

    def test_get_async_stats_with_loader(self):
        """Test get_async_stats includes loader stats"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin

        class TestClass(AsyncLoadingMixin):
            pass

        obj = TestClass()
        obj.async_image_loader = Mock()
        obj.async_image_loader.get_stats.return_value = {"loaded": 10}

        stats = obj.get_async_stats()

        assert "image_loader" in stats
        assert stats["image_loader"]["loaded"] == 10

    def test_get_async_stats_with_pdf_generator(self):
        """Test get_async_stats includes PDF generator stats"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin

        class TestClass(AsyncLoadingMixin):
            pass

        obj = TestClass()
        obj.async_pdf_generator = Mock()
        obj.async_pdf_generator.get_stats.return_value = {"exports": 5}

        stats = obj.get_async_stats()

        assert "pdf_generator" in stats
        assert stats["pdf_generator"]["exports"] == 5

    def test_get_async_stats_with_all_components(self):
        """Test get_async_stats includes all component stats"""
        from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin

        class TestClass(AsyncLoadingMixin):
            pass

        obj = TestClass()
        obj.async_image_loader = Mock()
        obj.async_image_loader.get_stats.return_value = {"loaded": 10}
        obj.async_pdf_generator = Mock()
        obj.async_pdf_generator.get_stats.return_value = {"exports": 5}

        stats = obj.get_async_stats()

        assert "image_loader" in stats
        assert "pdf_generator" in stats
