"""Integration tests for NFO batch workflow.

This module tests end-to-end batch NFO workflows including:
- Creating NFO files for 10+ series simultaneously
- Media file download (poster, logo, fanart) in batch
- TMDB API rate limiting during batch operations
- WebSocket progress notifications during batch operations
"""
import asyncio
from pathlib import Path
from unittest.mock import AsyncMock, Mock, patch

import pytest

from src.core.entities.series import Serie
from src.core.services.nfo_service import NFOService
from src.server.api.nfo import batch_create_nfo
from src.server.models.nfo import NFOBatchCreateRequest


@pytest.fixture
def large_series_app():
    """Create a mock SeriesApp with 15 series for batch testing."""
    app = Mock()
    
    series = []
    for i in range(15):
        serie = Mock(spec=Serie)
        serie.key = f"anime{i:02d}"
        serie.folder = f"Anime {i:02d}"
        serie.name = f"Test Anime {i:02d}"
        serie.year = 2020 + (i % 5)
        serie.ensure_folder_with_year = Mock(
            return_value=f"Anime {i:02d} ({2020 + (i % 5)})"
        )
        series.append(serie)
    
    app.list = Mock()
    app.list.GetList = Mock(return_value=series)
    
    return app


@pytest.fixture
def mock_nfo_service_with_media():
    """Create a mock NFO service that simulates media downloads."""
    service = Mock(spec=NFOService)
    service.check_nfo_exists = AsyncMock(return_value=False)
    
    # Simulate NFO creation with media download time
    async def create_with_delay(*args, **kwargs):
        await asyncio.sleep(0.1)  # Simulate TMDB API call + file writing
        # Get serie_folder from kwargs or args
        serie_folder = kwargs.get('serie_folder', args[1] if len(args) > 1 else 'unknown')
        return Path(f"/fake/path/{serie_folder}/tvshow.nfo")
    
    service.create_tvshow_nfo = AsyncMock(side_effect=create_with_delay)
    
    return service


@pytest.fixture
def mock_settings():
    """Create mock settings."""
    with patch("src.server.api.nfo.settings") as mock:
        mock.anime_directory = "/fake/anime/dir"
        yield mock


class TestBatchNFOCreationWorkflow:
    """Tests for creating NFO files for multiple series."""

    @pytest.mark.asyncio
    async def test_create_nfos_for_10_plus_series(
        self,
        large_series_app,
        mock_nfo_service_with_media,
        mock_settings
    ):
        """Test creating NFO files for 15 series simultaneously."""
        request = NFOBatchCreateRequest(
            serie_ids=[f"anime{i:02d}" for i in range(15)],
            download_media=True,
            skip_existing=False,
            max_concurrent=5
        )
        
        import time
        start_time = time.time()
        
        with patch("src.server.api.nfo.get_series_app", return_value=large_series_app), \
             patch("src.server.api.nfo.get_nfo_service", return_value=mock_nfo_service_with_media):
            
            result = await batch_create_nfo(
                request=request,
                _auth={"username": "test"},
                series_app=large_series_app,
                nfo_service=mock_nfo_service_with_media
            )
        
        elapsed_time = time.time() - start_time
        
        # Verify all created successfully
        assert result.total == 15
        assert result.successful == 15
        assert result.failed == 0
        
        # Verify all results present
        assert len(result.results) == 15
        
        # Verify NFO paths are set
        for res in result.results:
            assert res.success
            assert res.nfo_path is not None
            assert "tvshow.nfo" in res.nfo_path
        
        # Verify concurrency (should be faster than sequential)
        # Sequential would take 15 * 0.1 = 1.5s
        # With max_concurrent=5, should take ~0.3s (3 batches)
        assert elapsed_time < 1.0  # Allow some overhead

    @pytest.mark.asyncio
    async def test_batch_creation_performance(
        self,
        large_series_app,
        mock_nfo_service_with_media,
        mock_settings
    ):
        """Test that batch operations complete in reasonable time."""
        request = NFOBatchCreateRequest(
            serie_ids=[f"anime{i:02d}" for i in range(10)],
            max_concurrent=3,
            skip_existing=False
        )
        
        import time
        start_time = time.time()
        
        with patch("src.server.api.nfo.get_series_app", return_value=large_series_app), \
             patch("src.server.api.nfo.get_nfo_service", return_value=mock_nfo_service_with_media):
            
            result = await batch_create_nfo(
                request=request,
                _auth={"username": "test"},
                series_app=large_series_app,
                nfo_service=mock_nfo_service_with_media
            )
        
        elapsed_time = time.time() - start_time
        
        assert result.successful == 10
        # Should complete in under 0.5s with max_concurrent=3
        # (10 series / 3 concurrent = 4 batches * 0.1s = 0.4s + overhead)
        assert elapsed_time < 0.7


