"""Unit tests for the download queue service.

Tests cover queue management, manual download control, database persistence,
and error scenarios for the simplified download service.
"""
from __future__ import annotations

import asyncio
from datetime import datetime, timezone
from typing import Dict, List, Optional
from unittest.mock import AsyncMock, MagicMock

import pytest

from src.server.models.download import (
    DownloadItem,
    DownloadPriority,
    DownloadStatus,
    EpisodeIdentifier,
)
from src.server.services.anime_service import AnimeService
from src.server.services.download_service import DownloadService, DownloadServiceError


class MockQueueRepository:
    """Mock implementation of QueueRepository for testing.
    
    This provides an in-memory storage that mimics the simplified database
    repository behavior without requiring actual database connections.
    
    Note: The repository is simplified - status, priority, progress are
    now managed in-memory by DownloadService, not stored in database.
    """

    def __init__(self):
        """Initialize mock repository with in-memory storage."""
        self._items: Dict[str, DownloadItem] = {}

    async def save_item(self, item: DownloadItem) -> DownloadItem:
        """Save item to in-memory storage."""
        self._items[item.id] = item
        return item

    async def get_item(self, item_id: str) -> Optional[DownloadItem]:
        """Get item by ID from in-memory storage."""
        return self._items.get(item_id)

    async def get_all_items(self) -> List[DownloadItem]:
        """Get all items in storage."""
        return list(self._items.values())

    async def set_error(
        self,
        item_id: str,
        error: str,
    ) -> bool:
        """Set error message on an item."""
        if item_id not in self._items:
            return False
        self._items[item_id].error = error
        return True

    async def set_status(
        self,
        item_id: str,
        status: str,
    ) -> bool:
        """Set status on an item."""
        if item_id not in self._items:
            return False
        self._items[item_id].status = DownloadStatus(status)
        return True

    async def increment_retry(
        self,
        item_id: str,
    ) -> bool:
        """Increment retry count on an item."""
        if item_id not in self._items:
            return False
        self._items[item_id].retry_count += 1
        return True

    async def delete_item(self, item_id: str) -> bool:
        """Delete item from storage."""
        if item_id in self._items:
            del self._items[item_id]
            return True
        return False

    async def clear_all(self) -> int:
        """Clear all items."""
        count = len(self._items)
        self._items.clear()
        return count


@pytest.fixture
def mock_anime_service():
    """Create a mock AnimeService."""
    service = MagicMock(spec=AnimeService)
    service.download = AsyncMock(return_value=True)
    service._directory = "/mock/anime/directory"
    service._broadcast_series_updated = AsyncMock(return_value=None)
    return service


@pytest.fixture
def mock_queue_repository():
    """Create a mock QueueRepository for testing."""
    return MockQueueRepository()


@pytest.fixture
def download_service(mock_anime_service, mock_queue_repository):
    """Create a DownloadService instance for testing."""
    return DownloadService(
        anime_service=mock_anime_service,
        queue_repository=mock_queue_repository,
        max_retries=3,
    )


class TestDownloadServiceInitialization:
    """Test download service initialization."""

    def test_initialization_creates_queues(
        self, mock_anime_service, mock_queue_repository
    ):
        """Test that initialization creates empty queues."""
        service = DownloadService(
            anime_service=mock_anime_service,
            queue_repository=mock_queue_repository,
        )

        assert len(service._pending_queue) == 0
        assert service._active_download is None
        assert len(service._completed_items) == 0
        assert len(service._failed_items) == 0
        assert service._is_stopped is True

    @pytest.mark.asyncio
    async def test_initialization_loads_persisted_queue(
        self, mock_anime_service, mock_queue_repository
    ):
        """Test that initialization loads persisted queue from database."""
        # Pre-populate the mock repository with a pending item
        test_item = DownloadItem(
            id="test-id-1",
            serie_id="series-1",
            serie_folder="test-series",
            serie_name="Test Series",
            episode=EpisodeIdentifier(season=1, episode=1),
            status=DownloadStatus.PENDING,
            priority=DownloadPriority.NORMAL,
            added_at=datetime.now(timezone.utc),
        )
        await mock_queue_repository.save_item(test_item)

        # Create service and initialize from database
        service = DownloadService(
            anime_service=mock_anime_service,
            queue_repository=mock_queue_repository,
        )
        await service.initialize()

        assert len(service._pending_queue) == 1
        assert service._pending_queue[0].id == "test-id-1"


