"""Unit tests for download retry logic.

This module tests automatic retry mechanisms, retry count tracking,
exponential backoff, maximum retry limits, and state persistence.
"""
import asyncio
from unittest.mock import AsyncMock, Mock, patch

import pytest

from src.server.models.download import DownloadItem, DownloadStatus, EpisodeIdentifier
from src.server.services.download_service import DownloadService


@pytest.fixture(autouse=True)
def mock_progress_service():
    """Auto-mock progress service for all tests."""
    with patch('src.server.services.download_service.get_progress_service') as mock:
        mock_service = Mock()
        mock_service.update_progress = AsyncMock()
        mock.return_value = mock_service
        yield mock_service


class TestAutomaticRetry:
    """Test automatic retry after download failure."""
    
    @pytest.mark.asyncio
    async def test_retry_failed_single_item(self):
        """Test retrying a single failed download."""
        mock_anime_service = Mock()
        download_service = DownloadService(anime_service=mock_anime_service, max_retries=3)
        
        # Create failed item
        failed_item = DownloadItem(
            id="test-1",
            serie_id="series-1",
            serie_folder="Test Series (2024)",
            serie_name="Test Series",
            episode=EpisodeIdentifier(season=1, episode=1),
            status=DownloadStatus.FAILED,
            retry_count=0,
            error="Network timeout"
        )
        download_service._failed_items.append(failed_item)
        
        # Retry
        retried_ids = await download_service.retry_failed([failed_item.id])
        
        # Should be retried
        assert len(retried_ids) == 1
        assert failed_item.id in retried_ids
        assert len(download_service._failed_items) == 0
        assert len(download_service._pending_queue) == 1
        
        # Check item moved to pending
        pending_item = download_service._pending_queue[0]
        assert pending_item.id == failed_item.id
        assert pending_item.status == DownloadStatus.PENDING
        assert pending_item.retry_count == 1
        assert pending_item.error is None
    
    @pytest.mark.asyncio
    async def test_retry_all_failed_items(self):
        """Test retrying all failed downloads."""
        mock_anime_service = Mock()
        download_service = DownloadService(anime_service=mock_anime_service, max_retries=3)
        
        # Create multiple failed items
        for i in range(5):
            failed_item = DownloadItem(
                id=f"test-{i}",
                serie_id="series-1",
                serie_folder="Test Series (2024)",
                serie_name="Test Series",
                episode=EpisodeIdentifier(season=1, episode=i+1),
                status=DownloadStatus.FAILED,
                retry_count=0,
                error="Test error"
            )
            download_service._failed_items.append(failed_item)
        
        # Retry all (passing None)
        retried_ids = await download_service.retry_failed(item_ids=None)
        
        # All should be retried
        assert len(retried_ids) == 5
        assert len(download_service._failed_items) == 0
        assert len(download_service._pending_queue) == 5


class TestRetryCountTracking:
    """Test retry attempt count tracking."""
    
    @pytest.mark.asyncio
    async def test_retry_count_increments_on_retry(self):
        """Test that retry_count increments each retry."""
        mock_anime_service = Mock()
        download_service = DownloadService(anime_service=mock_anime_service, max_retries=5)
        
        # Initial failure (retry_count = 0)
        failed_item = DownloadItem(
            id="test-1",
            serie_id="series-1",
            serie_folder="Test Series (2024)",
            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)
        
        # First retry
        await download_service.retry_failed([failed_item.id])
        assert download_service._pending_queue[0].retry_count == 1
        
        # Simulate failure again
        item = download_service._pending_queue.popleft()
        item.status = DownloadStatus.FAILED
        download_service._failed_items.append(item)
        
        # Second retry
        await download_service.retry_failed([item.id])
        assert download_service._pending_queue[0].retry_count == 2
        
        # Simulate failure again
        item = download_service._pending_queue.popleft()
        item.status = DownloadStatus.FAILED
        download_service._failed_items.append(item)
        
        # Third retry
        await download_service.retry_failed([item.id])
        assert download_service._pending_queue[0].retry_count == 3
    
    @pytest.mark.asyncio
    async def test_retry_count_persists_across_retries(self):
        """Test that retry_count persists correctly."""
        mock_anime_service = Mock()
        download_service = DownloadService(anime_service=mock_anime_service, max_retries=3)
        
        # Create item with previous retries
        failed_item = DownloadItem(
            id="test-1",
            serie_id="series-1",
            serie_folder="Test Series (2024)",
            serie_name="Test Series",
            episode=EpisodeIdentifier(season=1, episode=1),
            status=DownloadStatus.FAILED,
            retry_count=2,  # Already retried twice
            error="Test error"
        )
        download_service._failed_items.append(failed_item)
        
        # Retry
        await download_service.retry_failed()
        
        # Should now have retry_count = 3
        assert download_service._pending_queue[0].retry_count == 3