class TestBatchMediaDownloads:
    """Tests for media file downloads during batch operations."""

    @pytest.mark.asyncio
    async def test_batch_download_all_media_types(
        self,
        large_series_app,
        mock_nfo_service_with_media,
        mock_settings
    ):
        """Test that all media types are downloaded in batch."""
        request = NFOBatchCreateRequest(
            serie_ids=[f"anime{i:02d}" for i in range(5)],
            download_media=True,
            skip_existing=False
        )
        
        with patch("src.server.api.nfo.get_series_app", return_value=large_series_app), \
             patch("src.server.api.nfo.get_nfo_service", return_value=mock_nfo_service_with_media):
            
            result = await batch_create_nfo(
                request=request,
                _auth={"username": "test"},
                series_app=large_series_app,
                nfo_service=mock_nfo_service_with_media
            )
        
        # Verify all series processed
        assert result.successful == 5
        
        # Verify media downloads were requested for all
        assert mock_nfo_service_with_media.create_tvshow_nfo.call_count == 5
        
        for call in mock_nfo_service_with_media.create_tvshow_nfo.call_args_list:
            kwargs = call[1]
            assert kwargs["download_poster"] is True
            assert kwargs["download_logo"] is True
            assert kwargs["download_fanart"] is True

    @pytest.mark.asyncio
    async def test_batch_without_media_downloads(
        self,
        large_series_app,
        mock_nfo_service_with_media,
        mock_settings
    ):
        """Test batch operation without media downloads is faster."""
        # NFO service without media delay
        fast_service = Mock(spec=NFOService)
        fast_service.check_nfo_exists = AsyncMock(return_value=False)
        fast_service.create_tvshow_nfo = AsyncMock(
            return_value=Path("/fake/path/tvshow.nfo")
        )
        
        request = NFOBatchCreateRequest(
            serie_ids=[f"anime{i:02d}" for i in range(10)],
            download_media=False,
            skip_existing=False,
            max_concurrent=5
        )
        
        import time
        start_time = time.time()
        
        with patch("src.server.api.nfo.get_series_app", return_value=large_series_app), \
             patch("src.server.api.nfo.get_nfo_service", return_value=fast_service):
            
            result = await batch_create_nfo(
                request=request,
                _auth={"username": "test"},
                series_app=large_series_app,
                nfo_service=fast_service
            )
        
        elapsed_time = time.time() - start_time
        
        assert result.successful == 10
        # Without media downloads, should be very fast
        assert elapsed_time < 0.3
        
        # Verify no media was requested
        for call in fast_service.create_tvshow_nfo.call_args_list:
            kwargs = call[1]
            assert kwargs["download_poster"] is False
            assert kwargs["download_logo"] is False
            assert kwargs["download_fanart"] is False

    @pytest.mark.asyncio
    async def test_media_download_failures_dont_block_batch(
        self,
        large_series_app,
        mock_settings
    ):
        """Test that media download failures don't stop batch processing."""
        service = Mock(spec=NFOService)
        service.check_nfo_exists = AsyncMock(return_value=False)
        
        # Simulate media download failures for some series
        async def selective_media_failure(serie_name, serie_folder, **kwargs):
            # Series 2 and 4 have media download issues
            if "02" in serie_folder or "04" in serie_folder:
                # Still create NFO but media fails
                await asyncio.sleep(0.05)
                return Path(f"/fake/{serie_folder}/tvshow.nfo")
            await asyncio.sleep(0.05)
            return Path(f"/fake/{serie_folder}/tvshow.nfo")
        
        service.create_tvshow_nfo = AsyncMock(side_effect=selective_media_failure)
        
        request = NFOBatchCreateRequest(
            serie_ids=[f"anime{i:02d}" for i in range(6)],
            download_media=True,
            skip_existing=False
        )
        
        with patch("src.server.api.nfo.get_series_app", return_value=large_series_app), \
             patch("src.server.api.nfo.get_nfo_service", return_value=service):
            
            result = await batch_create_nfo(
                request=request,
                _auth={"username": "test"},
                series_app=large_series_app,
                nfo_service=service
            )
        
        # All should succeed (NFO created even if media failed)
        assert result.successful == 6