class TestQueueManagement:
    """Test queue management operations."""

    @pytest.mark.asyncio
    async def test_add_to_queue_single_episode(self, download_service):
        """Test adding a single episode to queue."""
        episodes = [EpisodeIdentifier(season=1, episode=1)]

        item_ids = await download_service.add_to_queue(
            serie_id="series-1",
            serie_folder="series",
            serie_name="Test Series",
            episodes=episodes,
            priority=DownloadPriority.NORMAL,
        )

        assert len(item_ids) == 1
        assert len(download_service._pending_queue) == 1
        assert download_service._pending_queue[0].serie_id == "series-1"
        assert (
            download_service._pending_queue[0].status
            == DownloadStatus.PENDING
        )

    @pytest.mark.asyncio
    async def test_add_to_queue_multiple_episodes(self, download_service):
        """Test adding multiple episodes to queue."""
        episodes = [
            EpisodeIdentifier(season=1, episode=1),
            EpisodeIdentifier(season=1, episode=2),
            EpisodeIdentifier(season=1, episode=3),
        ]

        item_ids = await download_service.add_to_queue(
            serie_id="series-1",
            serie_folder="series",
            serie_name="Test Series",
            episodes=episodes,
            priority=DownloadPriority.NORMAL,
        )

        assert len(item_ids) == 3
        assert len(download_service._pending_queue) == 3

    @pytest.mark.asyncio
    async def test_remove_from_pending_queue(self, download_service):
        """Test removing items from pending queue."""
        item_ids = await download_service.add_to_queue(
            serie_id="series-1",
            serie_folder="series",
            serie_name="Test Series",
            episodes=[EpisodeIdentifier(season=1, episode=1)],
        )

        removed_ids = await download_service.remove_from_queue(item_ids)

        assert len(removed_ids) == 1
        assert removed_ids[0] == item_ids[0]
        assert len(download_service._pending_queue) == 0

    @pytest.mark.asyncio
    async def test_start_next_download(self, download_service):
        """Test starting the next download from queue."""
        # Add items to queue
        await download_service.add_to_queue(
            serie_id="series-1",
            serie_folder="series",
            serie_name="Test Series",
            episodes=[
                EpisodeIdentifier(season=1, episode=1),
                EpisodeIdentifier(season=1, episode=2),
            ],
        )

        # Start next download
        started_id = await download_service.start_next_download()

        assert started_id is not None
        assert started_id == "queue_started"  # Service returns this string
        # Queue processing starts in background, wait a moment
        await asyncio.sleep(0.2)
        # First item should be processing or completed
        assert len(download_service._pending_queue) <= 2
        assert download_service._is_stopped is False

    @pytest.mark.asyncio
    async def test_start_next_download_empty_queue(self, download_service):
        """Test starting download with empty queue returns None."""
        result = await download_service.start_next_download()
        assert result is None

    @pytest.mark.asyncio
    async def test_start_next_download_already_active(
        self, download_service, mock_anime_service
    ):
        """Test that starting download while one is active raises error."""
        # Add items and start one
        await download_service.add_to_queue(
            serie_id="series-1",
            serie_folder="series",
            serie_name="Test Series",
            episodes=[
                EpisodeIdentifier(season=1, episode=1),
                EpisodeIdentifier(season=1, episode=2),
            ],
        )

        # Make download slow so it stays active (fake - no real download)
        async def fake_slow_download(**kwargs):
            await asyncio.sleep(0.5)  # Reduced from 10s to speed up test
            return True  # Fake success
            
        mock_anime_service.download = AsyncMock(side_effect=fake_slow_download)

        # Start first download (will block for 0.5s in background)
        item_id = await download_service.start_next_download()
        assert item_id is not None
        await asyncio.sleep(0.1)  # Let it start processing

        # Try to start another - should fail because one is active
        with pytest.raises(DownloadServiceError, match="already active"):
            await download_service.start_next_download()

    @pytest.mark.asyncio
    async def test_stop_downloads(self, download_service):
        """Test stopping queue processing."""
        await download_service.stop_downloads()
        assert download_service._is_stopped is True

    @pytest.mark.asyncio
    async def test_download_completion_moves_to_list(
        self, download_service, mock_anime_service
    ):
        """Test successful download moves item to completed list."""
        # Ensure mock returns success (fake download - no real download)
        mock_anime_service.download = AsyncMock(return_value=True)
        
        # Add item
        await download_service.add_to_queue(
            serie_id="series-1",
            serie_folder="series",
            serie_name="Test Series",
            episodes=[EpisodeIdentifier(season=1, episode=1)],
        )

        # Start and wait for completion
        await download_service.start_next_download()
        await asyncio.sleep(0.2)  # Wait for download to complete

        assert len(download_service._completed_items) == 1
        assert download_service._active_download is None

    @pytest.mark.asyncio
    async def test_download_failure_moves_to_list(
        self, download_service, mock_anime_service
    ):
        """Test failed download moves item to failed list."""
        # Make download fail (fake download failure - no real download)
        mock_anime_service.download = AsyncMock(return_value=False)

        # Add item
        await download_service.add_to_queue(
            serie_id="series-1",
            serie_folder="series",
            serie_name="Test Series",
            episodes=[EpisodeIdentifier(season=1, episode=1)],
        )

        # Start and wait for failure
        await download_service.start_next_download()
        await asyncio.sleep(0.2)  # Wait for download to fail

        assert len(download_service._failed_items) == 1
        assert download_service._active_download is None


