"""Unit tests for series manager service.

Tests series orchestration, NFO processing, configuration handling,
and async batch processing.
"""

import asyncio
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from src.core.services.series_manager_service import SeriesManagerService


class TestSeriesManagerServiceInit:
    """Tests for SeriesManagerService initialization."""

    @patch("src.core.services.series_manager_service.SerieList")
    def test_init_without_nfo_service(self, mock_serie_list):
        """Service initializes without NFO when no API key provided."""
        svc = SeriesManagerService(
            anime_directory="/anime",
            tmdb_api_key=None,
            auto_create_nfo=False,
        )
        assert svc.nfo_service is None
        assert svc.anime_directory == "/anime"

    @patch("src.core.services.series_manager_service.SerieList")
    def test_init_with_nfo_disabled(self, mock_serie_list):
        """NFO service not created when auto_create and update both False."""
        svc = SeriesManagerService(
            anime_directory="/anime",
            tmdb_api_key="key123",
            auto_create_nfo=False,
            update_on_scan=False,
        )
        assert svc.nfo_service is None

    @patch("src.core.services.nfo_factory.get_nfo_factory")
    @patch("src.core.services.series_manager_service.SerieList")
    def test_init_creates_nfo_service_when_enabled(
        self, mock_serie_list, mock_factory_fn
    ):
        """NFO service is created when auto_create is True and key exists."""
        mock_factory = MagicMock()
        mock_nfo = MagicMock()
        mock_factory.create.return_value = mock_nfo
        mock_factory_fn.return_value = mock_factory

        svc = SeriesManagerService(
            anime_directory="/anime",
            tmdb_api_key="key123",
            auto_create_nfo=True,
        )
        assert svc.nfo_service is mock_nfo

    @patch("src.core.services.nfo_factory.get_nfo_factory")
    @patch("src.core.services.series_manager_service.SerieList")
    def test_init_handles_nfo_factory_error(
        self, mock_serie_list, mock_factory_fn
    ):
        """NFO service set to None if factory raises."""
        mock_factory_fn.side_effect = ValueError("bad config")
        svc = SeriesManagerService(
            anime_directory="/anime",
            tmdb_api_key="key123",
            auto_create_nfo=True,
        )
        assert svc.nfo_service is None

    @patch("src.core.services.series_manager_service.SerieList")
    def test_init_stores_config_flags(self, mock_serie_list):
        """Configuration flags are stored correctly."""
        svc = SeriesManagerService(
            anime_directory="/anime",
            auto_create_nfo=True,
            update_on_scan=True,
            download_poster=False,
            download_logo=False,
            download_fanart=True,
        )
        assert svc.auto_create_nfo is True
        assert svc.update_on_scan is True
        assert svc.download_poster is False
        assert svc.download_logo is False
        assert svc.download_fanart is True

    @patch("src.core.services.series_manager_service.SerieList")
    def test_serie_list_created_with_skip_load(self, mock_serie_list):
        """SerieList is created with skip_load=True."""
        SeriesManagerService(anime_directory="/anime")
        mock_serie_list.assert_called_once_with("/anime", skip_load=True)


class TestFromSettings:
    """Tests for from_settings classmethod."""

    @patch("src.core.services.series_manager_service.settings")
    @patch("src.core.services.series_manager_service.SerieList")
    def test_from_settings_uses_all_settings(self, mock_serie_list, mock_settings):
        """from_settings passes all relevant settings to constructor."""
        mock_settings.anime_directory = "/anime"
        mock_settings.tmdb_api_key = None
        mock_settings.nfo_auto_create = False
        mock_settings.nfo_update_on_scan = False
        mock_settings.nfo_download_poster = True
        mock_settings.nfo_download_logo = True
        mock_settings.nfo_download_fanart = True
        mock_settings.nfo_image_size = "original"

        svc = SeriesManagerService.from_settings()
        assert isinstance(svc, SeriesManagerService)
        assert svc.anime_directory == "/anime"


class TestProcessNfoForSeries:
    """Tests for process_nfo_for_series method."""

    @pytest.fixture
    def service(self):
        """Create a service with mocked dependencies."""
        with patch("src.core.services.series_manager_service.SerieList"):
            svc = SeriesManagerService(anime_directory="/anime")
            svc.nfo_service = AsyncMock()
            svc.auto_create_nfo = True
            return svc

    @pytest.mark.asyncio
    async def test_returns_early_without_nfo_service(self):
        """Does nothing when nfo_service is None."""
        with patch("src.core.services.series_manager_service.SerieList"):
            svc = SeriesManagerService(anime_directory="/anime")
            svc.nfo_service = None
            # Should not raise
            await svc.process_nfo_for_series("folder", "Name", "key")

    @pytest.mark.asyncio
    async def test_creates_nfo_when_not_exists(self, service):
        """Creates NFO file when it doesn't exist and auto_create is True."""
        service.nfo_service.check_nfo_exists = AsyncMock(return_value=False)
        service.nfo_service.create_tvshow_nfo = AsyncMock()

        await service.process_nfo_for_series("folder", "Name", "key", year=2024)
        service.nfo_service.create_tvshow_nfo.assert_awaited_once()

    @pytest.mark.asyncio
    async def test_skips_creation_when_exists(self, service):
        """Skips NFO creation when file already exists."""
        service.nfo_service.check_nfo_exists = AsyncMock(return_value=True)
        service.nfo_service.parse_nfo_ids = MagicMock(
            return_value={"tmdb_id": None, "tvdb_id": None}
        )
        service.nfo_service.create_tvshow_nfo = AsyncMock()

        await service.process_nfo_for_series("folder", "Name", "key")
        service.nfo_service.create_tvshow_nfo.assert_not_awaited()

    @pytest.mark.asyncio
    async def test_handles_tmdb_api_error(self, service):
        """TMDBAPIError is caught and logged (not re-raised)."""
        from src.core.services.tmdb_client import TMDBAPIError
        service.nfo_service.check_nfo_exists = AsyncMock(
            side_effect=TMDBAPIError("rate limited")
        )
        # Should not raise
        await service.process_nfo_for_series("folder", "Name", "key")

    @pytest.mark.asyncio
    async def test_handles_unexpected_error(self, service):
        """Unexpected exceptions are caught and logged."""
        service.nfo_service.check_nfo_exists = AsyncMock(
            side_effect=RuntimeError("unexpected")
        )
        await service.process_nfo_for_series("folder", "Name", "key")


class TestScanAndProcessNfo:
    """Tests for scan_and_process_nfo method."""

    @pytest.mark.asyncio
    async def test_skips_when_no_nfo_service(self):
        """Returns early when nfo_service is None."""
        with patch("src.core.services.series_manager_service.SerieList"):
            svc = SeriesManagerService(anime_directory="/anime")
            svc.nfo_service = None
            await svc.scan_and_process_nfo()


class TestClose:
    """Tests for close method."""

    @pytest.mark.asyncio
    async def test_close_with_nfo_service(self):
        """Closes NFO service when present."""
        with patch("src.core.services.series_manager_service.SerieList"):
            svc = SeriesManagerService(anime_directory="/anime")
            svc.nfo_service = AsyncMock()
            await svc.close()
            svc.nfo_service.close.assert_awaited_once()

    @pytest.mark.asyncio
    async def test_close_without_nfo_service(self):
        """Close works fine when no NFO service."""
        with patch("src.core.services.series_manager_service.SerieList"):
            svc = SeriesManagerService(anime_directory="/anime")
            svc.nfo_service = None
            await svc.close()  # Should not raise
