"""Unit tests for download queue Pydantic models.

This module tests all download-related models including validation,
serialization, and field constraints.
"""
from datetime import datetime, timedelta, timezone

import pytest
from pydantic import ValidationError

from src.server.models.download import (
    DownloadItem,
    DownloadPriority,
    DownloadProgress,
    DownloadRequest,
    DownloadResponse,
    DownloadStatus,
    EpisodeIdentifier,
    QueueOperationRequest,
    QueueReorderRequest,
    QueueStats,
    QueueStatus,
    QueueStatusResponse,
)


class TestDownloadStatus:
    """Test DownloadStatus enum."""

    def test_all_statuses_exist(self):
        """Test that all expected statuses are defined."""
        assert DownloadStatus.PENDING == "pending"
        assert DownloadStatus.DOWNLOADING == "downloading"
        assert DownloadStatus.PAUSED == "paused"
        assert DownloadStatus.COMPLETED == "completed"
        assert DownloadStatus.FAILED == "failed"
        assert DownloadStatus.CANCELLED == "cancelled"

    def test_status_values(self):
        """Test that status values are lowercase strings."""
        for status in DownloadStatus:
            assert isinstance(status.value, str)
            assert status.value.islower()


class TestDownloadPriority:
    """Test DownloadPriority enum."""

    def test_all_priorities_exist(self):
        """Test that all expected priorities are defined."""
        assert DownloadPriority.LOW == "LOW"
        assert DownloadPriority.NORMAL == "NORMAL"
        assert DownloadPriority.HIGH == "HIGH"

    def test_priority_values(self):
        """Test that priority values are uppercase strings."""
        for priority in DownloadPriority:
            assert isinstance(priority.value, str)
            assert priority.value.isupper()


class TestEpisodeIdentifier:
    """Test EpisodeIdentifier model."""

    def test_valid_episode_identifier(self):
        """Test creating a valid episode identifier."""
        episode = EpisodeIdentifier(
            season=1,
            episode=5,
            title="Test Episode"
        )
        assert episode.season == 1
        assert episode.episode == 5
        assert episode.title == "Test Episode"

    def test_episode_identifier_without_title(self):
        """Test creating episode identifier without title."""
        episode = EpisodeIdentifier(season=2, episode=10)
        assert episode.season == 2
        assert episode.episode == 10
        assert episode.title is None

    def test_invalid_season_number(self):
        """Test that season must be positive."""
        with pytest.raises(ValidationError) as exc_info:
            EpisodeIdentifier(season=0, episode=1)
        errors = exc_info.value.errors()
        assert any("season" in str(e["loc"]) for e in errors)

    def test_invalid_episode_number(self):
        """Test that episode must be positive."""
        with pytest.raises(ValidationError) as exc_info:
            EpisodeIdentifier(season=1, episode=0)
        errors = exc_info.value.errors()
        assert any("episode" in str(e["loc"]) for e in errors)

    def test_negative_season_rejected(self):
        """Test that negative season is rejected."""
        with pytest.raises(ValidationError):
            EpisodeIdentifier(season=-1, episode=1)

    def test_negative_episode_rejected(self):
        """Test that negative episode is rejected."""
        with pytest.raises(ValidationError):
            EpisodeIdentifier(season=1, episode=-1)