class TestQueueStatus:
    """Test queue status reporting."""

    @pytest.mark.asyncio
    async def test_get_queue_status(self, download_service):
        """Test getting queue status."""
        # Add items to queue
        await download_service.add_to_queue(
            serie_id="series-1",
            serie_folder="series",
            serie_name="Test Series",
            episodes=[
                EpisodeIdentifier(season=1, episode=1),
                EpisodeIdentifier(season=1, episode=2),
            ],
        )

        status = await download_service.get_queue_status()

        # Queue is stopped until start_next_download() is called
        assert status.is_running is False
        assert status.is_paused is False
        assert len(status.pending_queue) == 2
        assert len(status.active_downloads) == 0
        assert len(status.completed_downloads) == 0
        assert len(status.failed_downloads) == 0

    @pytest.mark.asyncio
    async def test_get_queue_stats(self, download_service):
        """Test getting queue statistics."""
        # Add items
        await download_service.add_to_queue(
            serie_id="series-1",
            serie_folder="series",
            serie_name="Test Series",
            episodes=[
                EpisodeIdentifier(season=1, episode=1),
                EpisodeIdentifier(season=1, episode=2),
            ],
        )

        stats = await download_service.get_queue_stats()

        assert stats.total_items == 2
        assert stats.pending_count == 2
        assert stats.active_count == 0
        assert stats.completed_count == 0
        assert stats.failed_count == 0
        assert stats.total_downloaded_mb == 0.0


