"""Unit tests for concurrent scan operation handling.

This module tests that the system properly handles multiple simultaneous
scan requests, prevents race conditions, and maintains consistent state.
"""
import asyncio
from unittest.mock import AsyncMock, Mock, patch

import pytest

from src.server.services.scan_service import (
    ScanService,
    ScanServiceError,
    get_scan_service,
    reset_scan_service,
)


class TestConcurrentScanPrevention:
    """Test that concurrent scans are properly prevented."""
    
    def setup_method(self):
        """Reset scan service before each test."""
        reset_scan_service()
    
    @pytest.mark.asyncio
    async def test_second_scan_blocked_while_first_running(self):
        """Test that second scan request is blocked while first is running."""
        scan_service = get_scan_service()
        
        # Mock scanner
        scanner = Mock()
        scanner.subscribe_on_progress = Mock()
        scanner.subscribe_on_error = Mock()
        scanner.subscribe_on_completion = Mock()
        scanner.unsubscribe_on_progress = Mock()
        scanner.unsubscribe_on_error = Mock()
        scanner.unsubscribe_on_completion = Mock()
        scanner.scan = Mock()
        
        # Start first scan but keep it running
        scan1_started = False
        scan1_can_finish = asyncio.Event()
        
        async def slow_scan_simulation():
            nonlocal scan1_started
            scan1_started = True
            # Don't finish until signaled
            await scan1_can_finish.wait()
        
        # Override progress service methods
        with patch.object(scan_service._progress_service, 'start_progress', new_callable=AsyncMock), \
             patch.object(scan_service._progress_service, 'update_progress', new_callable=AsyncMock):
            
            # Start first scan in background
            task1 = asyncio.create_task(scan_service.start_scan(scanner))
            
            # Wait for first scan to acquire lock
            await asyncio.sleep(0.1)
            assert scan1_started or scan_service._is_scanning
            
            # Try to start second scan - should raise error
            with pytest.raises(ScanServiceError, match="already in progress"):
                await scan_service.start_scan(scanner)
            
            # Allow first scan to finish
            scan1_can_finish.set()
            
            # Wait for completion
            await task1
    
    @pytest.mark.asyncio
    async def test_multiple_scan_attempts_queued(self):
        """Test that multiple scan attempts properly queue."""
        scan_service = get_scan_service()
        
        scanner = Mock()
        scanner.subscribe_on_progress = Mock()
        scanner.subscribe_on_error = Mock()
        scanner.subscribe_on_completion = Mock()
        scanner.unsubscribe_on_progress = Mock()
        scanner.unsubscribe_on_error = Mock()
        scanner.unsubscribe_on_completion = Mock()
        
        scan_count = 0
        lock = asyncio.Lock()
        
        async def attempt_scan():
            nonlocal scan_count
            try:
                async with lock:
                    # Only one scan at a time
                    with patch.object(scan_service._progress_service, 'start_progress', new_callable=AsyncMock), \
                         patch.object(scan_service._progress_service, 'update_progress', new_callable=AsyncMock):
                        await scan_service.start_scan(scanner)
                        scan_count += 1
            except ScanServiceError:
                # Expected for attempts while locked
                pass
        
        # Start 5 concurrent attempts
        tasks = [asyncio.create_task(attempt_scan()) for _ in range(5)]
        
        # Wait for all to complete
        await asyncio.gather(*tasks, return_exceptions=True)
        
        # Only one should have succeeded
        assert scan_count <= 1, "More than one scan executed concurrently"
    
    @pytest.mark.asyncio
    async def test_scan_lock_released_after_completion(self):
        """Test that scan lock is properly released after completion."""
        scan_service = get_scan_service()
        
        scanner = Mock()
        scanner.subscribe_on_progress = Mock()
        scanner.subscribe_on_error = Mock()
        scanner.subscribe_on_completion = Mock()
        scanner.unsubscribe_on_progress = Mock()
        scanner.unsubscribe_on_error = Mock()
        scanner.unsubscribe_on_completion = Mock()
        
        with patch.object(scan_service._progress_service, 'start_progress', new_callable=AsyncMock), \
             patch.object(scan_service._progress_service, 'update_progress', new_callable=AsyncMock):
            
            # First scan
            scan1_id = await scan_service.start_scan(scanner)
            assert scan1_id is not None
            
            # Manually mark as complete
            async with scan_service._lock:
                scan_service._is_scanning = False
            
            # Second scan should now succeed
            scan2_id = await scan_service.start_scan(scanner)
            assert scan2_id is not None
            assert scan2_id != scan1_id
    
    @pytest.mark.asyncio
    async def test_scan_lock_released_after_error(self):
        """Test that scan lock is released even after scan completion."""
        scan_service = get_scan_service()
        
        scanner = Mock()
        scanner.subscribe_on_progress = Mock()
        scanner.subscribe_on_error = Mock()
        scanner.subscribe_on_completion = Mock()
        scanner.unsubscribe_on_progress = Mock()
        scanner.unsubscribe_on_error = Mock()
        scanner.unsubscribe_on_completion = Mock()
        
        with patch.object(scan_service._progress_service, 'start_progress', new_callable=AsyncMock), \
             patch.object(scan_service._progress_service, 'update_progress', new_callable=AsyncMock):
            
            # First scan
            await scan_service.start_scan(scanner)
            
            # Manually complete it
            async with scan_service._lock:
                scan_service._is_scanning = False
            
            # Lock should be released
            assert not scan_service._is_scanning
            
            # Second scan should succeed
            scan_id = await scan_service.start_scan(scanner)
            assert scan_id is not None