class TestDownloadProgress:
    """Test DownloadProgress model."""

    def test_valid_progress(self):
        """Test creating valid progress information."""
        progress = DownloadProgress(
            percent=45.5,
            downloaded_mb=100.0,
            total_mb=220.0,
            speed_mbps=5.5,
            eta_seconds=120
        )
        assert progress.percent == 45.5
        assert progress.downloaded_mb == 100.0
        assert progress.total_mb == 220.0
        assert progress.speed_mbps == 5.5
        assert progress.eta_seconds == 120

    def test_progress_defaults(self):
        """Test default values for progress."""
        progress = DownloadProgress()
        assert progress.percent == 0.0
        assert progress.downloaded_mb == 0.0
        assert progress.total_mb is None
        assert progress.speed_mbps is None
        assert progress.eta_seconds is None

    def test_percent_range_validation(self):
        """Test that percent must be between 0 and 100."""
        # Valid boundary values
        DownloadProgress(percent=0.0)
        DownloadProgress(percent=100.0)

        # Invalid values
        with pytest.raises(ValidationError):
            DownloadProgress(percent=-0.1)
        with pytest.raises(ValidationError):
            DownloadProgress(percent=100.1)

    def test_negative_downloaded_mb_rejected(self):
        """Test that negative downloaded_mb is rejected."""
        with pytest.raises(ValidationError):
            DownloadProgress(downloaded_mb=-1.0)

    def test_negative_total_mb_rejected(self):
        """Test that negative total_mb is rejected."""
        with pytest.raises(ValidationError):
            DownloadProgress(total_mb=-1.0)

    def test_negative_speed_rejected(self):
        """Test that negative speed is rejected."""
        with pytest.raises(ValidationError):
            DownloadProgress(speed_mbps=-1.0)

    def test_negative_eta_rejected(self):
        """Test that negative ETA is rejected."""
        with pytest.raises(ValidationError):
            DownloadProgress(eta_seconds=-1)


class TestDownloadItem:
    """Test DownloadItem model."""

    def test_valid_download_item(self):
        """Test creating a valid download item."""
        episode = EpisodeIdentifier(season=1, episode=5)
        # Note: serie_id uses provider key format (URL-safe, lowercase)
        item = DownloadItem(
            id="download_123",
            serie_id="test-serie-key",
            serie_folder="Test Series (2023)",
            serie_name="Test Series",
            episode=episode,
            status=DownloadStatus.PENDING,
            priority=DownloadPriority.HIGH
        )
        assert item.id == "download_123"
        assert item.serie_id == "test-serie-key"
        assert item.serie_name == "Test Series"
        assert item.episode == episode
        assert item.status == DownloadStatus.PENDING
        assert item.priority == DownloadPriority.HIGH

    def test_serie_id_normalized_to_lowercase(self):
        """Test that serie_id (key) is normalized to lowercase."""
        episode = EpisodeIdentifier(season=1, episode=1)
        item = DownloadItem(
            id="test_id",
            serie_id="ATTACK-ON-TITAN",
            serie_folder="Test Folder",
            serie_name="Test",
            episode=episode
        )
        assert item.serie_id == "attack-on-titan"

    def test_serie_id_whitespace_stripped(self):
        """Test that serie_id whitespace is stripped."""
        episode = EpisodeIdentifier(season=1, episode=1)
        item = DownloadItem(
            id="test_id",
            serie_id="  attack-on-titan  ",
            serie_folder="Test Folder",
            serie_name="Test",
            episode=episode
        )
        assert item.serie_id == "attack-on-titan"

    def test_download_item_defaults(self):
        """Test default values for download item."""
        episode = EpisodeIdentifier(season=1, episode=1)
        # Note: serie_id uses provider key format (URL-safe, lowercase)
        item = DownloadItem(
            id="test_id",
            serie_id="default-test-key",
            serie_folder="Test Folder (2024)",
            serie_name="Test",
            episode=episode
        )
        assert item.status == DownloadStatus.PENDING
        assert item.priority == DownloadPriority.NORMAL
        assert item.started_at is None
        assert item.completed_at is None
        assert item.progress is None
        assert item.error is None
        assert item.retry_count == 0
        assert item.source_url is None

    def test_download_item_with_progress(self):
        """Test download item with progress information."""
        episode = EpisodeIdentifier(season=1, episode=1)
        progress = DownloadProgress(percent=50.0, downloaded_mb=100.0)
        # Note: serie_id uses provider key format (URL-safe, lowercase)
        item = DownloadItem(
            id="test_id",
            serie_id="progress-test-key",
            serie_folder="Test Folder (2024)",
            serie_name="Test",
            episode=episode,
            progress=progress
        )
        assert item.progress is not None
        assert item.progress.percent == 50.0

    def test_download_item_with_timestamps(self):
        """Test download item with timestamp fields."""
        episode = EpisodeIdentifier(season=1, episode=1)
        now = datetime.now(timezone.utc)
        # Note: serie_id uses provider key format (URL-safe, lowercase)
        item = DownloadItem(
            id="test_id",
            serie_id="timestamp-test-key",
            serie_folder="Test Folder (2024)",
            serie_name="Test",
            episode=episode,
            started_at=now,
            completed_at=now + timedelta(minutes=5)
        )
        assert item.started_at == now
        assert item.completed_at == now + timedelta(minutes=5)

    def test_empty_serie_name_rejected(self):
        """Test that empty serie name is rejected."""
        episode = EpisodeIdentifier(season=1, episode=1)
        with pytest.raises(ValidationError):
            DownloadItem(
                id="test_id",
                serie_id="empty-name-test-key",
                serie_folder="Test Folder (2024)",
                serie_name="",
                episode=episode
            )

    def test_negative_retry_count_rejected(self):
        """Test that negative retry count is rejected."""
        episode = EpisodeIdentifier(season=1, episode=1)
        with pytest.raises(ValidationError):
            DownloadItem(
                id="test_id",
                serie_id="retry-test-key",
                serie_folder="Test Folder (2024)",
                serie_name="Test",
                episode=episode,
                retry_count=-1
            )

    def test_added_at_auto_generated(self):
        """Test that added_at is automatically set."""
        episode = EpisodeIdentifier(season=1, episode=1)
        before = datetime.now(timezone.utc)
        # Note: serie_id uses provider key format (URL-safe, lowercase)
        item = DownloadItem(
            id="test_id",
            serie_id="auto-added-test-key",
            serie_folder="Test Folder (2024)",
            serie_name="Test",
            episode=episode
        )
        after = datetime.now(timezone.utc)
        assert before <= item.added_at <= after