class TestQueueControl:
    """Test queue control operations."""

    @pytest.mark.asyncio
    async def test_clear_completed(self, download_service):
        """Test clearing completed downloads."""
        # Manually add completed item
        completed_item = DownloadItem(
            id="completed-1",
            serie_id="series-1",
            serie_folder="Test Series (2023)",
            serie_name="Test Series",
            episode=EpisodeIdentifier(season=1, episode=1),
            status=DownloadStatus.COMPLETED,
        )
        download_service._completed_items.append(completed_item)

        count = await download_service.clear_completed()

        assert count == 1
        assert len(download_service._completed_items) == 0

    @pytest.mark.asyncio
    async def test_clear_pending(self, download_service):
        """Test clearing all pending downloads from the queue."""
        # Add multiple items to the queue
        await download_service.add_to_queue(
            serie_id="series-1",
            serie_folder="test-series-1",
            serie_name="Test Series 1",
            episodes=[EpisodeIdentifier(season=1, episode=1)],
        )
        await download_service.add_to_queue(
            serie_id="series-2",
            serie_folder="test-series-2",
            serie_name="Test Series 2",
            episodes=[
                EpisodeIdentifier(season=1, episode=2),
                EpisodeIdentifier(season=1, episode=3),
            ],
        )

        # Verify items were added
        assert len(download_service._pending_queue) == 3

        # Clear pending queue
        count = await download_service.clear_pending()

        # Verify all pending items were cleared
        assert count == 3
        assert len(download_service._pending_queue) == 0
        assert len(download_service._pending_items_by_id) == 0


class TestPersistence:
    """Test queue persistence functionality with database backend."""

    @pytest.mark.asyncio
    async def test_queue_persistence(
        self, download_service, mock_queue_repository
    ):
        """Test that queue state is persisted to database."""
        await download_service.add_to_queue(
            serie_id="series-1",
            serie_folder="series",
            serie_name="Test Series",
            episodes=[EpisodeIdentifier(season=1, episode=1)],
        )

        # Item should be saved in mock repository
        all_items = await mock_queue_repository.get_all_items()
        assert len(all_items) == 1
        assert all_items[0].serie_id == "series-1"

    @pytest.mark.asyncio
    async def test_queue_recovery_after_restart(
        self, mock_anime_service, mock_queue_repository
    ):
        """Test that queue is recovered after service restart."""
        # Create and populate first service
        service1 = DownloadService(
            anime_service=mock_anime_service,
            queue_repository=mock_queue_repository,
        )

        await service1.add_to_queue(
            serie_id="series-1",
            serie_folder="series",
            serie_name="Test Series",
            episodes=[
                EpisodeIdentifier(season=1, episode=1),
                EpisodeIdentifier(season=1, episode=2),
            ],
        )

        # Create new service with same repository (simulating restart)
        service2 = DownloadService(
            anime_service=mock_anime_service,
            queue_repository=mock_queue_repository,
        )
        # Initialize to load from database to recover state
        await service2.initialize()

        # Should recover pending items
        assert len(service2._pending_queue) == 2


class TestRetryLogic:
    """Test retry logic for failed downloads."""

    @pytest.mark.asyncio
    async def test_retry_failed_items(self, download_service):
        """Test retrying failed downloads."""
        # Manually add failed item
        failed_item = DownloadItem(
            id="failed-1",
            serie_id="series-1",
            serie_folder="Test Series (2023)",
            serie_name="Test Series",
            episode=EpisodeIdentifier(season=1, episode=1),
            status=DownloadStatus.FAILED,
            retry_count=0,
            error="Test error",
        )
        download_service._failed_items.append(failed_item)

        retried_ids = await download_service.retry_failed()

        assert len(retried_ids) == 1
        assert len(download_service._failed_items) == 0
        assert len(download_service._pending_queue) == 1
        # retry_count incremented on retry
        assert download_service._pending_queue[0].retry_count == 1
        assert download_service._pending_queue[0].status == DownloadStatus.PENDING

    @pytest.mark.asyncio
    async def test_max_retries_not_exceeded(self, download_service):
        """Test that items with max retries are not retried."""
        # Create item with max retries
        failed_item = DownloadItem(
            id="failed-1",
            serie_id="series-1",
            serie_folder="Test Series (2023)",
            serie_name="Test Series",
            episode=EpisodeIdentifier(season=1, episode=1),
            status=DownloadStatus.FAILED,
            retry_count=3,  # Max retries
            error="Test error",
        )
        download_service._failed_items.append(failed_item)

        retried_ids = await download_service.retry_failed()

        assert len(retried_ids) == 0
        assert len(download_service._failed_items) == 1
        assert len(download_service._pending_queue) == 0
    
    @pytest.mark.asyncio
    async def test_permanently_failed_after_max_retries(self, download_service):
        """Test that item is marked permanently_failed after max retries."""
        # Mock download to fail
        download_service._anime_service.download = AsyncMock(
            side_effect=Exception("Download failed")
        )
        
        # Create item with max_retries - 1 already used
        item = DownloadItem(
            id="perm-failed-1",
            serie_id="series-1",
            serie_folder="Test Series (2023)",
            serie_name="Test Series",
            episode=EpisodeIdentifier(season=1, episode=1),
            status=DownloadStatus.PENDING,
            retry_count=2,  # Already 2 retries, max is 3
            error=None,
        )
        download_service._pending_queue.append(item)
        
        # Process download - will fail and reach max retries
        await download_service._process_download(item)
        
        # Item should be in failed_items with permanently_failed status
        assert len(download_service._failed_items) == 1
        assert download_service._failed_items[0].retry_count == 3