class TestScanCancellation:
    """Test scan cancellation and interruption handling."""
    
    def setup_method(self):
        """Reset scan service before each test."""
        reset_scan_service()
    
    @pytest.mark.asyncio
    async def test_cancel_active_scan(self):
        """Test cancelling an active scan."""
        scan_service = get_scan_service()
        
        scanner = Mock()
        scanner.subscribe_on_progress = Mock()
        scanner.subscribe_on_error = Mock()
        scanner.subscribe_on_completion = Mock()
        scanner.unsubscribe_on_progress = Mock()
        scanner.unsubscribe_on_error = Mock()
        scanner.unsubscribe_on_completion = Mock()
        
        with patch.object(scan_service._progress_service, 'start_progress', new_callable=AsyncMock), \
             patch.object(scan_service._progress_service, 'update_progress', new_callable=AsyncMock), \
             patch.object(scan_service._progress_service, 'fail_progress', new_callable=AsyncMock):
            
            # Start scan
            await scan_service.start_scan(scanner)
            
            # Scan should be active
            assert scan_service.is_scanning
            
            # Cancel it
            result = await scan_service.cancel_scan()
            
            # Should return True (cancelled)
            assert result is True
            
            # Scan should no longer be active
            assert not scan_service.is_scanning
            
            # Current scan status should be "cancelled"
            assert scan_service.current_scan.status == "cancelled"
    
    @pytest.mark.asyncio
    async def test_cancel_when_no_scan_active(self):
        """Test cancelling when no scan is active."""
        scan_service = get_scan_service()
        
        # No scan running
        assert not scan_service.is_scanning
        
        # Try to cancel
        result = await scan_service.cancel_scan()
        
        # Should return False (nothing to cancel)
        assert result is False
    
    @pytest.mark.asyncio
    async def test_cancellation_adds_to_history(self):
        """Test that cancelled scans are added to history."""
        scan_service = get_scan_service()
        
        scanner = Mock()
        scanner.subscribe_on_progress = Mock()
        scanner.subscribe_on_error = Mock()
        scanner.subscribe_on_completion = Mock()
        scanner.unsubscribe_on_progress = Mock()
        scanner.unsubscribe_on_error = Mock()
        scanner.unsubscribe_on_completion = Mock()
        
        with patch.object(scan_service._progress_service, 'start_progress', new_callable=AsyncMock), \
             patch.object(scan_service._progress_service, 'update_progress', new_callable=AsyncMock), \
             patch.object(scan_service._progress_service, 'fail_progress', new_callable=AsyncMock):
            
            # Start and cancel scan
            await scan_service.start_scan(scanner)
            await scan_service.cancel_scan()
            
            # Check history
            history = await scan_service.get_scan_history()
            assert len(history) > 0
            assert history[0]["status"] == "cancelled"
    
    @pytest.mark.asyncio
    async def test_new_scan_after_cancellation(self):
        """Test that new scan can start after cancellation."""
        scan_service = get_scan_service()
        
        scanner = Mock()
        scanner.subscribe_on_progress = Mock()
        scanner.subscribe_on_error = Mock()
        scanner.subscribe_on_completion = Mock()
        scanner.unsubscribe_on_progress = Mock()
        scanner.unsubscribe_on_error = Mock()
        scanner.unsubscribe_on_completion = Mock()
        
        with patch.object(scan_service._progress_service, 'start_progress', new_callable=AsyncMock), \
             patch.object(scan_service._progress_service, 'update_progress', new_callable=AsyncMock), \
             patch.object(scan_service._progress_service, 'fail_progress', new_callable=AsyncMock):
            
            # First scan
            scan1_id = await scan_service.start_scan(scanner)
            await scan_service.cancel_scan()
            
            # Should be able to start new scan
            scan2_id = await scan_service.start_scan(scanner)
            assert scan2_id != scan1_id