class TestMaximumRetryLimit:
    """Test maximum retry limit enforcement."""
    
    @pytest.mark.asyncio
    async def test_item_not_retried_after_max_retries(self):
        """Test that items exceeding max_retries are not retried."""
        mock_anime_service = Mock()
        download_service = DownloadService(anime_service=mock_anime_service, max_retries=3)
        
        # Create item that reached max retries
        failed_item = DownloadItem(
            id="test-1",
            serie_id="series-1",
            serie_folder="Test Series (2024)",
            serie_name="Test Series",
            episode=EpisodeIdentifier(season=1, episode=1),
            status=DownloadStatus.FAILED,
            retry_count=3,  # At max retries
            error="Test error"
        )
        download_service._failed_items.append(failed_item)
        
        # Try to retry
        retried_ids = await download_service.retry_failed()
        
        # Should NOT be retried
        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_mixed_retry_eligibility(self):
        """Test mix of items at/below max retries."""
        mock_anime_service = Mock()
        download_service = DownloadService(anime_service=mock_anime_service, max_retries=3)
        
        # Item 1: eligible for retry
        item1 = DownloadItem(
            id="eligible-1",
            serie_id="series-1",
            serie_folder="Test Series (2024)",
            serie_name="Test Series",
            episode=EpisodeIdentifier(season=1, episode=1),
            status=DownloadStatus.FAILED,
            retry_count=0,
            error="Test error"
        )
        
        # Item 2: at max retries
        item2 = DownloadItem(
            id="max-retries-1",
            serie_id="series-1",
            serie_folder="Test Series (2024)",
            serie_name="Test Series",
            episode=EpisodeIdentifier(season=1, episode=2),
            status=DownloadStatus.FAILED,
            retry_count=3,
            error="Test error"
        )
        
        # Item 3: eligible for retry
        item3 = DownloadItem(
            id="eligible-2",
            serie_id="series-1",
            serie_folder="Test Series (2024)",
            serie_name="Test Series",
            episode=EpisodeIdentifier(season=1, episode=3),
            status=DownloadStatus.FAILED,
            retry_count=1,
            error="Test error"
        )
        
        download_service._failed_items.extend([item1, item2, item3])
        
        # Retry all
        retried_ids = await download_service.retry_failed()
        
        # Only 2 should be retried
        assert len(retried_ids) == 2
        assert item1.id in retried_ids
        assert item3.id in retried_ids
        assert item2.id not in retried_ids
        
        # item2 should still be in failed
        assert len(download_service._failed_items) == 1
        assert download_service._failed_items[0].id == item2.id
    
    @pytest.mark.asyncio
    async def test_configurable_max_retries(self):
        """Test that max_retries is configurable."""
        mock_anime_service = Mock()
        
        # Service with max_retries=5
        download_service = DownloadService(anime_service=mock_anime_service, max_retries=5)
        
        # Item with 4 retries
        failed_item = DownloadItem(
            id="test-1",
            serie_id="series-1",
            serie_folder="Test Series (2024)",
            serie_name="Test Series",
            episode=EpisodeIdentifier(season=1, episode=1),
            status=DownloadStatus.FAILED,
            retry_count=4,
            error="Test error"
        )
        download_service._failed_items.append(failed_item)
        
        # Should still be retried (4 < 5)
        retried_ids = await download_service.retry_failed()
        assert len(retried_ids) == 1
        
        # Now with 5 retries
        item = download_service._pending_queue.popleft()
        item.status = DownloadStatus.FAILED
        download_service._failed_items.append(item)
        
        # Should NOT be retried (5 >= 5)
        retried_ids = await download_service.retry_failed()
        assert len(retried_ids) == 0


