"""Integration tests for GET /api/v1/series (browse/discover, section 17 -- additive beyond the spec's minimum endpoint list): sorting, filtering, and pagination against real PostgreSQL, exercised through tv_service.list_series directly rather than over HTTP. Mirrors test_movie_browse.py.""" from __future__ import annotations from typing import Any import pytest from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.ids import aurora_id from app.models.genre import Genre from app.services import tv_service from app.services.ingestion.series_ingestion import ingest_series from tests.integration.test_series_ingestion import FakeTMDbSeriesProvider def _series_payload(**overrides: Any) -> dict[str, Any]: base = { "id": 9000, "name": "Untitled", "status": "Ended", "genres": [{"id": 18, "name": "Drama"}], "first_air_date": "2020-01-01", "popularity": 10.0, "vote_average": 5.0, } base.update(overrides) return base async def _seed_series(db_session: AsyncSession) -> None: shows = [ _series_payload(id=9101, name="A Drama Show", popularity=10.0, vote_average=5.0), _series_payload( id=9102, name="B Drama Show", popularity=90.0, vote_average=8.0, first_air_date="2021-01-01", ), _series_payload( id=9103, name="C Comedy Show", genres=[{"id": 35, "name": "Comedy"}], popularity=50.0, vote_average=6.0, ), _series_payload( id=9104, name="D Planned Show", status="Planned", popularity=20.0, vote_average=None, first_air_date="2022-01-01", ), ] for payload in shows: await ingest_series( db_session, FakeTMDbSeriesProvider(payload, {}), str(payload["id"]) ) await db_session.commit() async def _genre_aurora_id(db_session: AsyncSession, name: str) -> str: genre = ( await db_session.execute(select(Genre).where(Genre.name == name)) ).scalar_one() return aurora_id("genre", str(genre.id)) @pytest.mark.asyncio async def test_list_series_defaults_to_popularity_descending(db_session: AsyncSession): await _seed_series(db_session) out = await tv_service.list_series( db_session, genre_aurora_id=None, year=None, status=None, sort_by="popularity", sort_dir="desc", page=1, page_size=20, ) assert out.total_results == 4 assert out.total_pages == 1 assert [s.title for s in out.results] == [ "B Drama Show", "C Comedy Show", "D Planned Show", "A Drama Show", ] @pytest.mark.asyncio async def test_list_series_filters_by_genre(db_session: AsyncSession): await _seed_series(db_session) drama_id = await _genre_aurora_id(db_session, "Drama") out = await tv_service.list_series( db_session, genre_aurora_id=drama_id, year=None, status=None, sort_by="popularity", sort_dir="desc", page=1, page_size=20, ) assert out.total_results == 3 assert {s.title for s in out.results} == {"B Drama Show", "D Planned Show", "A Drama Show"} @pytest.mark.asyncio async def test_list_series_filters_by_year(db_session: AsyncSession): await _seed_series(db_session) out = await tv_service.list_series( db_session, genre_aurora_id=None, year=2020, status=None, sort_by="popularity", sort_dir="desc", page=1, page_size=20, ) assert [s.title for s in out.results] == ["C Comedy Show", "A Drama Show"] @pytest.mark.asyncio async def test_list_series_filters_by_status(db_session: AsyncSession): await _seed_series(db_session) out = await tv_service.list_series( db_session, genre_aurora_id=None, year=None, status="planned", sort_by="popularity", sort_dir="desc", page=1, page_size=20, ) assert [s.title for s in out.results] == ["D Planned Show"] @pytest.mark.asyncio async def test_list_series_paginates(db_session: AsyncSession): await _seed_series(db_session) page1 = await tv_service.list_series( db_session, genre_aurora_id=None, year=None, status=None, sort_by="popularity", sort_dir="desc", page=1, page_size=2, ) page2 = await tv_service.list_series( db_session, genre_aurora_id=None, year=None, status=None, sort_by="popularity", sort_dir="desc", page=2, page_size=2, ) assert page1.total_results == 4 assert page1.total_pages == 2 assert [s.title for s in page1.results] == ["B Drama Show", "C Comedy Show"] assert [s.title for s in page2.results] == ["D Planned Show", "A Drama Show"] @pytest.mark.asyncio async def test_list_series_rejects_malformed_genre_filter(db_session: AsyncSession): with pytest.raises(tv_service.InvalidSeriesFilterError): await tv_service.list_series( db_session, genre_aurora_id="not-an-aurora-id", year=None, status=None, sort_by="popularity", sort_dir="desc", page=1, page_size=20, ) @pytest.mark.asyncio async def test_list_series_rejects_unknown_status_filter(db_session: AsyncSession): with pytest.raises(tv_service.InvalidSeriesFilterError): await tv_service.list_series( db_session, genre_aurora_id=None, year=None, status="not_a_real_status", sort_by="popularity", sort_dir="desc", page=1, page_size=20, )