class TestDatabaseRaceConditions:
    """Test prevention of database race conditions during scans."""
    
    def setup_method(self):
        """Reset scan service before each test."""
        reset_scan_service()
    
    @pytest.mark.asyncio
    async def test_scan_service_lock_prevents_race_conditions(self):
        """Test that asyncio lock prevents race conditions."""
        scan_service = get_scan_service()
        
        # Verify lock exists
        assert hasattr(scan_service, '_lock')
        assert isinstance(scan_service._lock, asyncio.Lock)
        
        # Test acquiring lock prevents concurrent access
        async with scan_service._lock:
            # Lock is held
            assert scan_service._lock.locked()
            
            # Try to acquire in another context (should timeout)
            try:
                await asyncio.wait_for(
                    scan_service._lock.acquire(),
                    timeout=0.1
                )
                # Should not reach here
                assert False, "Lock was acquired when it should be held"
            except asyncio.TimeoutError:
                # Expected - lock is properly held
                pass
    
    @pytest.mark.asyncio
    async def test_scan_state_consistency_with_concurrent_reads(self):
        """Test scan state remains consistent with concurrent reads."""
        scan_service = get_scan_service()
        
        # Initial state
        assert not scan_service.is_scanning
        assert scan_service.current_scan is None
        
        # Simulate concurrent state reads
        async def read_state():
            status = await scan_service.get_scan_status()
            return status["is_scanning"]
        
        # Run multiple concurrent reads
        results = await asyncio.gather(*[read_state() for _ in range(10)])
        
        # All should be consistent
        assert all(r == False for r in results)
    
    @pytest.mark.asyncio
    async def test_scan_history_thread_safe_updates(self):
        """Test that scan history updates are thread-safe."""
        scan_service = get_scan_service()
        
        scanner = Mock()
        scanner.subscribe_on_progress = Mock()
        scanner.subscribe_on_error = Mock()
        scanner.subscribe_on_completion = Mock()
        scanner.unsubscribe_on_progress = Mock()
        scanner.unsubscribe_on_error = Mock()
        scanner.unsubscribe_on_completion = Mock()
        
        with patch.object(scan_service._progress_service, 'start_progress', new_callable=AsyncMock), \
             patch.object(scan_service._progress_service, 'update_progress', new_callable=AsyncMock), \
             patch.object(scan_service._progress_service, 'fail_progress', new_callable=AsyncMock):
            
            # Start multiple scans sequentially
            for i in range(5):
                await scan_service.start_scan(scanner)
                await scan_service.cancel_scan()
            
            # Read history concurrently
            async def read_history():
                return await scan_service.get_scan_history()
            
            histories = await asyncio.gather(*[read_history() for _ in range(10)])
            
            # All reads should return same length
            lengths = [len(h) for h in histories]
            assert all(length == lengths[0] for length in lengths)