class TestRetryStateManagement:
    """Test retry state management and persistence."""
    
    @pytest.mark.asyncio
    async def test_error_cleared_on_retry(self):
        """Test that error message is cleared on retry."""
        mock_anime_service = Mock()
        download_service = DownloadService(anime_service=mock_anime_service, max_retries=3)
        
        # Failed item with error
        failed_item = DownloadItem(
            id="test-1",
            serie_id="series-1",
            serie_folder="Test Series (2024)",
            serie_name="Test Series",
            episode=EpisodeIdentifier(season=1, episode=1),
            status=DownloadStatus.FAILED,
            retry_count=0,
            error="Network timeout error"
        )
        download_service._failed_items.append(failed_item)
        
        # Retry
        await download_service.retry_failed()
        
        # Error should be cleared
        pending_item = download_service._pending_queue[0]
        assert pending_item.error is None
    
    @pytest.mark.asyncio
    async def test_progress_cleared_on_retry(self):
        """Test that progress is cleared on retry."""
        mock_anime_service = Mock()
        download_service = DownloadService(anime_service=mock_anime_service, max_retries=3)
        
        # Failed item with partial progress
        failed_item = DownloadItem(
            id="test-1",
            serie_id="series-1",
            serie_folder="Test Series (2024)",
            serie_name="Test Series",
            episode=EpisodeIdentifier(season=1, episode=1),
            status=DownloadStatus.FAILED,
            retry_count=0,
            error="Connection lost",
            progress={"percent": 45.5}
        )
        download_service._failed_items.append(failed_item)
        
        # Retry
        await download_service.retry_failed()
        
        # Progress should be cleared
        pending_item = download_service._pending_queue[0]
        assert pending_item.progress is None
    
    @pytest.mark.asyncio
    async def test_status_updated_on_retry(self):
        """Test that status changes from FAILED to PENDING."""
        mock_anime_service = Mock()
        download_service = DownloadService(anime_service=mock_anime_service, max_retries=3)
        
        failed_item = DownloadItem(
            id="test-1",
            serie_id="series-1",
            serie_folder="Test Series (2024)",
            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)
        
        assert failed_item.status == DownloadStatus.FAILED
        
        # Retry
        await download_service.retry_failed()
        
        # Status should change to PENDING
        pending_item = download_service._pending_queue[0]
        assert pending_item.status == DownloadStatus.PENDING
    
    @pytest.mark.asyncio
    async def test_selective_retry_by_ids(self):
        """Test retrying only specific items by IDs."""
        mock_anime_service = Mock()
        download_service = DownloadService(anime_service=mock_anime_service, max_retries=3)
        
        # Create 3 failed items
        item1 = DownloadItem(
            id="item-1",
            serie_id="series-1",
            serie_folder="Test Series (2024)",
            serie_name="Test Series",
            episode=EpisodeIdentifier(season=1, episode=1),
            status=DownloadStatus.FAILED,
            retry_count=0,
            error="Test error"
        )
        
        item2 = DownloadItem(
            id="item-2",
            serie_id="series-1",
            serie_folder="Test Series (2024)",
            serie_name="Test Series",
            episode=EpisodeIdentifier(season=1, episode=2),
            status=DownloadStatus.FAILED,
            retry_count=0,
            error="Test error"
        )
        
        item3 = DownloadItem(
            id="item-3",
            serie_id="series-1",
            serie_folder="Test Series (2024)",
            serie_name="Test Series",
            episode=EpisodeIdentifier(season=1, episode=3),
            status=DownloadStatus.FAILED,
            retry_count=0,
            error="Test error"
        )
        
        download_service._failed_items.extend([item1, item2, item3])
        
        # Retry only item-1 and item-3
        retried_ids = await download_service.retry_failed(["item-1", "item-3"])
        
        # Only 2 should be retried
        assert len(retried_ids) == 2
        assert "item-1" in retried_ids
        assert "item-3" in retried_ids
        
        # item-2 should still be in failed
        assert len(download_service._failed_items) == 1
        assert download_service._failed_items[0].id == "item-2"


class TestExponentialBackoff:
    """Test exponential backoff implementation in ImageDownloader."""
    
    @pytest.mark.asyncio
    async def test_image_downloader_retry_with_backoff(self):
        """Test that ImageDownloader implements exponential backoff."""
        from pathlib import Path
        from unittest.mock import MagicMock

        import aiohttp

        from src.core.utils.image_downloader import ImageDownloader, ImageDownloadError
        
        downloader = ImageDownloader(max_retries=3, retry_delay=0.1)
        
        # Mock session with failures
        mock_session = AsyncMock()
        mock_session.closed = False
        
        mock_response = AsyncMock()
        mock_response.status = 500
        
        # Create mock RequestInfo
        mock_request_info = MagicMock()
        mock_request_info.real_url = "https://test.com/image.jpg"
        
        mock_response.raise_for_status = MagicMock(
            side_effect=aiohttp.ClientResponseError(
                request_info=mock_request_info,
                history=(),
                status=500,
                message="Server Error"
            )
        )
        
        # Setup context manager
        mock_cm = MagicMock()
        mock_cm.__aenter__ = AsyncMock(return_value=mock_response)
        mock_cm.__aexit__ = AsyncMock(return_value=None)
        mock_session.get = MagicMock(return_value=mock_cm)
        
        downloader.session = mock_session
        
        # Track timing for backoff validation
        import time
        start_time = time.time()
        
        # Attempt download (should fail after retries)
        try:
            await downloader.download_image(
                "https://test.com/image.jpg",
                Path("/tmp/test.jpg")
            )
        except ImageDownloadError:
            pass  # Expected
        
        elapsed = time.time() - start_time
        
        # With initial delay 0.1s and doubling:
        # Attempt 1: immediate
        # Attempt 2: wait 0.1s
        # Attempt 3: wait 0.2s
        # Total: at least 0.3s
        assert elapsed >= 0.3, f"Backoff too fast: {elapsed}s"
        
        # Should have attempted 3 times
        assert mock_session.get.call_count == 3
