"""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