class TestDeadLetterQueue:
    """Test dead-letter queue behavior for permanently failed items."""
    
    @pytest.mark.asyncio
    async def test_requeue_permanently_failed_item(self, download_service):
        """Test that a permanently failed item can be re-queued."""
        # The unique constraint now includes status, so a permanently_failed
        # item doesn't block re-queuing the same episode
        pass  # Implementation depends on UI/API behavior


class TestBroadcastCallbacks:
    """Test WebSocket broadcast functionality."""

    @pytest.mark.asyncio
    async def test_broadcast_on_queue_update(self, download_service):
        """Test that queue updates work correctly (no broadcast callbacks)."""
        # Note: The service no longer has set_broadcast_callback method
        # It uses the progress service internally for websocket updates
        
        await download_service.add_to_queue(
            serie_id="series-1",
            serie_folder="series",
            serie_name="Test Series",
            episodes=[EpisodeIdentifier(season=1, episode=1)],
        )

        # Verify item was added successfully
        assert len(download_service._pending_queue) == 1

    @pytest.mark.asyncio
    async def test_progress_callback_format(self, download_service):
        """Test that download completes successfully with mocked service."""
        # Note: Progress updates are handled by SeriesApp events and
        # ProgressService, not via direct callbacks to the download service.
        # This test verifies that downloads complete without errors.
        
        # Mock successful download (fake download - no real download)
        download_service._anime_service.download = AsyncMock(return_value=True)
        
        # Add and process a download
        await download_service.add_to_queue(
            serie_id="series-1",
            serie_folder="series",
            serie_name="Test Series",
            episodes=[EpisodeIdentifier(season=1, episode=1)],
        )

        # Start download and wait for completion
        await download_service.start_next_download()
        await asyncio.sleep(0.5)  # Wait for processing

        # Verify download completed successfully
        assert len(download_service._completed_items) == 1
        assert download_service._completed_items[0].status == (
            DownloadStatus.COMPLETED
        )


class TestServiceLifecycle:
    """Test service start and stop operations."""

    @pytest.mark.asyncio
    async def test_start_service(self, download_service):
        """Test starting the service."""
        # initialize() is the proper method for service initialization
        await download_service.initialize()
        # Service initialized successfully

    @pytest.mark.asyncio
    async def test_stop_service(self, download_service):
        """Test stopping the service."""
        await download_service.initialize()
        await download_service.stop_downloads()
        # Verifies service can be stopped without errors
        # No _is_running attribute in simplified service

    @pytest.mark.asyncio
    async def test_start_already_running(self, download_service):
        """Test starting service when already running."""
        await download_service.initialize()
        await download_service.initialize()  # Should not raise error
        # No _is_running attribute in simplified service