class TestScanStateConsistency:
    """Test scan state consistency with concurrent requests."""
    
    def setup_method(self):
        """Reset scan service before each test."""
        reset_scan_service()
    
    @pytest.mark.asyncio
    async def test_is_scanning_flag_consistency(self):
        """Test that is_scanning flag remains consistent."""
        scan_service = get_scan_service()
        
        scanner = Mock()
        scanner.subscribe_on_progress = Mock()
        scanner.subscribe_on_error = Mock()
        scanner.subscribe_on_completion = Mock()
        scanner.unsubscribe_on_progress = Mock()
        scanner.unsubscribe_on_error = Mock()
        scanner.unsubscribe_on_completion = Mock()
        
        # Initial state
        assert not scan_service.is_scanning
        
        with patch.object(scan_service._progress_service, 'start_progress', new_callable=AsyncMock), \
             patch.object(scan_service._progress_service, 'update_progress', new_callable=AsyncMock):
            
            # Start scan
            await scan_service.start_scan(scanner)
            assert scan_service.is_scanning
            
            # Manually complete it
            async with scan_service._lock:
                scan_service._is_scanning = False
            
            # State should be consistent
            assert not scan_service.is_scanning
    
    @pytest.mark.asyncio
    async def test_current_scan_object_consistency(self):
        """Test that current_scan object remains consistent."""
        scan_service = get_scan_service()
        
        scanner = Mock()
        scanner.subscribe_on_progress = Mock()
        scanner.subscribe_on_error = Mock()
        scanner.subscribe_on_completion = Mock()
        scanner.unsubscribe_on_progress = Mock()
        scanner.unsubscribe_on_error = Mock()
        scanner.unsubscribe_on_completion = Mock()
        
        with patch.object(scan_service._progress_service, 'start_progress', new_callable=AsyncMock), \
             patch.object(scan_service._progress_service, 'update_progress', new_callable=AsyncMock):
            
            # No scan
            assert scan_service.current_scan is None
            
            # Start scan
            await scan_service.start_scan(scanner)
            scan_obj = scan_service.current_scan
            assert scan_obj is not None
            
            # Read multiple times - should be same object
            assert scan_service.current_scan is scan_obj
            assert scan_service.current_scan is scan_obj
    
    @pytest.mark.asyncio
    async def test_scan_status_api_consistency(self):
        """Test that scan status API returns consistent data."""
        scan_service = get_scan_service()
        
        # No scan running
        status1 = await scan_service.get_scan_status()
        status2 = await scan_service.get_scan_status()
        
        assert status1["is_scanning"] == status2["is_scanning"]
        assert status1["current_scan"] == status2["current_scan"]
    
    @pytest.mark.asyncio
    async def test_concurrent_status_checks_during_scan(self):
        """Test concurrent status checks while scan is running."""
        scan_service = get_scan_service()
        
        scanner = Mock()
        scanner.subscribe_on_progress = Mock()
        scanner.subscribe_on_error = Mock()
        scanner.subscribe_on_completion = Mock()
        scanner.unsubscribe_on_progress = Mock()
        scanner.unsubscribe_on_error = Mock()
        scanner.unsubscribe_on_completion = Mock()
        
        with patch.object(scan_service._progress_service, 'start_progress', new_callable=AsyncMock), \
             patch.object(scan_service._progress_service, 'update_progress', new_callable=AsyncMock):
            
            # Start scan
            await scan_service.start_scan(scanner)
            
            # Check status concurrently
            async def check_status():
                status = await scan_service.get_scan_status()
                return status["is_scanning"]
            
            results = await asyncio.gather(*[check_status() for _ in range(20)])
            
            # All should report scanning
            assert all(r is True for r in results)


