"""Unit tests for adding series with episode scanning.

This module tests the complete flow of adding a series:
1. Series is added to database
2. Episodes are scanned
3. Episodes are saved to database
4. GUI is updated via WebSocket

All tests use mocks to avoid network traffic.
"""
from datetime import datetime
from unittest.mock import AsyncMock, MagicMock, Mock, patch

import pytest

from src.core.entities.series import Serie
from src.server.database.models import AnimeSeries, Episode


@pytest.fixture
def mock_series_app():
    """Create a mock SeriesApp with scanner."""
    app = MagicMock()
    
    # Mock serie_scanner
    app.serie_scanner = MagicMock()
    app.serie_scanner.keyDict = {}
    
    # Mock list
    app.list = MagicMock()
    app.list.keyDict = {}
    
    # Mock loader
    app.loader = MagicMock()
    app.loader.get_year = MagicMock(return_value=2024)
    
    return app


@pytest.fixture
def mock_db_session():
    """Create a mock database session."""
    session = AsyncMock()
    session.commit = AsyncMock()
    session.rollback = AsyncMock()
    session.close = AsyncMock()
    session.flush = AsyncMock()
    session.refresh = AsyncMock()
    return session


@pytest.fixture
def mock_anime_service(mock_series_app):
    """Create a mock AnimeService."""
    from src.server.services.anime_service import AnimeService
    
    service = AnimeService(mock_series_app)
    return service


