"""Tests for the pure parsing helpers in ``alfred.infrastructure.api.tmdb.dto``. These tests exercise :func:`parse_tv_show_info` without any HTTP — the function takes the raw dicts that the client would otherwise pass after deserializing the TMDB JSON response. The reference date is injected so the ``aired`` derivation is deterministic. """ from __future__ import annotations from datetime import date import pytest from alfred.domain.movies_TO_CHECK.value_objects import MovieTitle, ReleaseYear from alfred.domain.shared_TO_CHECK.value_objects import ImdbId, TmdbId from alfred.domain.tv_shows.value_objects import ShowStatus from alfred.infrastructure.api_TO_CHECK.tmdb.dto import ( TmdbMovieInfo, TmdbSeasonInfo, TmdbShowInfo, parse_movie_info, parse_tv_show_info, ) REF_DATE = date(2026, 5, 25) def _details(**overrides): base = { "id": 84958, "name": "Foundation", "status": "Returning Series", "seasons": [], } base.update(overrides) return base class TestParseTvShowInfoHappyPath: def test_minimal(self): info = parse_tv_show_info( _details(), {"imdb_id": "tt0804484"}, today=REF_DATE, ) assert info == TmdbShowInfo( tmdb_id=TmdbId(84958), imdb_id=ImdbId("tt0804484"), name="Foundation", status=ShowStatus.RETURNING_SERIES, seasons=(), ) def test_with_seasons(self): info = parse_tv_show_info( _details( seasons=[ {"season_number": 1, "episode_count": 10, "air_date": "2021-09-24"}, {"season_number": 2, "episode_count": 10, "air_date": "2023-07-14"}, {"season_number": 3, "episode_count": 10, "air_date": "2027-01-01"}, ], ), {"imdb_id": "tt0804484"}, today=REF_DATE, ) assert info.seasons == ( TmdbSeasonInfo(number=1, episode_count=10, aired=True), TmdbSeasonInfo(number=2, episode_count=10, aired=True), TmdbSeasonInfo(number=3, episode_count=10, aired=False), ) class TestParseTvShowInfoImdb: def test_missing_imdb_id_becomes_none(self): info = parse_tv_show_info(_details(), {}, today=REF_DATE) assert info.imdb_id is None def test_null_imdb_id_becomes_none(self): info = parse_tv_show_info(_details(), {"imdb_id": None}, today=REF_DATE) assert info.imdb_id is None def test_empty_string_imdb_id_becomes_none(self): info = parse_tv_show_info(_details(), {"imdb_id": ""}, today=REF_DATE) assert info.imdb_id is None class TestParseTvShowInfoAired: def test_air_date_today_counts_as_aired(self): info = parse_tv_show_info( _details( seasons=[{"season_number": 1, "episode_count": 1, "air_date": "2026-05-25"}], ), {}, today=REF_DATE, ) assert info.seasons[0].aired is True def test_air_date_tomorrow_not_aired(self): info = parse_tv_show_info( _details( seasons=[{"season_number": 1, "episode_count": 1, "air_date": "2026-05-26"}], ), {}, today=REF_DATE, ) assert info.seasons[0].aired is False def test_no_air_date_not_aired(self): info = parse_tv_show_info( _details( seasons=[{"season_number": 1, "episode_count": 1}], ), {}, today=REF_DATE, ) assert info.seasons[0].aired is False def test_empty_air_date_not_aired(self): info = parse_tv_show_info( _details( seasons=[{"season_number": 1, "episode_count": 1, "air_date": ""}], ), {}, today=REF_DATE, ) assert info.seasons[0].aired is False def test_malformed_air_date_not_aired(self): info = parse_tv_show_info( _details( seasons=[{"season_number": 1, "episode_count": 1, "air_date": "soon"}], ), {}, today=REF_DATE, ) assert info.seasons[0].aired is False class TestParseTvShowInfoErrors: def test_missing_id_raises(self): with pytest.raises(ValueError, match="'id'"): parse_tv_show_info({"name": "X", "status": "Ended"}, {}, today=REF_DATE) def test_missing_name_raises(self): with pytest.raises(ValueError, match="'name'"): parse_tv_show_info({"id": 1, "status": "Ended"}, {}, today=REF_DATE) def test_empty_name_raises(self): with pytest.raises(ValueError, match="'name'"): parse_tv_show_info( {"id": 1, "name": "", "status": "Ended"}, {}, today=REF_DATE ) def test_missing_status_raises(self): with pytest.raises(ValueError, match="'status'"): parse_tv_show_info({"id": 1, "name": "X"}, {}, today=REF_DATE) def test_season_missing_number_raises(self): with pytest.raises(ValueError, match="season_number"): parse_tv_show_info( _details(seasons=[{"episode_count": 5}]), {}, today=REF_DATE, ) def test_season_missing_episode_count_raises(self): with pytest.raises(ValueError, match="episode_count"): parse_tv_show_info( _details(seasons=[{"season_number": 1}]), {}, today=REF_DATE, ) # ════════════════════════════════════════════════════════════════════════════ # parse_movie_info # ════════════════════════════════════════════════════════════════════════════ def _movie_details(**overrides): base = { "id": 27205, "title": "Inception", "release_date": "2010-07-16", } base.update(overrides) return base class TestParseMovieInfoHappyPath: def test_minimal(self): info = parse_movie_info( _movie_details(), {"imdb_id": "tt1375666"}, ) assert info == TmdbMovieInfo( tmdb_id=TmdbId(27205), imdb_id=ImdbId("tt1375666"), title=MovieTitle("Inception"), release_year=ReleaseYear(2010), ) def test_release_year_extracted_from_release_date(self): info = parse_movie_info( _movie_details(release_date="1999-03-31"), {}, ) assert info.release_year == ReleaseYear(1999) class TestParseMovieInfoImdb: def test_missing_imdb_id_becomes_none(self): info = parse_movie_info(_movie_details(), {}) assert info.imdb_id is None def test_null_imdb_id_becomes_none(self): info = parse_movie_info(_movie_details(), {"imdb_id": None}) assert info.imdb_id is None def test_empty_string_imdb_id_becomes_none(self): info = parse_movie_info(_movie_details(), {"imdb_id": ""}) assert info.imdb_id is None class TestParseMovieInfoReleaseYear: def test_no_release_date_yields_none(self): info = parse_movie_info(_movie_details(release_date=None), {}) assert info.release_year is None def test_empty_release_date_yields_none(self): info = parse_movie_info(_movie_details(release_date=""), {}) assert info.release_year is None def test_release_date_too_short_yields_none(self): info = parse_movie_info(_movie_details(release_date="201"), {}) assert info.release_year is None def test_release_date_non_numeric_prefix_yields_none(self): info = parse_movie_info(_movie_details(release_date="soon"), {}) assert info.release_year is None def test_missing_release_date_yields_none(self): # release_date key absent from the payload. info = parse_movie_info({"id": 1, "title": "X"}, {}) assert info.release_year is None class TestParseMovieInfoErrors: def test_missing_id_raises(self): with pytest.raises(ValueError, match="'id'"): parse_movie_info({"title": "X"}, {}) def test_missing_title_raises(self): with pytest.raises(ValueError, match="'title'"): parse_movie_info({"id": 1}, {}) def test_empty_title_raises(self): with pytest.raises(ValueError, match="'title'"): parse_movie_info({"id": 1, "title": ""}, {})