class TestSchedulerConcurrentScanPrevention:
    """Test that scheduler properly prevents concurrent scans."""
    
    @pytest.mark.asyncio
    async def test_scheduler_skips_rescan_if_already_running(self):
        """Test scheduler skips scheduled rescan if one is already running."""
        from src.server.services.scheduler_service import SchedulerService
        
        scheduler = SchedulerService()
        
        # Simulate scan in progress
        scheduler._scan_in_progress = True
        
        # Try to perform rescan
        # Should return immediately without starting new scan
        with patch('src.server.utils.dependencies.get_anime_service') as mock_get_anime:
            mock_anime_service = AsyncMock()
            mock_get_anime.return_value = mock_anime_service
            
            await scheduler._perform_rescan()
            
            # anime_service.rescan should NOT have been called
            mock_anime_service.rescan.assert_not_called()
    
    @pytest.mark.asyncio
    async def test_scheduler_sets_flag_during_rescan(self):
        """Test that scheduler properly sets scan_in_progress flag."""
        from src.server.services.scheduler_service import SchedulerService
        
        scheduler = SchedulerService()
        
        # Initial state
        assert not scheduler._scan_in_progress
        
        # Mock anime service
        with patch('src.server.utils.dependencies.get_anime_service') as mock_get_anime, \
             patch('src.server.services.websocket_service.get_websocket_service') as mock_get_ws:
            
            mock_anime_service = AsyncMock()
            mock_anime_service.rescan = AsyncMock()
            mock_get_anime.return_value = mock_anime_service
            
            mock_ws_service = Mock()
            mock_ws_service.manager = Mock()
            mock_ws_service.manager.broadcast = AsyncMock()
            mock_get_ws.return_value = mock_ws_service
            
            # Start rescan
            rescan_task = asyncio.create_task(scheduler._perform_rescan())
            
            # Wait a bit
            await asyncio.sleep(0.05)
            
            # Flag should be set
            # (or rescan should be called, indicating flag was properly managed)
            await rescan_task
            
            # After completion, flag should be cleared
            assert not scheduler._scan_in_progress


class TestAnimeServiceScanLock:
    """Test AnimeService scan lock behavior."""
    
    @pytest.mark.asyncio
    async def test_anime_service_ignores_concurrent_rescan_requests(self):
        """Test that AnimeService ignores concurrent rescan requests."""
        from src.core.SeriesApp import SeriesApp
        from src.server.services.anime_service import AnimeService

        # Mock database
        mock_db = AsyncMock()
        
        # Create service with mocked app
        mock_app = Mock(spec=SeriesApp)
        mock_app.directory_to_search = "/test/anime"
        mock_app.rescan = AsyncMock(return_value=[])
        
        service = AnimeService(mock_app, mock_db)
        
        # First rescan - will acquire lock
        async def long_rescan():
            async with service._scan_lock:
                await asyncio.sleep(0.2)
        
        # Start first rescan
        task1 = asyncio.create_task(long_rescan())
        
        # Wait for lock to be acquired
        await asyncio.sleep(0.05)
        
        # Second rescan attempt - should return immediately
        # (check that it completes quickly)
        start_time = asyncio.get_event_loop().time()
        await service.rescan()  # Should return immediately
        elapsed = asyncio.get_event_loop().time() - start_time
        
        # Should have returned almost immediately (< 0.1s)
        assert elapsed < 0.1, f"Second rescan took {elapsed}s, should return immediately"
        
        # Wait for first to finish
        await task1
        
        # rescan should not have been called on mock_app
        # (because lock was held)
        assert mock_app.rescan.call_count == 0