class TestTMDBAPIRateLimiting:
    """Tests for TMDB API rate limiting during batch operations."""

    @pytest.mark.asyncio
    async def test_rate_limiting_with_delays(
        self,
        large_series_app,
        mock_settings
    ):
        """Test that batch operations handle TMDB rate limiting."""
        service = Mock(spec=NFOService)
        service.check_nfo_exists = AsyncMock(return_value=False)
        
        call_times = []
        
        async def track_api_calls(*args, **kwargs):
            import time
            call_times.append(time.time())
            # Simulate rate limit delay for 3rd call
            if len(call_times) == 3:
                await asyncio.sleep(0.2)  # Simulate rate limit wait
            else:
                await asyncio.sleep(0.05)
            return Path("/fake/path/tvshow.nfo")
        
        service.create_tvshow_nfo = AsyncMock(side_effect=track_api_calls)
        
        request = NFOBatchCreateRequest(
            serie_ids=[f"anime{i:02d}" for i in range(5)],
            max_concurrent=2,
            skip_existing=False
        )
        
        with patch("src.server.api.nfo.get_series_app", return_value=large_series_app), \
             patch("src.server.api.nfo.get_nfo_service", return_value=service):
            
            result = await batch_create_nfo(
                request=request,
                _auth={"username": "test"},
                series_app=large_series_app,
                nfo_service=service
            )
        
        # All should complete despite rate limiting
        assert result.successful == 5
        assert len(call_times) == 5

    @pytest.mark.asyncio
    async def test_concurrent_limit_reduces_rate_limit_risk(
        self,
        large_series_app,
        mock_nfo_service_with_media,
        mock_settings
    ):
        """Test that lower max_concurrent reduces rate limit risk."""
        # Test with low concurrency
        request_low = NFOBatchCreateRequest(
            serie_ids=[f"anime{i:02d}" for i in range(10)],
            max_concurrent=2,  # Low concurrency
            skip_existing=False
        )
        
        with patch("src.server.api.nfo.get_series_app", return_value=large_series_app), \
             patch("src.server.api.nfo.get_nfo_service", return_value=mock_nfo_service_with_media):
            
            result = await batch_create_nfo(
                request=request_low,
                _auth={"username": "test"},
                series_app=large_series_app,
                nfo_service=mock_nfo_service_with_media
            )
        
        assert result.successful == 10
        
        # Test with high concurrency
        request_high = NFOBatchCreateRequest(
            serie_ids=[f"anime{i:02d}" for i in range(10)],
            max_concurrent=10,  # High concurrency
            skip_existing=False
        )
        
        # Reset mock
        mock_nfo_service_with_media.create_tvshow_nfo.reset_mock()
        
        with patch("src.server.api.nfo.get_series_app", return_value=large_series_app), \
             patch("src.server.api.nfo.get_nfo_service", return_value=mock_nfo_service_with_media):
            
            result = await batch_create_nfo(
                request=request_high,
                _auth={"username": "test"},
                series_app=large_series_app,
                nfo_service=mock_nfo_service_with_media
            )
        
        # Both should succeed, but high concurrency is riskier
        assert result.successful == 10


