"""Unit tests for ScanService.

This module contains comprehensive tests for the scan service,
including scan lifecycle, progress events, and key-based identification.
"""
from datetime import datetime
from unittest.mock import AsyncMock, MagicMock, Mock

import pytest

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


class TestScanProgress:
    """Test ScanProgress class."""

    def test_scan_progress_creation(self):
        """Test creating a scan progress instance."""
        progress = ScanProgress("scan-123")

        assert progress.scan_id == "scan-123"
        assert progress.status == "started"
        assert progress.current == 0
        assert progress.total == 0
        assert progress.percentage == 0.0
        assert progress.message == "Initializing scan..."
        assert progress.key is None
        assert progress.folder is None
        assert progress.series_found == 0
        assert progress.errors == []
        assert isinstance(progress.started_at, datetime)
        assert isinstance(progress.updated_at, datetime)

    def test_scan_progress_to_dict_basic(self):
        """Test converting scan progress to dictionary without key/folder."""
        progress = ScanProgress("scan-123")
        progress.current = 5
        progress.total = 10
        progress.percentage = 50.0
        progress.status = "in_progress"
        progress.message = "Scanning..."

        result = progress.to_dict()

        assert result["scan_id"] == "scan-123"
        assert result["status"] == "in_progress"
        assert result["current"] == 5
        assert result["total"] == 10
        assert result["percentage"] == 50.0
        assert result["message"] == "Scanning..."
        assert result["series_found"] == 0
        assert result["errors"] == []
        assert "started_at" in result
        assert "updated_at" in result
        # key and folder should not be present when None
        assert "key" not in result
        assert "folder" not in result

    def test_scan_progress_to_dict_with_key_and_folder(self):
        """Test converting scan progress to dictionary with key and folder."""
        progress = ScanProgress("scan-123")
        progress.key = "attack-on-titan"
        progress.folder = "Attack on Titan (2013)"
        progress.series_found = 5

        result = progress.to_dict()

        assert result["key"] == "attack-on-titan"
        assert result["folder"] == "Attack on Titan (2013)"
        assert result["series_found"] == 5

    def test_scan_progress_to_dict_with_errors(self):
        """Test scan progress with error messages."""
        progress = ScanProgress("scan-123")
        progress.errors = ["Error 1", "Error 2"]

        result = progress.to_dict()

        assert result["errors"] == ["Error 1", "Error 2"]