class TestQueueStatus:
    """Test QueueStatus model."""

    def test_valid_queue_status(self):
        """Test creating valid queue status."""
        episode = EpisodeIdentifier(season=1, episode=1)
        # Note: serie_id uses provider key format (URL-safe, lowercase)
        item = DownloadItem(
            id="test_id",
            serie_id="queue-status-test-key",
            serie_folder="Test Folder (2024)",
            serie_name="Test",
            episode=episode
        )
        status = QueueStatus(
            is_running=True,
            is_paused=False,
            active_downloads=[item],
            pending_queue=[item],
            completed_downloads=[],
            failed_downloads=[]
        )
        assert status.is_running is True
        assert status.is_paused is False
        assert len(status.active_downloads) == 1
        assert len(status.pending_queue) == 1

    def test_queue_status_defaults(self):
        """Test default values for queue status."""
        status = QueueStatus()
        assert status.is_running is False
        assert status.is_paused is False
        assert status.active_downloads == []
        assert status.pending_queue == []
        assert status.completed_downloads == []
        assert status.failed_downloads == []


class TestQueueStats:
    """Test QueueStats model."""

    def test_valid_queue_stats(self):
        """Test creating valid queue statistics."""
        stats = QueueStats(
            total_items=10,
            pending_count=3,
            active_count=2,
            completed_count=4,
            failed_count=1,
            total_downloaded_mb=500.5,
            average_speed_mbps=5.0,
            estimated_time_remaining=120
        )
        assert stats.total_items == 10
        assert stats.pending_count == 3
        assert stats.active_count == 2
        assert stats.completed_count == 4
        assert stats.failed_count == 1
        assert stats.total_downloaded_mb == 500.5
        assert stats.average_speed_mbps == 5.0
        assert stats.estimated_time_remaining == 120

    def test_queue_stats_defaults(self):
        """Test default values for queue stats."""
        stats = QueueStats()
        assert stats.total_items == 0
        assert stats.pending_count == 0
        assert stats.active_count == 0
        assert stats.completed_count == 0
        assert stats.failed_count == 0
        assert stats.total_downloaded_mb == 0.0
        assert stats.average_speed_mbps is None
        assert stats.estimated_time_remaining is None

    def test_negative_counts_rejected(self):
        """Test that negative counts are rejected."""
        with pytest.raises(ValidationError):
            QueueStats(total_items=-1)
        with pytest.raises(ValidationError):
            QueueStats(pending_count=-1)
        with pytest.raises(ValidationError):
            QueueStats(active_count=-1)
        with pytest.raises(ValidationError):
            QueueStats(completed_count=-1)
        with pytest.raises(ValidationError):
            QueueStats(failed_count=-1)

    def test_negative_speed_rejected(self):
        """Test that negative speed is rejected."""
        with pytest.raises(ValidationError):
            QueueStats(average_speed_mbps=-1.0)

    def test_negative_eta_rejected(self):
        """Test that negative ETA is rejected."""
        with pytest.raises(ValidationError):
            QueueStats(estimated_time_remaining=-1)


