"""Tests for SerieScanner DB persistence functionality."""

from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from src.core.entities.series import Serie
from src.core.SerieScanner import SerieScanner


@pytest.fixture
def mock_session_factory():
    """Create a mock async session factory."""
    mock_session = AsyncMock()
    mock_session_factory = MagicMock(return_value=mock_session)
    return mock_session_factory, mock_session


@pytest.fixture
def sample_serie():
    """Create a sample Serie for testing."""
    return Serie(
        key="attack-on-titan",
        name="Attack on Titan",
        site="aniworld.to",
        folder="Attack on Titan (2013)",
        episodeDict={1: [1, 2, 3], 2: [1, 2]},
        year=2013
    )


class TestPersistSerieToDb:
    """Test _persist_serie_to_db method."""

    @pytest.mark.asyncio
    async def test_creates_new_series_when_not_exists(
        self, mock_session_factory, sample_serie
    ):
        """Verify new series is created in DB."""
        mock_factory, mock_session = mock_session_factory

        with patch(
            "src.server.database.connection.get_async_session_factory",
            return_value=mock_factory
        ):
            with patch(
                "src.server.database.service.AnimeSeriesService.get_by_key",
                return_value=None
            ):
                mock_anime_series = MagicMock()
                mock_anime_series.id = 1
                with patch(
                    "src.server.database.service.AnimeSeriesService.create",
                    return_value=mock_anime_series
                ):
                    scanner = SerieScanner("/tmp", MagicMock())
                    await scanner._persist_serie_to_db(sample_serie)

                    from src.server.database.service import AnimeSeriesService
                    AnimeSeriesService.create.assert_called_once()
                    call_kwargs = AnimeSeriesService.create.call_args[1]
                    assert call_kwargs["key"] == "attack-on-titan"
                    assert call_kwargs["name"] == "Attack on Titan"

    @pytest.mark.asyncio
    async def test_updates_existing_series(self, mock_session_factory, sample_serie):
        """Verify existing series is updated."""
        mock_factory, mock_session = mock_session_factory

        mock_existing = MagicMock()
        mock_existing.id = 42
        mock_existing.key = "attack-on-titan"

        scanner = SerieScanner("/tmp", MagicMock())

        with patch(
            "src.server.database.connection.get_async_session_factory",
            return_value=mock_factory
        ):
            with patch(
                "src.server.database.service.AnimeSeriesService.get_by_key",
                return_value=mock_existing
            ):
                with patch(
                    "src.server.database.service.AnimeSeriesService.update",
                    new_callable=AsyncMock
                ) as mock_update:
                    with patch.object(
                        scanner,
                        "_sync_episodes_to_db",
                        new_callable=AsyncMock
                    ):
                        await scanner._persist_serie_to_db(sample_serie)

                        mock_update.assert_called_once()
                        call_args = mock_update.call_args[0]
                        assert call_args[1] == 42  # series_id


class TestSyncEpisodesToDb:
    """Test _sync_episodes_to_db method."""

    @pytest.mark.asyncio
    async def test_preserves_downloaded_episodes(self):
        """Verify downloaded episodes are not removed even when no longer missing."""
        mock_session = AsyncMock()

        # S01E1 was downloaded (file exists), S01E2 was missing but file now exists
        # Both are no longer in episode_dict
        existing_eps = [
            MagicMock(id=1, season=1, episode_number=1, is_downloaded=True),
            MagicMock(id=2, season=1, episode_number=2, is_downloaded=True),
        ]

        with patch(
            "src.server.database.service.EpisodeService.get_by_series",
            return_value=existing_eps
        ):
            with patch(
                "src.server.database.service.EpisodeService.delete_by_series",
                new_callable=AsyncMock
            ) as mock_delete:
                scanner = SerieScanner("/tmp", MagicMock())
                # Neither S01E1 nor S01E2 are missing now
                await scanner._sync_episodes_to_db(
                    mock_session, 1, {}  # No episodes missing
                )

                # Neither should be deleted since both are downloaded
                mock_delete.assert_not_called()

    @pytest.mark.asyncio
    async def test_removes_missing_episodes_when_no_longer_missing(self):
        """Verify episodes removed from DB if file now exists."""
        mock_session = AsyncMock()

        existing_eps = [
            MagicMock(id=1, season=1, episode_number=1, is_downloaded=False),
            MagicMock(id=2, season=1, episode_number=2, is_downloaded=False),
        ]

        with patch(
            "src.server.database.service.EpisodeService.get_by_series",
            return_value=existing_eps
        ):
            with patch(
                "src.server.database.service.EpisodeService.delete_by_series",
                new_callable=AsyncMock
            ) as mock_delete:
                with patch(
                    "src.server.database.service.EpisodeService.create",
                    new_callable=AsyncMock
                ):
                    scanner = SerieScanner("/tmp", MagicMock())
                    await scanner._sync_episodes_to_db(
                        mock_session, 1, {1: [1]}  # Only S01E01 now missing
                    )

                    # S01E02 should be deleted since no longer missing
                    mock_delete.assert_called_once()

    @pytest.mark.asyncio
    async def test_adds_new_missing_episodes(self):
        """Verify new missing episodes are added."""
        mock_session = AsyncMock()

        existing_eps = [
            MagicMock(id=1, season=1, episode_number=1, is_downloaded=False),
        ]

        with patch(
            "src.server.database.service.EpisodeService.get_by_series",
            return_value=existing_eps
        ):
            with patch(
                "src.server.database.service.EpisodeService.create",
                new_callable=AsyncMock
            ) as mock_create:
                scanner = SerieScanner("/tmp", MagicMock())
                await scanner._sync_episodes_to_db(
                    mock_session, 1, {1: [1, 2, 3]}  # S01E01, S01E02, S01E03
                )

                # S01E02 and S01E03 should be created
                assert mock_create.call_count == 2


class TestPersistSerieToDbErrorHandling:
    """Test error handling in _persist_serie_to_db."""

    @pytest.mark.asyncio
    async def test_logs_error_when_db_unavailable(self, sample_serie):
        """Verify DB unavailability is logged but doesn't crash."""
        with patch(
            "src.server.database.connection.get_async_session_factory",
            side_effect=RuntimeError("DB not initialized")
        ):
            scanner = SerieScanner("/tmp", MagicMock())
            # Should not raise
            await scanner._persist_serie_to_db(sample_serie)

    @pytest.mark.asyncio
    async def test_rollback_on_failure(self, mock_session_factory, sample_serie):
        """Verify rollback on DB failure."""
        mock_factory, mock_session = mock_session_factory

        mock_existing = MagicMock()
        mock_existing.id = 1

        with patch(
            "src.server.database.connection.get_async_session_factory",
            return_value=mock_session
        ):
            with patch(
                "src.server.database.service.AnimeSeriesService.get_by_key",
                return_value=mock_existing
            ):
                with patch(
                    "src.server.database.service.AnimeSeriesService.update",
                    side_effect=Exception("DB error")
                ):
                    scanner = SerieScanner("/tmp", MagicMock())
                    # Should not raise but should rollback
                    await scanner._persist_serie_to_db(sample_serie)
                    mock_session.rollback.assert_called_once()