class TestScanService:
    """Test ScanService class."""

    @pytest.fixture
    def mock_progress_service(self):
        """Create a mock progress service."""
        service = MagicMock()
        service.start_progress = AsyncMock()
        service.update_progress = AsyncMock()
        service.complete_progress = AsyncMock()
        service.fail_progress = AsyncMock()
        return service

    @pytest.fixture
    def service(self, mock_progress_service):
        """Create a ScanService instance for each test."""
        return ScanService(progress_service=mock_progress_service)

    @pytest.mark.asyncio
    async def test_service_initialization(self, service):
        """Test ScanService initialization."""
        assert service.is_scanning is False
        assert service.current_scan is None
        assert service._scan_history == []

    @pytest.mark.asyncio
    async def test_start_scan(self, service):
        """Test starting a new scan."""
        scanner_factory = MagicMock()
        
        scan_id = await service.start_scan(scanner_factory)

        assert scan_id is not None
        assert len(scan_id) > 0
        assert service.is_scanning is True
        assert service.current_scan is not None
        assert service.current_scan.scan_id == scan_id

    @pytest.mark.asyncio
    async def test_start_scan_while_scanning(self, service):
        """Test starting scan while another is in progress raises error."""
        scanner_factory = MagicMock()
        
        await service.start_scan(scanner_factory)

        with pytest.raises(ScanServiceError, match="already in progress"):
            await service.start_scan(scanner_factory)

    @pytest.mark.asyncio
    async def test_cancel_scan(self, service):
        """Test cancelling a scan in progress."""
        scanner_factory = MagicMock()
        scan_id = await service.start_scan(scanner_factory)

        result = await service.cancel_scan()

        assert result is True
        assert service.is_scanning is False
        assert service.current_scan.status == "cancelled"
        assert len(service._scan_history) == 1

    @pytest.mark.asyncio
    async def test_cancel_scan_no_scan_in_progress(self, service):
        """Test cancelling when no scan is in progress."""
        result = await service.cancel_scan()

        assert result is False

    @pytest.mark.asyncio
    async def test_get_scan_status(self, service):
        """Test getting scan status."""
        status = await service.get_scan_status()

        assert status["is_scanning"] is False
        assert status["current_scan"] is None

    @pytest.mark.asyncio
    async def test_get_scan_status_while_scanning(self, service):
        """Test getting scan status while scanning."""
        scanner_factory = MagicMock()
        scan_id = await service.start_scan(scanner_factory)

        status = await service.get_scan_status()

        assert status["is_scanning"] is True
        assert status["current_scan"] is not None
        assert status["current_scan"]["scan_id"] == scan_id

    @pytest.mark.asyncio
    async def test_get_scan_history_empty(self, service):
        """Test getting scan history when empty."""
        history = await service.get_scan_history()

        assert history == []

    @pytest.mark.asyncio
    async def test_get_scan_history_with_entries(self, service):
        """Test getting scan history with entries."""
        # Start and cancel multiple scans to populate history
        scanner_factory = MagicMock()
        
        await service.start_scan(scanner_factory)
        await service.cancel_scan()
        
        await service.start_scan(scanner_factory)
        await service.cancel_scan()

        history = await service.get_scan_history()

        assert len(history) == 2
        # Should be newest first
        assert history[0]["status"] == "cancelled"

    @pytest.mark.asyncio
    async def test_get_scan_history_limit(self, service):
        """Test scan history respects limit."""
        scanner_factory = MagicMock()
        
        # Create 3 history entries
        for _ in range(3):
            await service.start_scan(scanner_factory)
            await service.cancel_scan()

        history = await service.get_scan_history(limit=2)

        assert len(history) == 2

    @pytest.mark.asyncio
    async def test_subscribe_to_scan_events(self, service):
        """Test subscribing to scan events."""
        handler = MagicMock()
        
        service.subscribe_to_scan_events(handler)

        assert handler in service._scan_event_handlers

    @pytest.mark.asyncio
    async def test_unsubscribe_from_scan_events(self, service):
        """Test unsubscribing from scan events."""
        handler = MagicMock()
        service.subscribe_to_scan_events(handler)
        
        service.unsubscribe_from_scan_events(handler)

        assert handler not in service._scan_event_handlers

    @pytest.mark.asyncio
    async def test_emit_scan_event(self, service):
        """Test emitting scan events to handlers."""
        handler = AsyncMock()
        service.subscribe_to_scan_events(handler)

        await service._emit_scan_event({
            "type": "scan_progress",
            "key": "test-series",
            "folder": "Test Folder",
        })

        handler.assert_called_once_with({
            "type": "scan_progress",
            "key": "test-series",
            "folder": "Test Folder",
        })

    @pytest.mark.asyncio
    async def test_emit_scan_event_sync_handler(self, service):
        """Test emitting scan events to sync handlers."""
        handler = MagicMock()
        service.subscribe_to_scan_events(handler)

        await service._emit_scan_event({
            "type": "scan_progress",
            "data": {"key": "test-series"},
        })

        handler.assert_called_once()

    @pytest.mark.asyncio
    async def test_handle_progress_update(
        self, service, mock_progress_service
    ):
        """Test handling progress update."""
        scanner_factory = MagicMock()
        await service.start_scan(scanner_factory)
        
        scan_progress = service.current_scan
        scan_progress.current = 5
        scan_progress.total = 10
        scan_progress.percentage = 50.0
        scan_progress.message = "Processing..."
        scan_progress.key = "test-series"
        scan_progress.folder = "Test Folder"

        await service._handle_progress_update(scan_progress)

        mock_progress_service.update_progress.assert_called_once()
        call_kwargs = mock_progress_service.update_progress.call_args.kwargs
        assert call_kwargs["progress_id"] == f"scan_{scan_progress.scan_id}"
        assert call_kwargs["current"] == 5
        assert call_kwargs["total"] == 10
        assert call_kwargs["message"] == "Processing..."

    @pytest.mark.asyncio
    async def test_handle_scan_error(self, service):
        """Test handling scan error."""
        handler = AsyncMock()
        service.subscribe_to_scan_events(handler)
        
        scanner_factory = MagicMock()
        await service.start_scan(scanner_factory)
        
        scan_progress = service.current_scan
        error_data = {
            "error": ValueError("Test error"),
            "message": "Test error message",
            "recoverable": True,
        }

        await service._handle_scan_error(scan_progress, error_data)

        # Handler is called twice: once for start, once for error
        assert handler.call_count == 2
        # Get the error event (second call)
        error_event = handler.call_args_list[1][0][0]
        assert error_event["type"] == "scan_error"
        assert error_event["message"] == "Test error message"
        assert error_event["recoverable"] is True

    @pytest.mark.asyncio
    async def test_handle_scan_completion_success(
        self, service, mock_progress_service
    ):
        """Test handling successful scan completion."""
        handler = AsyncMock()
        service.subscribe_to_scan_events(handler)
        
        scanner_factory = MagicMock()
        scan_id = await service.start_scan(scanner_factory)
        
        scan_progress = service.current_scan
        completion_data = {
            "success": True,
            "message": "Scan completed",
            "statistics": {"series_found": 5, "total_folders": 10},
        }

        await service._handle_scan_completion(
            scan_progress, completion_data
        )

        assert service.is_scanning is False
        assert len(service._scan_history) == 1
        mock_progress_service.complete_progress.assert_called_once()
        # Handler is called twice: once for start, once for completion
        assert handler.call_count == 2
        # Get the completion event (second call)
        completion_event = handler.call_args_list[1][0][0]
        assert completion_event["type"] == "scan_completed"
        assert completion_event["success"] is True

    @pytest.mark.asyncio
    async def test_handle_scan_completion_failure(
        self, service, mock_progress_service
    ):
        """Test handling failed scan completion."""
        handler = AsyncMock()
        service.subscribe_to_scan_events(handler)
        
        scanner_factory = MagicMock()
        scan_id = await service.start_scan(scanner_factory)
        
        scan_progress = service.current_scan
        completion_data = {
            "success": False,
            "message": "Scan failed: critical error",
        }

        await service._handle_scan_completion(
            scan_progress, completion_data
        )

        assert service.is_scanning is False
        mock_progress_service.fail_progress.assert_called_once()
        # Handler is called twice: once for start, once for failure
        assert handler.call_count == 2
        # Get the failure event (second call)
        failure_event = handler.call_args_list[1][0][0]
        assert failure_event["type"] == "scan_failed"
        assert failure_event["success"] is False