class TestBatchWorkflowCompleteScenarios:
    """Tests for complete batch workflow scenarios."""

    @pytest.mark.asyncio
    async def test_mixed_existing_and_new_nfos(
        self,
        large_series_app,
        mock_nfo_service_with_media,
        mock_settings
    ):
        """Test batch with mix of existing and new NFOs."""
        # Series 0, 2, 4, 6, 8 already have NFOs (pattern: even numbers 0-8)
        async def check_exists(serie_folder):
            # Check for exact ID matches to avoid false positives like "01" matching "10"
            for i in [0, 2, 4, 6, 8]:
                if f" {i:02d}" in serie_folder:  # Match " 00", " 02", etc.
                    return True
            return False
        
        mock_nfo_service_with_media.check_nfo_exists.side_effect = check_exists
        
        request = NFOBatchCreateRequest(
            serie_ids=[f"anime{i:02d}" for i in range(10)],
            skip_existing=True,
            download_media=True,
            max_concurrent=3
        )
        
        with patch("src.server.api.nfo.get_series_app", return_value=large_series_app), \
             patch("src.server.api.nfo.get_nfo_service", return_value=mock_nfo_service_with_media):
            
            result = await batch_create_nfo(
                request=request,
                _auth={"username": "test"},
                series_app=large_series_app,
                nfo_service=mock_nfo_service_with_media
            )
        
        # 7 new, 3 skipped (but anime04 doesn't exist, so actually 5 skipped in the first 10)
        # Actually: 00, 02, 04, 06, 08 have NFOs = 5 skipped, 5 created
        assert result.total == 10
        # anime00, anime02, anime04, anime06, anime08 skipped
        assert result.skipped == 5
        assert result.successful == 5

    @pytest.mark.asyncio
    async def test_batch_with_partial_failures_and_skips(
        self,
        large_series_app,
        mock_settings
    ):
        """Test batch with combination of successes, failures, and skips."""
        service = Mock(spec=NFOService)
        
        # Series 1, 3, 5 already exist
        async def check_exists(serie_folder):
            # Match exact IDs to avoid false positives
            for i in [1, 3, 5]:
                if f" {i:02d}" in serie_folder:  # Match " 01", " 03", " 05"
                    return True
            return False
        
        service.check_nfo_exists = AsyncMock(side_effect=check_exists)
        
        # Series 2, 6 fail
        async def selective_failure(*args, **kwargs):
            serie_folder = kwargs.get('serie_folder', args[1] if len(args) > 1 else 'unknown')
            # Check for exact ID matches: " 02" and " 06"
            if " 02" in serie_folder or " 06" in serie_folder:
                raise Exception("TMDB API error")
            await asyncio.sleep(0.05)
            return Path(f"/fake/{serie_folder}/tvshow.nfo")
        
        service.create_tvshow_nfo = AsyncMock(side_effect=selective_failure)
        
        request = NFOBatchCreateRequest(
            serie_ids=[f"anime{i:02d}" for i in range(10)],
            skip_existing=True,
            max_concurrent=3
        )
        
        with patch("src.server.api.nfo.get_series_app", return_value=large_series_app), \
             patch("src.server.api.nfo.get_nfo_service", return_value=service):
            
            result = await batch_create_nfo(
                request=request,
                _auth={"username": "test"},
                series_app=large_series_app,
                nfo_service=service
            )
        
        assert result.total == 10
        # Skipped: 01, 03, 05 = 3
        # Failed: 02, 06 = 2
        # Success: 00, 04, 07, 08, 09 = 5
        assert result.skipped == 3
        assert result.failed == 2
        assert result.successful == 5

    @pytest.mark.asyncio
    async def test_full_library_nfo_creation(
        self,
        mock_settings
    ):
        """Test creating NFOs for entire library (realistic scenario)."""
        # Create app with 50 series
        app = Mock()
        series = []
        for i in range(50):
            serie = Mock(spec=Serie)
            serie.key = f"anime{i:03d}"
            serie.folder = f"Anime {i:03d}"
            serie.name = f"Test Anime {i:03d}"
            serie.ensure_folder_with_year = Mock(return_value=f"Anime {i:03d} (2020)")
            series.append(serie)
        app.list = Mock()
        app.list.GetList = Mock(return_value=series)
        
        service = Mock(spec=NFOService)
        service.check_nfo_exists = AsyncMock(return_value=False)
        
        async def fast_create(*args, **kwargs):
            await asyncio.sleep(0.01)  # Very fast for testing
            return Path("/fake/path/tvshow.nfo")
        
        service.create_tvshow_nfo = AsyncMock(side_effect=fast_create)
        
        request = NFOBatchCreateRequest(
            serie_ids=[f"anime{i:03d}" for i in range(50)],
            download_media=False,  # Faster for testing
            skip_existing=False,
            max_concurrent=10  # High concurrency for large batch
        )
        
        import time
        start_time = time.time()
        
        with patch("src.server.api.nfo.get_series_app", return_value=app), \
             patch("src.server.api.nfo.get_nfo_service", return_value=service):
            
            result = await batch_create_nfo(
                request=request,
                _auth={"username": "test"},
                series_app=app,
                nfo_service=service
            )
        
        elapsed_time = time.time() - start_time
        
        # Verify all created
        assert result.total == 50
        assert result.successful == 50
        assert result.failed == 0
        
        # Should complete quickly with high concurrency
        # 50 series / 10 concurrent = 5 batches * 0.01s = 0.05s + overhead
        assert elapsed_time < 0.3

    @pytest.mark.asyncio
    async def test_batch_operation_result_detail(
        self,
        large_series_app,
        mock_nfo_service_with_media,
        mock_settings
    ):
        """Test that batch results contain all necessary details."""
        request = NFOBatchCreateRequest(
            serie_ids=[f"anime{i:02d}" for i in range(5)],
            download_media=True,
            skip_existing=False
        )
        
        with patch("src.server.api.nfo.get_series_app", return_value=large_series_app), \
             patch("src.server.api.nfo.get_nfo_service", return_value=mock_nfo_service_with_media):
            
            result = await batch_create_nfo(
                request=request,
                _auth={"username": "test"},
                series_app=large_series_app,
                nfo_service=mock_nfo_service_with_media
            )
        
        # Verify result structure
        assert result.total == 5
        assert len(result.results) == 5
        
        for res in result.results:
            # Each result should have required fields
            assert res.serie_id is not None
            assert res.serie_folder is not None
            assert res.success is not None
            assert res.message is not None
            
            if res.success:
                # Successful results should have NFO path
                assert res.nfo_path is not None
                assert Path(res.nfo_path).name == "tvshow.nfo"


