"""Unit tests for ProgressService.

This module contains comprehensive tests for the progress tracking service,
including progress lifecycle, broadcasting, error handling, and concurrency.
"""
import asyncio
from datetime import datetime
from unittest.mock import AsyncMock, MagicMock

import pytest

from src.server.services.progress_service import (
    ProgressService,
    ProgressServiceError,
    ProgressStatus,
    ProgressType,
    ProgressUpdate,
)


class TestProgressUpdate:
    """Test ProgressUpdate dataclass."""

    def test_progress_update_creation(self):
        """Test creating a progress update."""
        update = ProgressUpdate(
            id="test-1",
            type=ProgressType.DOWNLOAD,
            status=ProgressStatus.STARTED,
            title="Test Download",
            message="Starting download",
            total=100,
        )

        assert update.id == "test-1"
        assert update.type == ProgressType.DOWNLOAD
        assert update.status == ProgressStatus.STARTED
        assert update.title == "Test Download"
        assert update.message == "Starting download"
        assert update.total == 100
        assert update.current == 0
        assert update.percent == 0.0

    def test_progress_update_to_dict(self):
        """Test converting progress update to dictionary."""
        update = ProgressUpdate(
            id="test-1",
            type=ProgressType.SCAN,
            status=ProgressStatus.IN_PROGRESS,
            title="Test Scan",
            message="Scanning files",
            current=50,
            total=100,
            metadata={"test_key": "test_value"},
        )

        result = update.to_dict()

        assert result["id"] == "test-1"
        assert result["type"] == "scan"
        assert result["status"] == "in_progress"
        assert result["title"] == "Test Scan"
        assert result["message"] == "Scanning files"
        assert result["current"] == 50
        assert result["total"] == 100
        assert result["percent"] == 0.0
        assert result["metadata"]["test_key"] == "test_value"
        assert "started_at" in result
        assert "updated_at" in result