class TestScanServiceSingleton:
    """Test ScanService singleton functions."""

    def test_get_scan_service_returns_singleton(self):
        """Test that get_scan_service returns a singleton."""
        reset_scan_service()
        
        service1 = get_scan_service()
        service2 = get_scan_service()

        assert service1 is service2

    def test_reset_scan_service(self):
        """Test that reset_scan_service clears the singleton."""
        reset_scan_service()
        service1 = get_scan_service()
        
        reset_scan_service()
        service2 = get_scan_service()

        assert service1 is not service2


class TestScanServiceKeyIdentification:
    """Test that ScanService uses key as primary identifier."""

    @pytest.fixture
    def mock_progress_service(self):
        """Create a mock progress service."""
        service = MagicMock()
        service.start_progress = AsyncMock()
        service.update_progress = AsyncMock()
        service.complete_progress = AsyncMock()
        service.fail_progress = AsyncMock()
        return service

    @pytest.fixture
    def service(self, mock_progress_service):
        """Create a ScanService instance."""
        return ScanService(progress_service=mock_progress_service)

    @pytest.mark.asyncio
    async def test_progress_update_includes_key(
        self, service, mock_progress_service
    ):
        """Test that progress updates include key via scan event."""
        events = []

        async def capture(event):
            events.append(event)

        service.subscribe_to_scan_events(capture)

        scanner_factory = MagicMock()
        await service.start_scan(scanner_factory)

        scan_progress = service.current_scan
        scan_progress.key = "attack-on-titan"
        scan_progress.folder = "Attack on Titan (2013)"

        await service._handle_progress_update(scan_progress)

        # First event is scan_started, second is the progress update
        progress_event = events[-1]
        assert progress_event["type"] == "scan_progress"
        data = progress_event["data"]
        assert data["key"] == "attack-on-titan"
        assert data["folder"] == "Attack on Titan (2013)"

    @pytest.mark.asyncio
    async def test_scan_event_includes_key(self, service):
        """Test that scan events include key as primary identifier."""
        events_received = []
        
        async def capture_event(event):
            events_received.append(event)
        
        service.subscribe_to_scan_events(capture_event)

        await service._emit_scan_event({
            "type": "scan_progress",
            "key": "my-hero-academia",
            "folder": "My Hero Academia (2016)",
            "data": {"status": "in_progress"},
        })

        assert len(events_received) == 1
        assert events_received[0]["key"] == "my-hero-academia"
        assert events_received[0]["folder"] == "My Hero Academia (2016)"

    @pytest.mark.asyncio
    async def test_error_event_includes_key(self, service):
        """Test that error events include key as primary identifier."""
        events_received = []

        async def capture_event(event):
            events_received.append(event)

        service.subscribe_to_scan_events(capture_event)

        scanner_factory = MagicMock()
        await service.start_scan(scanner_factory)

        scan_progress = service.current_scan
        error_data = {
            "error": ValueError("Test"),
            "message": "Error message",
            "recoverable": True,
        }

        await service._handle_scan_error(scan_progress, error_data)

        assert len(events_received) == 2  # Started + error
        error_event = events_received[1]
        assert error_event["type"] == "scan_error"
        assert error_event["message"] == "Error message"

    @pytest.mark.asyncio
    async def test_scan_status_includes_key(self, service):
        """Test that scan status includes key in current scan."""
        scanner_factory = MagicMock()
        await service.start_scan(scanner_factory)
        
        service.current_scan.key = "one-piece"
        service.current_scan.folder = "One Piece (1999)"

        status = await service.get_scan_status()

        assert status["current_scan"]["key"] == "one-piece"
        assert status["current_scan"]["folder"] == "One Piece (1999)"

    @pytest.mark.asyncio
    async def test_scan_history_includes_key(self, service):
        """Test that scan history includes key in entries."""
        scanner_factory = MagicMock()
        
        await service.start_scan(scanner_factory)
        service.current_scan.key = "naruto"
        service.current_scan.folder = "Naruto (2002)"
        await service.cancel_scan()

        history = await service.get_scan_history()

        assert len(history) == 1
        assert history[0]["key"] == "naruto"
        assert history[0]["folder"] == "Naruto (2002)"