class TestDownloadRequest:
    """Test DownloadRequest model."""

    def test_valid_download_request(self):
        """Test creating a valid download request."""
        episode1 = EpisodeIdentifier(season=1, episode=1)
        episode2 = EpisodeIdentifier(season=1, episode=2)
        # Note: serie_id uses provider key format (URL-safe, lowercase)
        request = DownloadRequest(
            serie_id="test-series-key",
            serie_folder="Test Series (2023)",
            serie_name="Test Series",
            episodes=[episode1, episode2],
            priority=DownloadPriority.HIGH
        )
        assert request.serie_id == "test-series-key"
        assert request.serie_name == "Test Series"
        assert len(request.episodes) == 2
        assert request.priority == DownloadPriority.HIGH

    def test_serie_id_normalized_to_lowercase(self):
        """Test that serie_id (key) is normalized to lowercase."""
        episode = EpisodeIdentifier(season=1, episode=1)
        request = DownloadRequest(
            serie_id="ATTACK-ON-TITAN",
            serie_folder="Test Series (2023)",
            serie_name="Test Series",
            episodes=[episode]
        )
        assert request.serie_id == "attack-on-titan"

    def test_serie_id_whitespace_stripped(self):
        """Test that serie_id whitespace is stripped."""
        episode = EpisodeIdentifier(season=1, episode=1)
        request = DownloadRequest(
            serie_id="  attack-on-titan  ",
            serie_folder="Test Series (2023)",
            serie_name="Test Series",
            episodes=[episode]
        )
        assert request.serie_id == "attack-on-titan"

    def test_download_request_default_priority(self):
        """Test default priority for download request."""
        episode = EpisodeIdentifier(season=1, episode=1)
        # Note: serie_id uses provider key format (URL-safe, lowercase)
        request = DownloadRequest(
            serie_id="default-priority-test-key",
            serie_folder="Test Series (2023)",
            serie_name="Test Series",
            episodes=[episode]
        )
        assert request.priority == DownloadPriority.NORMAL

    def test_empty_episodes_list_allowed(self):
        """Test that empty episodes list is allowed at model level.
        
        (endpoint validates)
        """
        # Empty list is now allowed at model level; endpoint validates
        # Note: serie_id uses provider key format (URL-safe, lowercase)
        request = DownloadRequest(
            serie_id="empty-episodes-test-key",
            serie_folder="Test Series (2023)",
            serie_name="Test Series",
            episodes=[]
        )
        assert request.episodes == []

    def test_empty_serie_name_rejected(self):
        """Test that empty serie name is rejected."""
        episode = EpisodeIdentifier(season=1, episode=1)
        with pytest.raises(ValidationError):
            # Note: serie_id uses provider key format (URL-safe, lowercase)
            DownloadRequest(
                serie_id="empty-name-request-key",
                serie_folder="Test Series (2023)",
                serie_name="",
                episodes=[episode]
            )