class TestProgressService:
    """Test ProgressService class."""

    @pytest.fixture
    def service(self):
        """Create a fresh ProgressService instance for each test."""
        return ProgressService()

    @pytest.fixture
    def mock_broadcast(self):
        """Create a mock broadcast callback."""
        return AsyncMock()

    @pytest.mark.asyncio
    async def test_start_progress(self, service):
        """Test starting a new progress operation."""
        update = await service.start_progress(
            progress_id="download-1",
            progress_type=ProgressType.DOWNLOAD,
            title="Downloading episode",
            total=1000,
            message="Starting...",
            metadata={"episode": "S01E01"},
        )

        assert update.id == "download-1"
        assert update.type == ProgressType.DOWNLOAD
        assert update.status == ProgressStatus.STARTED
        assert update.title == "Downloading episode"
        assert update.total == 1000
        assert update.message == "Starting..."
        assert update.metadata["episode"] == "S01E01"

    @pytest.mark.asyncio
    async def test_start_progress_duplicate_id(self, service):
        """Test starting progress with duplicate ID raises error."""
        await service.start_progress(
            progress_id="test-1",
            progress_type=ProgressType.DOWNLOAD,
            title="Test",
        )

        with pytest.raises(ProgressServiceError, match="already exists"):
            await service.start_progress(
                progress_id="test-1",
                progress_type=ProgressType.DOWNLOAD,
                title="Test Duplicate",
            )

    @pytest.mark.asyncio
    async def test_update_progress(self, service):
        """Test updating an existing progress operation."""
        await service.start_progress(
            progress_id="test-1",
            progress_type=ProgressType.DOWNLOAD,
            title="Test",
            total=100,
        )

        update = await service.update_progress(
            progress_id="test-1",
            current=50,
            message="Half way",
        )

        assert update.current == 50
        assert update.total == 100
        assert update.percent == 50.0
        assert update.message == "Half way"
        assert update.status == ProgressStatus.IN_PROGRESS

    @pytest.mark.asyncio
    async def test_update_progress_not_found(self, service):
        """Test updating non-existent progress raises error."""
        with pytest.raises(ProgressServiceError, match="not found"):
            await service.update_progress(
                progress_id="nonexistent",
                current=50,
            )

    @pytest.mark.asyncio
    async def test_update_progress_percentage_calculation(self, service):
        """Test progress percentage is calculated correctly."""
        await service.start_progress(
            progress_id="test-1",
            progress_type=ProgressType.DOWNLOAD,
            title="Test",
            total=200,
        )

        await service.update_progress(progress_id="test-1", current=50)
        update = await service.get_progress("test-1")
        assert update.percent == 25.0

        await service.update_progress(progress_id="test-1", current=100)
        update = await service.get_progress("test-1")
        assert update.percent == 50.0

        await service.update_progress(progress_id="test-1", current=200)
        update = await service.get_progress("test-1")
        assert update.percent == 100.0

    @pytest.mark.asyncio
    async def test_complete_progress(self, service):
        """Test completing a progress operation."""
        await service.start_progress(
            progress_id="test-1",
            progress_type=ProgressType.SCAN,
            title="Test Scan",
            total=100,
        )

        await service.update_progress(progress_id="test-1", current=50)

        update = await service.complete_progress(
            progress_id="test-1",
            message="Scan completed successfully",
            metadata={"items_found": 42},
        )

        assert update.status == ProgressStatus.COMPLETED
        assert update.percent == 100.0
        assert update.current == update.total
        assert update.message == "Scan completed successfully"
        assert update.metadata["items_found"] == 42

        # Should be moved to history
        active_progress = await service.get_all_active_progress()
        assert "test-1" not in active_progress

    @pytest.mark.asyncio
    async def test_fail_progress(self, service):
        """Test failing a progress operation."""
        await service.start_progress(
            progress_id="test-1",
            progress_type=ProgressType.DOWNLOAD,
            title="Test Download",
        )

        update = await service.fail_progress(
            progress_id="test-1",
            error_message="Network timeout",
            metadata={"retry_count": 3},
        )

        assert update.status == ProgressStatus.FAILED
        assert update.message == "Network timeout"
        assert update.metadata["retry_count"] == 3

        # Should be moved to history
        active_progress = await service.get_all_active_progress()
        assert "test-1" not in active_progress

    @pytest.mark.asyncio
    async def test_cancel_progress(self, service):
        """Test cancelling a progress operation."""
        await service.start_progress(
            progress_id="test-1",
            progress_type=ProgressType.DOWNLOAD,
            title="Test Download",
        )

        update = await service.cancel_progress(
            progress_id="test-1",
            message="Cancelled by user",
        )

        assert update.status == ProgressStatus.CANCELLED
        assert update.message == "Cancelled by user"

        # Should be moved to history
        active_progress = await service.get_all_active_progress()
        assert "test-1" not in active_progress

    @pytest.mark.asyncio
    async def test_get_progress(self, service):
        """Test retrieving progress by ID."""
        await service.start_progress(
            progress_id="test-1",
            progress_type=ProgressType.SCAN,
            title="Test",
        )

        progress = await service.get_progress("test-1")
        assert progress is not None
        assert progress.id == "test-1"

        # Test non-existent progress
        progress = await service.get_progress("nonexistent")
        assert progress is None

    @pytest.mark.asyncio
    async def test_get_all_active_progress(self, service):
        """Test retrieving all active progress operations."""
        await service.start_progress(
            progress_id="download-1",
            progress_type=ProgressType.DOWNLOAD,
            title="Download 1",
        )
        await service.start_progress(
            progress_id="download-2",
            progress_type=ProgressType.DOWNLOAD,
            title="Download 2",
        )
        await service.start_progress(
            progress_id="scan-1",
            progress_type=ProgressType.SCAN,
            title="Scan 1",
        )

        all_progress = await service.get_all_active_progress()
        assert len(all_progress) == 3
        assert "download-1" in all_progress
        assert "download-2" in all_progress
        assert "scan-1" in all_progress

    @pytest.mark.asyncio
    async def test_get_all_active_progress_filtered(self, service):
        """Test retrieving active progress filtered by type."""
        await service.start_progress(
            progress_id="download-1",
            progress_type=ProgressType.DOWNLOAD,
            title="Download 1",
        )
        await service.start_progress(
            progress_id="download-2",
            progress_type=ProgressType.DOWNLOAD,
            title="Download 2",
        )
        await service.start_progress(
            progress_id="scan-1",
            progress_type=ProgressType.SCAN,
            title="Scan 1",
        )

        download_progress = await service.get_all_active_progress(
            progress_type=ProgressType.DOWNLOAD
        )
        assert len(download_progress) == 2
        assert "download-1" in download_progress
        assert "download-2" in download_progress
        assert "scan-1" not in download_progress

    @pytest.mark.asyncio
    async def test_history_management(self, service):
        """Test progress history is maintained with size limit."""
        # Start and complete multiple progress operations
        for i in range(60):  # More than max_history_size (50)
            await service.start_progress(
                progress_id=f"test-{i}",
                progress_type=ProgressType.DOWNLOAD,
                title=f"Test {i}",
            )
            await service.complete_progress(
                progress_id=f"test-{i}",
                message="Completed",
            )

        # Check that oldest entries were removed
        history = service._history
        assert len(history) <= 50

        # Most recent should be in history
        recent_progress = await service.get_progress("test-59")
        assert recent_progress is not None

    @pytest.mark.asyncio
    async def test_broadcast_callback(self, service, mock_broadcast):
        """Test broadcast callback is invoked correctly."""
        # Subscribe to progress_updated events
        service.subscribe("progress_updated", mock_broadcast)

        await service.start_progress(
            progress_id="test-1",
            progress_type=ProgressType.DOWNLOAD,
            title="Test",
        )

        # Verify callback was called for start
        mock_broadcast.assert_called_once()
        # First positional arg is ProgressEvent
        call_args = mock_broadcast.call_args[0][0]
        assert call_args.event_type == "download_progress"
        assert call_args.room == "downloads"  # Room name for DOWNLOAD type
        assert call_args.progress_id == "test-1"
        assert call_args.progress.id == "test-1"

    @pytest.mark.asyncio
    async def test_broadcast_on_update(self, service, mock_broadcast):
        """Test broadcast on progress update."""
        # Subscribe to progress_updated events
        service.subscribe("progress_updated", mock_broadcast)

        await service.start_progress(
            progress_id="test-1",
            progress_type=ProgressType.DOWNLOAD,
            title="Test",
            total=100,
        )
        mock_broadcast.reset_mock()

        # Update with significant change (>1%)
        await service.update_progress(
            progress_id="test-1",
            current=50,
            force_broadcast=True,
        )

        # Should have been called
        assert mock_broadcast.call_count >= 1
        # First positional arg is ProgressEvent
        call_args = mock_broadcast.call_args[0][0]
        assert call_args.progress.current == 50

    @pytest.mark.asyncio
    async def test_broadcast_on_complete(self, service, mock_broadcast):
        """Test broadcast on progress completion."""
        # Subscribe to progress_updated events
        service.subscribe("progress_updated", mock_broadcast)

        await service.start_progress(
            progress_id="test-1",
            progress_type=ProgressType.SCAN,
            title="Test",
        )
        mock_broadcast.reset_mock()

        await service.complete_progress(
            progress_id="test-1",
            message="Done",
        )

        # Should have been called
        mock_broadcast.assert_called_once()
        # First positional arg is ProgressEvent
        call_args = mock_broadcast.call_args[0][0]
        assert call_args.progress.status.value == "completed"

    @pytest.mark.asyncio
    async def test_broadcast_on_failure(self, service, mock_broadcast):
        """Test broadcast on progress failure."""
        # Subscribe to progress_updated events
        service.subscribe("progress_updated", mock_broadcast)

        await service.start_progress(
            progress_id="test-1",
            progress_type=ProgressType.DOWNLOAD,
            title="Test",
        )
        mock_broadcast.reset_mock()

        await service.fail_progress(
            progress_id="test-1",
            error_message="Test error",
        )

        # Should have been called
        mock_broadcast.assert_called_once()
        # First positional arg is ProgressEvent
        call_args = mock_broadcast.call_args[0][0]
        assert call_args.progress.status.value == "failed"

    @pytest.mark.asyncio
    async def test_clear_history(self, service):
        """Test clearing progress history."""
        # Create and complete some progress
        for i in range(5):
            await service.start_progress(
                progress_id=f"test-{i}",
                progress_type=ProgressType.DOWNLOAD,
                title=f"Test {i}",
            )
            await service.complete_progress(
                progress_id=f"test-{i}",
                message="Done",
            )

        # History should not be empty
        assert len(service._history) > 0

        # Clear history
        await service.clear_history()

        # History should now be empty
        assert len(service._history) == 0

    @pytest.mark.asyncio
    async def test_concurrent_progress_operations(self, service):
        """Test handling multiple concurrent progress operations."""
        
        async def create_and_complete_progress(id_num: int):
            """Helper to create and complete a progress."""
            await service.start_progress(
                progress_id=f"test-{id_num}",
                progress_type=ProgressType.DOWNLOAD,
                title=f"Test {id_num}",
                total=100,
            )
            for i in range(0, 101, 10):
                await service.update_progress(
                    progress_id=f"test-{id_num}",
                    current=i,
                )
                await asyncio.sleep(0.01)
            await service.complete_progress(
                progress_id=f"test-{id_num}",
                message="Done",
            )

        # Run multiple concurrent operations
        tasks = [create_and_complete_progress(i) for i in range(10)]
        await asyncio.gather(*tasks)

        # All should be in history
        for i in range(10):
            progress = await service.get_progress(f"test-{i}")
            assert progress is not None
            assert progress.status == ProgressStatus.COMPLETED

    @pytest.mark.asyncio
    async def test_update_with_metadata(self, service):
        """Test updating progress with metadata."""
        await service.start_progress(
            progress_id="test-1",
            progress_type=ProgressType.DOWNLOAD,
            title="Test",
            metadata={"initial": "value"},
        )

        await service.update_progress(
            progress_id="test-1",
            current=50,
            metadata={"additional": "data", "speed": 1.5},
        )

        progress = await service.get_progress("test-1")
        assert progress.metadata["initial"] == "value"
        assert progress.metadata["additional"] == "data"
        assert progress.metadata["speed"] == 1.5

    @pytest.mark.asyncio
    async def test_progress_with_key_and_folder(self, service):
        """Test progress tracking with series key and folder."""
        # Start progress with key and folder
        update = await service.start_progress(
            progress_id="download-series-1",
            progress_type=ProgressType.DOWNLOAD,
            title="Downloading Attack on Titan",
            key="attack-on-titan",
            folder="Attack on Titan (2013)",
            total=100,
        )

        assert update.key == "attack-on-titan"
        assert update.folder == "Attack on Titan (2013)"

        # Verify to_dict includes key and folder
        dict_repr = update.to_dict()
        assert dict_repr["key"] == "attack-on-titan"
        assert dict_repr["folder"] == "Attack on Titan (2013)"

        # Update progress and verify key/folder are preserved
        updated = await service.update_progress(
            progress_id="download-series-1",
            current=50,
        )

        assert updated.key == "attack-on-titan"
        assert updated.folder == "Attack on Titan (2013)"

    @pytest.mark.asyncio
    async def test_progress_update_key_and_folder(self, service):
        """Test updating key and folder in existing progress."""
        # Start without key/folder
        await service.start_progress(
            progress_id="test-1",
            progress_type=ProgressType.SCAN,
            title="Test Scan",
        )

        # Update with key and folder
        updated = await service.update_progress(
            progress_id="test-1",
            key="one-piece",
            folder="One Piece (1999)",
            current=10,
        )

        assert updated.key == "one-piece"
        assert updated.folder == "One Piece (1999)"

        # Verify to_dict includes the fields
        dict_repr = updated.to_dict()
        assert dict_repr["key"] == "one-piece"
        assert dict_repr["folder"] == "One Piece (1999)"

    def test_progress_update_to_dict_without_key_folder(self):
        """Test to_dict doesn't include key/folder if not set."""
        update = ProgressUpdate(
            id="test-1",
            type=ProgressType.SYSTEM,
            status=ProgressStatus.STARTED,
            title="System Task",
        )

        result = update.to_dict()

        # key and folder should not be in dict if not set
        assert "key" not in result
        assert "folder" not in result

    def test_progress_update_creation_with_key_folder(self):
        """Test creating progress update with key and folder."""
        update = ProgressUpdate(
            id="test-1",
            type=ProgressType.DOWNLOAD,
            status=ProgressStatus.STARTED,
            title="Test Download",
            key="naruto",
            folder="Naruto (2002)",
            total=100,
        )

        assert update.key == "naruto"
        assert update.folder == "Naruto (2002)"

        # Verify to_dict includes them
        result = update.to_dict()
        assert result["key"] == "naruto"
        assert result["folder"] == "Naruto (2002)"
