"""Tests for ``sync_movie`` — TMDB sync orchestrator for movies.""" from __future__ import annotations import logging from datetime import timedelta import pytest from alfred.application.exceptions import MovieNotFoundInLibrary from alfred.application.movies.sync import sync_movie from alfred.domain.shared.value_objects import TmdbId from alfred.infrastructure.api.tmdb.dto import TmdbMovieInfo from alfred.infrastructure.persistence.dot_alfred.v2.repository import ( DotAlfredMovieLibraryIndex, DotAlfredMovieReleaseRepository, ) class _StubTMDBClient: """Minimal stand-in implementing only ``get_movie_info``.""" def __init__(self, info: TmdbMovieInfo) -> None: self._info = info self.calls: list[int] = [] def get_movie_info(self, tmdb_id: int) -> TmdbMovieInfo: self.calls.append(tmdb_id) return self._info @pytest.fixture def tmdb_info_fresh() -> TmdbMovieInfo: """TMDB payload returned by the stub — distinct from any placeholder.""" return TmdbMovieInfo( tmdb_id=27205, imdb_id="tt1375666", title="Inception", release_year=2010, ) def _make_index_and_repos(movie_library): repo = DotAlfredMovieReleaseRepository(movie_library) index = DotAlfredMovieLibraryIndex(movie_library, release_repo=repo) return repo, index # ════════════════════════════════════════════════════════════════════════════ # Happy paths # ════════════════════════════════════════════════════════════════════════════ class TestSyncMovieRefreshesPlaceholder: def test_placeholder_entry_refreshes_regardless_of_ttl( self, movie_library, inception_release, tmdb_info_fresh, now_utc ): repo, index = _make_index_and_repos(movie_library) repo.save(inception_release) client = _StubTMDBClient(tmdb_info_fresh) result = sync_movie( movie_library, tmdb_id=TmdbId(27205), index=index, release_repo=repo, tmdb_client=client, ttl_days=365, now=lambda: now_utc, ) assert client.calls == [27205] assert result.name == "Inception" assert result.release_year == 2010 class TestSyncMovieTTLGate: def test_fresh_entry_within_ttl_is_noop( self, movie_library, inception_release, tmdb_info_fresh, now_utc ): repo, index = _make_index_and_repos(movie_library) repo.save(inception_release) client = _StubTMDBClient(tmdb_info_fresh) sync_movie( movie_library, tmdb_id=TmdbId(27205), index=index, release_repo=repo, tmdb_client=client, ttl_days=14, now=lambda: now_utc, ) first = len(client.calls) later = now_utc + timedelta(days=3) result = sync_movie( movie_library, tmdb_id=TmdbId(27205), index=index, release_repo=repo, tmdb_client=client, ttl_days=14, now=lambda: later, ) assert len(client.calls) == first assert result.name == "Inception" def test_stale_entry_past_ttl_refreshes( self, movie_library, inception_release, tmdb_info_fresh, now_utc ): repo, index = _make_index_and_repos(movie_library) repo.save(inception_release) client = _StubTMDBClient(tmdb_info_fresh) sync_movie( movie_library, tmdb_id=TmdbId(27205), index=index, release_repo=repo, tmdb_client=client, ttl_days=14, now=lambda: now_utc, ) first = len(client.calls) later = now_utc + timedelta(days=20) sync_movie( movie_library, tmdb_id=TmdbId(27205), index=index, release_repo=repo, tmdb_client=client, ttl_days=14, now=lambda: later, ) assert len(client.calls) == first + 1 class TestSyncMovieForce: def test_force_bypasses_ttl( self, movie_library, inception_release, tmdb_info_fresh, now_utc ): repo, index = _make_index_and_repos(movie_library) repo.save(inception_release) client = _StubTMDBClient(tmdb_info_fresh) sync_movie( movie_library, tmdb_id=TmdbId(27205), index=index, release_repo=repo, tmdb_client=client, ttl_days=14, now=lambda: now_utc, ) first = len(client.calls) sync_movie( movie_library, tmdb_id=TmdbId(27205), index=index, release_repo=repo, tmdb_client=client, ttl_days=14, now=lambda: now_utc, force=True, ) assert len(client.calls) == first + 1 # ════════════════════════════════════════════════════════════════════════════ # Degenerate paths # ════════════════════════════════════════════════════════════════════════════ class TestSyncMovieMissingSidecar: def test_indexed_movie_without_sidecar_logs_and_returns_existing( self, movie_library, inception_release, tmdb_info_fresh, now_utc, caplog ): repo, index = _make_index_and_repos(movie_library) repo.save(inception_release) client = _StubTMDBClient(tmdb_info_fresh) sync_movie( movie_library, tmdb_id=TmdbId(27205), index=index, release_repo=repo, tmdb_client=client, ttl_days=14, now=lambda: now_utc, ) # Wipe the per-movie sidecar. (movie_library / inception_release.folder / ".alfred").unlink() with caplog.at_level(logging.WARNING): result = sync_movie( movie_library, tmdb_id=TmdbId(27205), index=index, release_repo=repo, tmdb_client=client, ttl_days=14, now=lambda: now_utc, force=True, ) # Existing index entry returned unchanged — sync can't upsert # without a release to source folder/imdb_id from. assert result.name == "Inception" assert any("per-movie sidecar missing" in r.message for r in caplog.records) class TestSyncMovieNotFound: def test_no_index_entry_no_sidecar_raises( self, movie_library, tmdb_info_fresh, now_utc ): repo, index = _make_index_and_repos(movie_library) client = _StubTMDBClient(tmdb_info_fresh) with pytest.raises(MovieNotFoundInLibrary, match="27205"): sync_movie( movie_library, tmdb_id=TmdbId(27205), index=index, release_repo=repo, tmdb_client=client, ttl_days=14, now=lambda: now_utc, )