class TestDownloadResponse:
    """Test DownloadResponse model."""

    def test_valid_download_response(self):
        """Test creating a valid download response."""
        response = DownloadResponse(
            status="success",
            message="Added 2 episodes to queue",
            added_items=["item1", "item2"],
            failed_items=[]
        )
        assert response.status == "success"
        assert response.message == "Added 2 episodes to queue"
        assert len(response.added_items) == 2
        assert response.failed_items == []

    def test_download_response_defaults(self):
        """Test default values for download response."""
        response = DownloadResponse(
            status="success",
            message="Test message"
        )
        assert response.added_items == []
        assert response.failed_items == []


class TestQueueOperationRequest:
    """Test QueueOperationRequest model."""

    def test_valid_operation_request(self):
        """Test creating a valid operation request."""
        request = QueueOperationRequest(
            item_ids=["item1", "item2", "item3"]
        )
        assert len(request.item_ids) == 3
        assert "item1" in request.item_ids

    def test_empty_item_ids_allowed(self):
        """Test that empty item_ids list is allowed at model level.
        
        (endpoint validates)
        """
        # Empty list is now allowed at model level; endpoint validates
        request = QueueOperationRequest(item_ids=[])
        assert request.item_ids == []


class TestQueueReorderRequest:
    """Test QueueReorderRequest model."""

    def test_valid_reorder_request(self):
        """Test creating a valid reorder request."""
        request = QueueReorderRequest(
            item_id="item_123",
            new_position=5
        )
        assert request.item_id == "item_123"
        assert request.new_position == 5

    def test_zero_position_allowed(self):
        """Test that position zero is allowed."""
        request = QueueReorderRequest(
            item_id="item_123",
            new_position=0
        )
        assert request.new_position == 0

    def test_negative_position_rejected(self):
        """Test that negative position is rejected."""
        with pytest.raises(ValidationError):
            QueueReorderRequest(
                item_id="item_123",
                new_position=-1
            )


class TestQueueStatusResponse:
    """Test QueueStatusResponse model."""

    def test_valid_status_response(self):
        """Test creating a valid status response."""
        status = QueueStatus()
        stats = QueueStats()
        response = QueueStatusResponse(
            status=status,
            statistics=stats
        )
        assert response.status is not None
        assert response.statistics is not None


class TestModelSerialization:
    """Test model serialization and deserialization."""

    def test_download_item_to_dict(self):
        """Test serializing download item to dict."""
        episode = EpisodeIdentifier(season=1, episode=5, title="Test")
        # Note: serie_id uses provider key format (URL-safe, lowercase)
        item = DownloadItem(
            id="test_id",
            serie_id="serialization-test-key",
            serie_folder="Test Series (2023)",
            serie_name="Test Series",
            episode=episode
        )
        data = item.model_dump()
        assert data["id"] == "test_id"
        assert data["serie_id"] == "serialization-test-key"
        assert data["serie_name"] == "Test Series"
        assert data["episode"]["season"] == 1
        assert data["episode"]["episode"] == 5

    def test_download_item_from_dict(self):
        """Test deserializing download item from dict."""
        # Note: serie_id uses provider key format (URL-safe, lowercase)
        data = {
            "id": "test_id",
            "serie_id": "deserialize-test-key",
            "serie_folder": "Test Series (2023)",
            "serie_name": "Test Series",
            "episode": {
                "season": 1,
                "episode": 5,
                "title": "Test Episode"
            }
        }
        item = DownloadItem(**data)
        assert item.id == "test_id"
        assert item.serie_id == "deserialize-test-key"
        assert item.serie_name == "Test Series"
        assert item.episode.season == 1

    def test_queue_status_to_json(self):
        """Test serializing queue status to JSON."""
        status = QueueStatus(is_running=True)
        json_str = status.model_dump_json()
        assert '"is_running":true' in json_str.lower()

    def test_queue_stats_from_json(self):
        """Test deserializing queue stats from JSON."""
        json_str = '{"total_items": 5, "pending_count": 3}'
        stats = QueueStats.model_validate_json(json_str)
        assert stats.total_items == 5
        assert stats.pending_count == 3
