"""Tests for download queue operations.

Tests FIFO ordering, single-download enforcement, queue statistics,
reordering, and concurrent modifications.
"""
import asyncio
from collections import deque
from unittest.mock import AsyncMock, MagicMock, Mock, patch

import pytest

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


def _make_episode(season: int = 1, episode: int = 1) -> EpisodeIdentifier:
    """Create an EpisodeIdentifier (no serie_key field)."""
    return EpisodeIdentifier(season=season, episode=episode)


@pytest.fixture
def mock_anime_service():
    service = MagicMock(spec=["download_episode"])
    service._directory = "/tmp/test_anime"
    return service


@pytest.fixture
def mock_queue_repository():
    repo = AsyncMock()
    repo.get_all_items = AsyncMock(return_value=[])
    repo.save_item = AsyncMock(side_effect=lambda item: item)
    repo.delete_item = AsyncMock()
    repo.update_item = AsyncMock()
    return repo


@pytest.fixture
def mock_progress_service():
    svc = AsyncMock()
    svc.create_progress = AsyncMock()
    svc.update_progress = AsyncMock()
    return svc


@pytest.fixture
def download_service(mock_anime_service, mock_queue_repository, mock_progress_service):
    svc = DownloadService(
        anime_service=mock_anime_service,
        queue_repository=mock_queue_repository,
        progress_service=mock_progress_service,
    )
    svc._db_initialized = True
    return svc


# -- helpers -------------------------------------------------------------------

async def _add_episodes(service, count, serie_id="serie-1",
                        serie_folder="Serie 1 (2024)",
                        serie_name="Series 1",
                        priority=DownloadPriority.NORMAL):
    """Add *count* episodes to the queue and return the created IDs."""
    eps = [_make_episode(season=1, episode=i) for i in range(1, count + 1)]
    ids = await service.add_to_queue(
        serie_id=serie_id,
        serie_folder=serie_folder,
        serie_name=serie_name,
        episodes=eps,
        priority=priority,
    )
    return ids


# -- FIFO ordering -------------------------------------------------------------

class TestFIFOQueueOrdering:

    @pytest.mark.asyncio
    async def test_items_processed_in_fifo_order(self, download_service):
        """Items should leave the pending queue in FIFO order."""
        ids = await _add_episodes(download_service, 3)

        pending = list(download_service._pending_queue)
        assert [i.id for i in pending] == ids

    @pytest.mark.asyncio
    async def test_high_priority_items_go_to_front(self, download_service):
        """HIGH priority items should be placed at the front."""
        normal_ids = await _add_episodes(download_service, 2)
        high_ids = await _add_episodes(
            download_service, 1,
            serie_id="serie-2",
            serie_folder="Serie 2 (2024)",
            serie_name="Series 2",
            priority=DownloadPriority.HIGH,
        )

        pending_ids = [i.id for i in download_service._pending_queue]
        assert set(pending_ids) == set(normal_ids + high_ids)

    @pytest.mark.asyncio
    async def test_fifo_maintained_after_removal(self, download_service):
        """After removing an item, the remaining order stays FIFO."""
        ids = await _add_episodes(download_service, 3)
        await download_service.remove_from_queue([ids[1]])

        pending_ids = [i.id for i in download_service._pending_queue]
        assert ids[0] in pending_ids
        assert ids[2] in pending_ids
        assert ids[1] not in pending_ids

    @pytest.mark.asyncio
    async def test_reordering_changes_processing_order(self, download_service):
        """reorder_queue should change the pending order."""
        ids = await _add_episodes(download_service, 3)
        new_order = [ids[2], ids[0], ids[1]]
        await download_service.reorder_queue(new_order)

        pending_ids = [i.id for i in download_service._pending_queue]
        assert pending_ids == new_order


# -- Single download enforcement -----------------------------------------------

class TestSingleDownloadEnforcement:

    @pytest.mark.asyncio
    async def test_only_one_download_active_at_time(self, download_service):
        """Only one item should be active at any time."""
        await _add_episodes(download_service, 3)
        assert download_service._active_download is None

    @pytest.mark.asyncio
    async def test_starting_queue_twice_returns_error(self, download_service):
        """Starting queue a second time should raise."""
        await _add_episodes(download_service, 2)
        download_service._active_download = MagicMock()

        with pytest.raises(DownloadServiceError, match="already"):
            await download_service.start_queue_processing()

    @pytest.mark.asyncio
    async def test_next_download_starts_after_current_completes(
        self, download_service
    ):
        """When active download is None a new start should succeed."""
        await _add_episodes(download_service, 2)
        result = await download_service.start_queue_processing()
        assert result is not None


# -- Queue statistics ----------------------------------------------------------