class TestErrorHandling:
    """Test error handling in download service."""

    @pytest.mark.asyncio
    async def test_download_failure_moves_to_failed(self, download_service):
        """Test that download failures are handled correctly."""
        # Mock download to fail with exception (fake - no real download)
        download_service._anime_service.download = AsyncMock(
            side_effect=Exception("Fake download failed")
        )

        await download_service.add_to_queue(
            serie_id="series-1",
            serie_folder="series",
            serie_name="Test Series",
            episodes=[EpisodeIdentifier(season=1, episode=1)],
        )

        # Process the download
        item = download_service._pending_queue.popleft()
        await download_service._process_download(item)

        # Item should be in failed queue
        assert len(download_service._failed_items) == 1
        assert (
            download_service._failed_items[0].status == DownloadStatus.FAILED
        )
        assert download_service._failed_items[0].error is not None


class TestRemoveEpisodeFromMissingList:
    """Test that completed downloads remove episodes from missing list."""

    @pytest.mark.asyncio
    async def test_remove_episode_from_memory(self, download_service):
        """Test _remove_episode_from_memory updates in-memory state."""
        from src.core.entities.series import Serie

        # Set up in-memory series with missing episodes
        serie = Serie(
            key="test-series",
            name="Test Series",
            site="https://example.com",
            folder="Test Series (2024)",
            episodeDict={1: [1, 2, 3], 2: [1, 2]},
        )
        mock_app = MagicMock()
        mock_app.list.keyDict = {"test-series": serie}
        mock_app.list.GetMissingEpisode.return_value = [serie]
        mock_app.series_list = [serie]
        download_service._anime_service._app = mock_app

        # Remove episode S01E02
        download_service._remove_episode_from_memory("test-series", 1, 2)

        # Episode should be removed from episodeDict
        assert 2 not in serie.episodeDict[1]
        assert serie.episodeDict[1] == [1, 3]
        # Season 2 should be untouched
        assert serie.episodeDict[2] == [1, 2]

    @pytest.mark.asyncio
    async def test_remove_last_episode_in_season_removes_season(
        self, download_service
    ):
        """Test removing the last episode in a season removes the season key."""
        from src.core.entities.series import Serie

        serie = Serie(
            key="test-series",
            name="Test Series",
            site="https://example.com",
            folder="Test Series (2024)",
            episodeDict={1: [5], 2: [1, 2]},
        )
        mock_app = MagicMock()
        mock_app.list.keyDict = {"test-series": serie}
        mock_app.list.GetMissingEpisode.return_value = [serie]
        mock_app.series_list = [serie]
        download_service._anime_service._app = mock_app

        # Remove the only episode in season 1
        download_service._remove_episode_from_memory("test-series", 1, 5)

        # Season 1 should be completely removed
        assert 1 not in serie.episodeDict
        # Season 2 untouched
        assert serie.episodeDict[2] == [1, 2]
        # GetMissingEpisode should have been called to refresh
        mock_app.list.GetMissingEpisode.assert_called()

    @pytest.mark.asyncio
    async def test_remove_episode_unknown_series_no_error(
        self, download_service
    ):
        """Test removing episode for unknown series does not raise."""
        mock_app = MagicMock()
        mock_app.list.keyDict = {}
        download_service._anime_service._app = mock_app

        # Should not raise
        download_service._remove_episode_from_memory(
            "nonexistent-series", 1, 1
        )

    @pytest.mark.asyncio
    async def test_remove_episode_from_missing_list_calls_db_and_memory(
        self, download_service
    ):
        """Test _remove_episode_from_missing_list updates both DB and memory."""
        from unittest.mock import patch

        from src.core.entities.series import Serie

        # Set up in-memory state
        serie = Serie(
            key="test-series",
            name="Test Series",
            site="https://example.com",
            folder="Test Series (2024)",
            episodeDict={1: [1, 2, 3]},
        )
        mock_app = MagicMock()
        mock_app.list.keyDict = {"test-series": serie}
        mock_app.list.GetMissingEpisode.return_value = [serie]
        mock_app.series_list = [serie]
        download_service._anime_service._app = mock_app
        download_service._anime_service._cached_list_missing = MagicMock()

        # Mock DB session
        mock_db_session = AsyncMock()
        
        # Mock series returned by get_by_key
        mock_series = MagicMock()
        mock_series.id = 1
        
        # Mock episode returned by get_by_episode
        mock_episode = MagicMock()
        mock_episode.id = 100

        with patch(
            "src.server.database.connection.get_db_session"
        ) as mock_get_db, patch(
            "src.server.database.service.AnimeSeriesService"
        ) as mock_series_svc, patch(
            "src.server.database.service.EpisodeService"
        ) as mock_ep_svc:
            mock_get_db.return_value.__aenter__ = AsyncMock(
                return_value=mock_db_session
            )
            mock_get_db.return_value.__aexit__ = AsyncMock(
                return_value=False
            )
            
            # Mock get_by_key to return series
            mock_series_svc.get_by_key = AsyncMock(return_value=mock_series)
            
            # Mock get_by_episode to return episode
            mock_ep_svc.get_by_episode = AsyncMock(return_value=mock_episode)
            
            # Mock mark_downloaded to succeed
            mock_ep_svc.mark_downloaded = AsyncMock(return_value=mock_episode)

            result = await download_service._remove_episode_from_missing_list(
                series_key="test-series",
                season=1,
                episode=2,
                serie_folder="Test Series (2024)",
            )

        # mark_downloaded was called instead of delete
        mock_ep_svc.mark_downloaded.assert_awaited_once_with(
            db=mock_db_session,
            episode_id=100,
            file_path=(
                f"{download_service._directory}/Test Series (2024)/Season 1"
            ),
        )
        # In-memory update happened
        assert 2 not in serie.episodeDict[1]
        assert serie.episodeDict[1] == [1, 3]
        # Cache was cleared
        download_service._anime_service._cached_list_missing.cache_clear.assert_called()
        # Broadcast was sent so frontend gets real-time update
        download_service._anime_service._broadcast_series_updated.assert_awaited_once_with(
            "test-series"
        )
        assert result is True

    @pytest.mark.asyncio
    async def test_download_completion_removes_missing_episode(
        self, download_service
    ):
        """Test full flow: download success removes episode from missing list."""
        from unittest.mock import patch

        from src.core.entities.series import Serie

        # Setup mock anime service to return success
        download_service._anime_service.download = AsyncMock(
            return_value=True
        )

        # Set up in-memory series state
        serie = Serie(
            key="series-1",
            name="Test Series",
            site="https://example.com",
            folder="series",
            episodeDict={1: [1, 2, 3]},
        )
        mock_app = MagicMock()
        mock_app.list.keyDict = {"series-1": serie}
        mock_app.list.GetMissingEpisode.return_value = [serie]
        mock_app.series_list = [serie]
        download_service._anime_service._app = mock_app
        download_service._anime_service._cached_list_missing = MagicMock()

        # Add episode to queue
        await download_service.add_to_queue(
            serie_id="series-1",
            serie_folder="series",
            serie_name="Test Series",
            episodes=[EpisodeIdentifier(season=1, episode=2)],
        )

        # Mock DB calls
        mock_db_session = AsyncMock()
        
        # Mock series returned by get_by_key
        mock_series = MagicMock()
        mock_series.id = 1
        
        # Mock episode returned by get_by_episode
        mock_episode = MagicMock()
        mock_episode.id = 100

        with patch(
            "src.server.database.connection.get_db_session"
        ) as mock_get_db, patch(
            "src.server.database.service.AnimeSeriesService"
        ) as mock_series_svc, patch(
            "src.server.database.service.EpisodeService"
        ) as mock_ep_svc:
            mock_get_db.return_value.__aenter__ = AsyncMock(
                return_value=mock_db_session
            )
            mock_get_db.return_value.__aexit__ = AsyncMock(
                return_value=False
            )
            
            # Mock get_by_key to return series
            mock_series_svc.get_by_key = AsyncMock(return_value=mock_series)
            
            # Mock get_by_episode to return episode
            mock_ep_svc.get_by_episode = AsyncMock(return_value=mock_episode)
            
            # Mock mark_downloaded to succeed
            mock_ep_svc.mark_downloaded = AsyncMock(return_value=mock_episode)

            # Process the download
            item = download_service._pending_queue.popleft()
            download_service._pending_items_by_id.pop(item.id, None)
            await download_service._process_download(item)

        # Episode should be completed
        assert len(download_service._completed_items) == 1
        assert download_service._completed_items[0].status == DownloadStatus.COMPLETED

        # Episode 2 should be removed from in-memory missing list
        assert 2 not in serie.episodeDict[1]
        assert serie.episodeDict[1] == [1, 3]