class TestBatchOperationRobustness:
    """Tests for batch operation robustness and resilience."""

    @pytest.mark.asyncio
    async def test_batch_handles_slow_series(
        self,
        large_series_app,
        mock_settings
    ):
        """Test that batch handles slow series without blocking others."""
        service = Mock(spec=NFOService)
        service.check_nfo_exists = AsyncMock(return_value=False)
        
        # anime02 is very slow
        async def variable_speed_create(serie_name, serie_folder, **kwargs):
            if "02" in serie_folder:
                await asyncio.sleep(0.5)  # Very slow
            else:
                await asyncio.sleep(0.05)  # Normal speed
            return Path(f"/fake/{serie_folder}/tvshow.nfo")
        
        service.create_tvshow_nfo = AsyncMock(side_effect=variable_speed_create)
        
        request = NFOBatchCreateRequest(
            serie_ids=[f"anime{i:02d}" for i in range(6)],
            max_concurrent=3,
            skip_existing=False
        )
        
        import time
        start_time = time.time()
        
        with patch("src.server.api.nfo.get_series_app", return_value=large_series_app), \
             patch("src.server.api.nfo.get_nfo_service", return_value=service):
            
            result = await batch_create_nfo(
                request=request,
                _auth={"username": "test"},
                series_app=large_series_app,
                nfo_service=service
            )
        
        elapsed_time = time.time() - start_time
        
        # All should complete
        assert result.successful == 6
        
        # Should not take as long as sequential
        # Sequential: 5*0.05 + 0.5 = 0.75s
        # Concurrent: max(0.5, 5*0.05/3) ≈ 0.5s
        # Allow some overhead for async scheduling
        assert elapsed_time < 1.2

    @pytest.mark.asyncio
    async def test_batch_operation_idempotency(
        self,
        large_series_app,
        mock_nfo_service_with_media,
        mock_settings
    ):
        """Test that running same batch twice is safe."""
        request = NFOBatchCreateRequest(
            serie_ids=[f"anime{i:02d}" for i in range(3)],
            skip_existing=False,  # Overwrite
            download_media=False
        )
        
        # First run
        with patch("src.server.api.nfo.get_series_app", return_value=large_series_app), \
             patch("src.server.api.nfo.get_nfo_service", return_value=mock_nfo_service_with_media):
            
            result1 = await batch_create_nfo(
                request=request,
                _auth={"username": "test"},
                series_app=large_series_app,
                nfo_service=mock_nfo_service_with_media
            )
        
        # Second run (idempotent)
        mock_nfo_service_with_media.create_tvshow_nfo.reset_mock()
        
        with patch("src.server.api.nfo.get_series_app", return_value=large_series_app), \
             patch("src.server.api.nfo.get_nfo_service", return_value=mock_nfo_service_with_media):
            
            result2 = await batch_create_nfo(
                request=request,
                _auth={"username": "test"},
                series_app=large_series_app,
                nfo_service=mock_nfo_service_with_media
            )
        
        # Both should succeed with same results
        assert result1.successful == result2.successful == 3
        assert result1.total == result2.total == 3