@pytest.mark.asyncio
class TestAddSeriesWithEpisodes:
    """Test suite for adding series with episode scanning."""

    async def test_scan_single_series_updates_scanner_keydict(
        self,
        mock_series_app
    ):
        """Test that scan_single_series updates serie_scanner.keyDict."""
        # Arrange
        key = "test-anime"
        folder = "Test Anime (2024)"
        
        # Mock scan_single_series to update keyDict
        def mock_scan(key, folder):
            # Create Serie with episodes
            serie = Serie(
                key=key,
                name="Test Anime",
                site="aniworld.to",
                folder=folder,
                episodeDict={1: [1, 2, 3]},
                year=2024
            )
            # Update scanner's keyDict
            mock_series_app.serie_scanner.keyDict[key] = serie
            return {1: [1, 2, 3]}
        
        mock_series_app.serie_scanner.scan_single_series = mock_scan
        
        # Act
        result = mock_series_app.serie_scanner.scan_single_series(key, folder)
        
        # Assert
        assert key in mock_series_app.serie_scanner.keyDict
        serie = mock_series_app.serie_scanner.keyDict[key]
        assert serie.episodeDict == {1: [1, 2, 3]}
        assert len(serie.episodeDict[1]) == 3

    async def test_sync_single_series_gets_from_scanner_keydict(
        self,
        mock_series_app,
        mock_anime_service
    ):
        """Test that sync_single_series_after_scan gets Serie from scanner.keyDict."""
        # Arrange
        key = "test-anime"
        
        # Create Serie in scanner's keyDict with episodes
        serie = Serie(
            key=key,
            name="Test Anime",
            site="aniworld.to",
            folder="Test Anime (2024)",
            episodeDict={1: [1, 2, 3], 2: [1, 2]},
            year=2024
        )
        mock_series_app.serie_scanner.keyDict[key] = serie
        
        # Mock the database save method
        with patch.object(
            mock_anime_service,
            '_save_scan_results_to_db',
            new_callable=AsyncMock
        ) as mock_save:
            mock_save.return_value = 1
            
            with patch.object(
                mock_anime_service,
                '_load_series_from_db',
                new_callable=AsyncMock
            ):
                with patch.object(
                    mock_anime_service,
                    '_broadcast_series_updated',
                    new_callable=AsyncMock
                ):
                    # Act
                    await mock_anime_service.sync_single_series_after_scan(key)
                    
                    # Assert
                    mock_save.assert_called_once()
                    series_list = mock_save.call_args[0][0]
                    assert len(series_list) == 1
                    saved_serie = series_list[0]
                    assert saved_serie.key == key
                    assert saved_serie.episodeDict == {1: [1, 2, 3], 2: [1, 2]}

    async def test_save_scan_results_creates_episodes_in_db(
        self,
        mock_anime_service,
        mock_db_session
    ):
        """Test that _save_scan_results_to_db creates episodes."""
        # Arrange
        serie = Serie(
            key="test-anime",
            name="Test Anime",
            site="aniworld.to",
            folder="Test Anime (2024)",
            episodeDict={1: [1, 2, 3], 2: [1, 2]},
            year=2024
        )
        
        # Mock database services
        with patch('src.server.database.connection.get_db_session') as mock_get_db:
            # Setup context manager for database session
            mock_get_db.return_value.__aenter__.return_value = mock_db_session
            mock_get_db.return_value.__aexit__.return_value = None
            
            with patch('src.server.database.service.AnimeSeriesService') as mock_series_service:
                with patch('src.server.database.service.EpisodeService') as mock_episode_service:
                    # Series doesn't exist - will create new
                    mock_series_service.get_by_key = AsyncMock(return_value=None)
                    
                    # Mock create to return a series with ID
                    mock_db_series = MagicMock()
                    mock_db_series.id = 1
                    mock_db_series.key = "test-anime"
                    mock_series_service.create = AsyncMock(return_value=mock_db_series)
                    
                    # Mock episode creation
                    episode_create_calls = []
                    async def track_episode_create(db, series_id, season, episode_number):
                        episode_create_calls.append((series_id, season, episode_number))
                        ep = MagicMock()
                        ep.id = len(episode_create_calls)
                        ep.series_id = series_id
                        ep.season = season
                        ep.episode_number = episode_number
                        return ep
                    
                    mock_episode_service.create = AsyncMock(side_effect=track_episode_create)
                    
                    # Act
                    result = await mock_anime_service._save_scan_results_to_db([serie])
                    
                    # Assert
                    assert result == 1  # One series saved
                    
                    # Verify episodes were created
                    assert len(episode_create_calls) == 5  # 3 from season 1, 2 from season 2
                    
                    # Check season 1 episodes
                    assert (1, 1, 1) in episode_create_calls
                    assert (1, 1, 2) in episode_create_calls
                    assert (1, 1, 3) in episode_create_calls
                    
                    # Check season 2 episodes
                    assert (1, 2, 1) in episode_create_calls
                    assert (1, 2, 2) in episode_create_calls

    async def test_update_series_adds_missing_episodes(
        self,
        mock_anime_service,
        mock_db_session
    ):
        """Test that _update_series_in_db adds new missing episodes."""
        # Arrange
        serie = Serie(
            key="test-anime",
            name="Test Anime",
            site="aniworld.to",
            folder="Test Anime (2024)",
            episodeDict={1: [1, 2, 3, 4]},  # 4 episodes
            year=2024
        )
        
        # Existing series in DB with only 2 episodes
        existing_db_series = MagicMock()
        existing_db_series.id = 1
        existing_db_series.key = "test-anime"
        existing_db_series.folder = "Test Anime (2024)"
        
        # Mock existing episodes in DB
        existing_episode_1 = MagicMock()
        existing_episode_1.id = 1
        existing_episode_1.series_id = 1
        existing_episode_1.season = 1
        existing_episode_1.episode_number = 1
        
        existing_episode_2 = MagicMock()
        existing_episode_2.id = 2
        existing_episode_2.series_id = 1
        existing_episode_2.season = 1
        existing_episode_2.episode_number = 2
        
        existing_episodes = [existing_episode_1, existing_episode_2]
        
        with patch('src.server.database.connection.get_db_session') as mock_get_db:
            mock_get_db.return_value.__aenter__.return_value = mock_db_session
            mock_get_db.return_value.__aexit__.return_value = None
            
            with patch('src.server.database.service.AnimeSeriesService') as mock_series_service:
                with patch('src.server.database.service.EpisodeService') as mock_episode_service:
                    # Series exists
                    mock_series_service.get_by_key = AsyncMock(return_value=existing_db_series)
                    
                    # Mock get_by_series to return existing episodes
                    mock_episode_service.get_by_series = AsyncMock(return_value=existing_episodes)
                    
                    # Track new episodes created
                    new_episodes_created = []
                    async def track_episode_create(db, series_id, season, episode_number):
                        new_episodes_created.append((series_id, season, episode_number))
                        return MagicMock()
                    
                    mock_episode_service.create = AsyncMock(side_effect=track_episode_create)
                    mock_episode_service.delete = AsyncMock()
                    
                    # Act
                    result = await mock_anime_service._save_scan_results_to_db([serie])
                    
                    # Assert
                    assert result == 1
                    
                    # Should create 2 new episodes (episode 3 and 4)
                    assert len(new_episodes_created) == 2
                    assert (1, 1, 3) in new_episodes_created
                    assert (1, 1, 4) in new_episodes_created

    async def test_complete_add_series_flow(
        self,
        mock_series_app
    ):
        """Integration test for complete add series flow."""
        from src.server.services.anime_service import AnimeService

        # Arrange
        key = "test-anime"
        folder = "Test Anime (2024)"
        
        # Setup mock scanner to populate keyDict
        def mock_scan(key, folder):
            serie = Serie(
                key=key,
                name="Test Anime",
                site="aniworld.to",
                folder=folder,
                episodeDict={1: [1, 2, 3]},
                year=2024
            )
            mock_series_app.serie_scanner.keyDict[key] = serie
            return {1: [1, 2, 3]}
        
        mock_series_app.serie_scanner.scan_single_series = mock_scan
        
        # Create service
        anime_service = AnimeService(mock_series_app)
        
        # Mock database operations
        with patch('src.server.database.connection.get_db_session') as mock_get_db:
            mock_db = AsyncMock()
            mock_get_db.return_value.__aenter__.return_value = mock_db
            mock_get_db.return_value.__aexit__.return_value = None
            
            with patch('src.server.database.service.AnimeSeriesService') as mock_series_service:
                with patch('src.server.database.service.EpisodeService') as mock_episode_service:
                    # Series doesn't exist
                    mock_series_service.get_by_key = AsyncMock(return_value=None)
                    
                    # Mock series creation
                    mock_db_series = MagicMock()
                    mock_db_series.id = 1
                    mock_series_service.create = AsyncMock(return_value=mock_db_series)
                    
                    # Track episodes
                    episodes_created = []
                    async def track_create(db, series_id, season, episode_number):
                        episodes_created.append((season, episode_number))
                        return MagicMock()
                    
                    mock_episode_service.create = AsyncMock(side_effect=track_create)
                    
                    # Mock other methods
                    with patch.object(anime_service, '_load_series_from_db', new_callable=AsyncMock):
                        with patch.object(anime_service, '_broadcast_series_updated', new_callable=AsyncMock):
                            # Act
                            # 1. Scan episodes
                            result = mock_series_app.serie_scanner.scan_single_series(key, folder)
                            
                            # 2. Sync to database
                            await anime_service.sync_single_series_after_scan(key)
                            
                            # Assert
                            # Episodes were scanned
                            assert result == {1: [1, 2, 3]}
                            
                            # Serie was added to scanner keyDict
                            assert key in mock_series_app.serie_scanner.keyDict
                            
                            # Episodes were saved to DB
                            assert len(episodes_created) == 3
                            assert (1, 1) in episodes_created
                            assert (1, 2) in episodes_created
                            assert (1, 3) in episodes_created

    async def test_websocket_broadcast_on_series_update(
        self,
        mock_series_app
    ):
        """Test that WebSocket broadcasts series_updated event with complete data including NFO fields."""
        from unittest.mock import AsyncMock, MagicMock, patch

        from src.server.database.models import AnimeSeries
        from src.server.services.anime_service import AnimeService

        # Arrange
        key = "test-anime"
        
        # Create Serie in list.keyDict with episodes
        serie = Serie(
            key=key,
            name="Test Anime",
            site="aniworld.to",
            folder="Test Anime (2024)",
            episodeDict={1: [1, 2, 3]},
            year=2024
        )
        mock_series_app.list.keyDict[key] = serie
        
        # Mock database AnimeSeries with NFO data
        mock_db_series = AnimeSeries(
            key=key,
            name="Test Anime",
            folder="Test Anime (2024)",
            site="aniworld.to",
            year=2024,
            has_nfo=True,
            tmdb_id="12345",
            tvdb_id="67890",
            nfo_created_at=datetime(2024, 1, 1, 12, 0, 0),
            nfo_updated_at=datetime(2024, 1, 2, 12, 0, 0)
        )
        mock_db_series.id = 1

        # Create service with mocked WebSocket
        anime_service = AnimeService(mock_series_app)
        mock_websocket = AsyncMock()
        anime_service._websocket_service = mock_websocket

        # Mock database session and service
        mock_db_session = AsyncMock()
        mock_db_session.__aenter__ = AsyncMock(return_value=mock_db_session)
        mock_db_session.__aexit__ = AsyncMock()

        # Mock episodes that match the in-memory episodeDict
        mock_episodes = [
            MagicMock(season=1, episode_number=1),
            MagicMock(season=1, episode_number=2),
            MagicMock(season=1, episode_number=3),
        ]

        with patch('src.server.database.connection.get_db_session', return_value=mock_db_session):
            with patch('src.server.database.service.AnimeSeriesService') as MockAnimeSeriesService:
                MockAnimeSeriesService.get_by_key = AsyncMock(return_value=mock_db_series)
                with patch('src.server.database.service.EpisodeService') as MockEpisodeService:
                    MockEpisodeService.get_by_series = AsyncMock(return_value=mock_episodes)

                    # Act
                    await anime_service._broadcast_series_updated(key)
        
        # Assert
        mock_websocket.broadcast.assert_called_once()
        call_args = mock_websocket.broadcast.call_args[0][0]
        
        # Verify payload structure
        assert call_args["type"] == "series_updated"
        assert call_args["key"] == key
        assert "data" in call_args
        
        # Verify basic series data
        assert call_args["data"]["key"] == key
        assert call_args["data"]["name"] == "Test Anime"
        assert call_args["data"]["missing_episodes"] == {"1": [1, 2, 3]}
        assert call_args["data"]["has_missing"] is True
        
        # Verify NFO metadata fields are included
        assert call_args["data"]["has_nfo"] is True
        assert call_args["data"]["tmdb_id"] == "12345"
        assert call_args["data"]["tvdb_id"] == "67890"
        assert call_args["data"]["nfo_created_at"] == "2024-01-01T12:00:00"
        assert call_args["data"]["nfo_updated_at"] == "2024-01-02T12:00:00"
        
        assert "timestamp" in call_args


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