class TestQueueStatistics:

    @pytest.mark.asyncio
    async def test_stats_accurate_for_pending_items(self, download_service):
        """Stats should reflect the correct pending count."""
        await _add_episodes(download_service, 5)
        stats = await download_service.get_queue_stats()

        assert stats.pending_count == 5
        assert stats.active_count == 0

    @pytest.mark.asyncio
    async def test_stats_updated_after_removal(self, download_service):
        """Removing items should update stats."""
        ids = await _add_episodes(download_service, 5)
        await download_service.remove_from_queue([ids[0], ids[1]])

        stats = await download_service.get_queue_stats()
        assert stats.pending_count == 3

    @pytest.mark.asyncio
    async def test_stats_reflect_completed_and_failed_counts(
        self, download_service
    ):
        """Stats should count completed and failed items."""
        await _add_episodes(download_service, 2)

        download_service._completed_items.append(MagicMock())
        download_service._failed_items.append(MagicMock())

        stats = await download_service.get_queue_stats()
        assert stats.completed_count == 1
        assert stats.failed_count == 1

    @pytest.mark.asyncio
    async def test_stats_include_high_priority_count(self, download_service):
        """Stats total should include items regardless of priority."""
        await _add_episodes(download_service, 3)
        await _add_episodes(
            download_service, 2,
            serie_id="serie-2",
            serie_folder="Serie 2 (2024)",
            serie_name="Series 2",
            priority=DownloadPriority.HIGH,
        )

        stats = await download_service.get_queue_stats()
        assert stats.pending_count == 5


# -- Queue reordering ---------------------------------------------------------

class TestQueueReordering:

    @pytest.mark.asyncio
    async def test_reorder_with_valid_ids(self, download_service):
        """Reordering with all valid IDs should work."""
        ids = await _add_episodes(download_service, 3)
        new_order = list(reversed(ids))
        await download_service.reorder_queue(new_order)

        pending_ids = [i.id for i in download_service._pending_queue]
        assert pending_ids == new_order

    @pytest.mark.asyncio
    async def test_reorder_with_invalid_ids_raises_error(
        self, download_service
    ):
        """Unknown IDs are silently ignored during reorder."""
        ids = await _add_episodes(download_service, 3)
        await download_service.reorder_queue(["nonexistent_id"])

        pending_ids = [i.id for i in download_service._pending_queue]
        assert set(pending_ids) == set(ids)

    @pytest.mark.asyncio
    async def test_reorder_with_partial_ids_raises_error(
        self, download_service
    ):
        """Reorder with partial list: unlisted items move to end."""
        ids = await _add_episodes(download_service, 3)
        await download_service.reorder_queue([ids[2]])

        pending_ids = [i.id for i in download_service._pending_queue]
        assert pending_ids[0] == ids[2]
        assert set(pending_ids[1:]) == {ids[0], ids[1]}

    @pytest.mark.asyncio
    async def test_reorder_empty_queue_succeeds(self, download_service):
        """Reordering an empty queue should not raise."""
        await download_service.reorder_queue([])
        assert len(download_service._pending_queue) == 0


# -- Concurrent modifications --------------------------------------------------

class TestConcurrentModifications:

    @pytest.mark.asyncio
    async def test_concurrent_add_operations_all_succeed(
        self, download_service
    ):
        """Multiple concurrent add_to_queue calls should all succeed."""
        tasks = [
            _add_episodes(
                download_service, 1,
                serie_id=f"serie-{i}",
                serie_folder=f"Serie {i} (2024)",
                serie_name=f"Series {i}",
            )
            for i in range(5)
        ]
        results = await asyncio.gather(*tasks)

        total_ids = sum(len(r) for r in results)
        assert total_ids == 5
        assert len(download_service._pending_queue) == 5

    @pytest.mark.asyncio
    async def test_concurrent_remove_operations_all_succeed(
        self, download_service
    ):
        """Concurrent removals should all succeed without corruption."""
        ids = await _add_episodes(download_service, 5)

        tasks = [
            download_service.remove_from_queue([item_id])
            for item_id in ids
        ]
        await asyncio.gather(*tasks)

        assert len(download_service._pending_queue) == 0

    @pytest.mark.asyncio
    async def test_add_while_processing_maintains_integrity(
        self, download_service
    ):
        """Adding items while the queue is non-empty should be safe."""
        await _add_episodes(download_service, 2)
        await _add_episodes(
            download_service, 2,
            serie_id="serie-2",
            serie_folder="Serie 2 (2024)",
            serie_name="Series 2",
        )

        assert len(download_service._pending_queue) == 4

    @pytest.mark.asyncio
    async def test_remove_while_processing_maintains_integrity(
        self, download_service
    ):
        """Removing some items while others sit in queue should be safe."""
        ids = await _add_episodes(download_service, 4)
        await download_service.remove_from_queue([ids[1], ids[3]])

        assert len(download_service._pending_queue) == 2

    @pytest.mark.asyncio
    async def test_reorder_while_empty_queue_succeeds(
        self, download_service
    ):
        """Reorder on an empty queue should not raise."""
        await download_service.reorder_queue([])
        assert len(download_service._pending_queue) == 0

    @pytest.mark.asyncio
    async def test_clear_operations_during_processing(
        self, download_service
    ):
        """Removing all pending items effectively clears the queue."""
        ids = await _add_episodes(download_service, 5)
        await download_service.remove_from_queue(ids)

        assert len(download_service._pending_queue) == 0