class TestQueueDeduplication:
    """Test queue deduplication to prevent duplicate entries."""

    @pytest.mark.asyncio
    async def test_add_same_episode_twice_creates_only_one_entry(
        self, download_service
    ):
        """Test that adding the same episode twice only creates one queue entry."""
        episodes = [EpisodeIdentifier(season=1, episode=1)]

        # Add same episode twice
        ids1 = await download_service.add_to_queue(
            serie_id="series-1",
            serie_folder="series",
            serie_name="Test Series",
            episodes=episodes,
        )
        ids2 = await download_service.add_to_queue(
            serie_id="series-1",
            serie_folder="series",
            serie_name="Test Series",
            episodes=episodes,
        )

        # Should only have one entry
        assert len(download_service._pending_queue) == 1
        # First call creates one ID
        assert len(ids1) == 1
        # Second call creates zero IDs (deduplicated)
        assert len(ids2) == 0

    @pytest.mark.asyncio
    async def test_add_different_episodes_creates_separate_entries(
        self, download_service
    ):
        """Test that different episodes create separate queue entries."""
        episodes1 = [EpisodeIdentifier(season=1, episode=1)]
        episodes2 = [EpisodeIdentifier(season=1, episode=2)]

        ids1 = await download_service.add_to_queue(
            serie_id="series-1",
            serie_folder="series",
            serie_name="Test Series",
            episodes=episodes1,
        )
        ids2 = await download_service.add_to_queue(
            serie_id="series-1",
            serie_folder="series",
            serie_name="Test Series",
            episodes=episodes2,
        )

        # Should have two separate entries
        assert len(download_service._pending_queue) == 2
        assert len(ids1) == 1
        assert len(ids2) == 1
        # IDs should be different
        assert ids1[0] != ids2[0]

    @pytest.mark.asyncio
    async def test_add_same_episode_different_series_creates_entries(
        self, download_service
    ):
        """Test that same episode in different series creates separate entries."""
        episodes = [EpisodeIdentifier(season=1, episode=1)]

        ids1 = await download_service.add_to_queue(
            serie_id="series-1",
            serie_folder="series1",
            serie_name="Test Series 1",
            episodes=episodes,
        )
        ids2 = await download_service.add_to_queue(
            serie_id="series-2",
            serie_folder="series2",
            serie_name="Test Series 2",
            episodes=episodes,
        )

        # Should have two separate entries (different series)
        assert len(download_service._pending_queue) == 2
        assert len(ids1) == 1
        assert len(ids2) == 1

    @pytest.mark.asyncio
    async def test_add_multiple_episodes_with_duplicates_filters_correctly(
        self, download_service
    ):
        """Test that adding multiple episodes with some duplicates filters correctly."""
        episodes = [
            EpisodeIdentifier(season=1, episode=1),
            EpisodeIdentifier(season=1, episode=2),
            EpisodeIdentifier(season=1, episode=1),  # duplicate
            EpisodeIdentifier(season=1, episode=3),
        ]

        ids1 = await download_service.add_to_queue(
            serie_id="series-1",
            serie_folder="series",
            serie_name="Test Series",
            episodes=episodes,
        )

        # Should only have 3 entries (1, 2, 3) - one filtered out
        assert len(download_service._pending_queue) == 3
        assert len(ids1) == 3
