"""Integration tests for GET /api/v1/movies (browse/discover, section 17 -- additive beyond the spec's minimum endpoint list): sorting, filtering, and pagination against real PostgreSQL, exercised through movie_service.list_movies directly rather than over HTTP.""" 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.models.keyword import Keyword from app.services import movie_service from app.services.ingestion.movie_ingestion import ingest_movie from tests.integration.test_movie_ingestion import FakeTMDbProvider def _movie_payload(**overrides: Any) -> dict[str, Any]: base = { "id": 9000, "title": "Untitled", "status": "Released", "genres": [{"id": 28, "name": "Action"}], "release_date": "2020-01-01", "popularity": 10.0, "vote_average": 5.0, } base.update(overrides) return base async def _seed_movies(db_session: AsyncSession) -> None: movies = [ _movie_payload(id=9001, title="A Action Movie", popularity=10.0, vote_average=5.0), _movie_payload( id=9002, title="B Action Movie", popularity=90.0, vote_average=8.0, release_date="2021-01-01", keywords={"keywords": [{"id": 1, "name": "heist"}]}, ), _movie_payload( id=9003, title="C Comedy Movie", genres=[{"id": 35, "name": "Comedy"}], popularity=50.0, vote_average=6.0, ), _movie_payload( id=9004, title="D Planned Movie", status="Planned", popularity=20.0, vote_average=None, release_date="2022-01-01", ), ] for payload in movies: await ingest_movie(db_session, FakeTMDbProvider(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)) async def _keyword_aurora_id(db_session: AsyncSession, name: str) -> str: keyword = ( await db_session.execute(select(Keyword).where(Keyword.name == name)) ).scalar_one() return aurora_id("keyword", str(keyword.id)) @pytest.mark.asyncio async def test_list_movies_defaults_to_popularity_descending(db_session: AsyncSession): await _seed_movies(db_session) out = await movie_service.list_movies( db_session, genre_aurora_id=None, keyword_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 [m.title for m in out.results] == [ "B Action Movie", "C Comedy Movie", "D Planned Movie", "A Action Movie", ] @pytest.mark.asyncio async def test_list_movies_filters_by_genre(db_session: AsyncSession): await _seed_movies(db_session) action_id = await _genre_aurora_id(db_session, "Action") out = await movie_service.list_movies( db_session, genre_aurora_id=action_id, keyword_aurora_id=None, year=None, status=None, sort_by="popularity", sort_dir="desc", page=1, page_size=20, ) assert out.total_results == 3 assert {m.title for m in out.results} == { "B Action Movie", "D Planned Movie", "A Action Movie", } @pytest.mark.asyncio async def test_list_movies_filters_by_keyword(db_session: AsyncSession): await _seed_movies(db_session) heist_id = await _keyword_aurora_id(db_session, "heist") out = await movie_service.list_movies( db_session, genre_aurora_id=None, keyword_aurora_id=heist_id, year=None, status=None, sort_by="popularity", sort_dir="desc", page=1, page_size=20, ) assert [m.title for m in out.results] == ["B Action Movie"] @pytest.mark.asyncio async def test_list_movies_rejects_malformed_keyword_filter(db_session: AsyncSession): with pytest.raises(movie_service.InvalidMovieFilterError): await movie_service.list_movies( db_session, genre_aurora_id=None, keyword_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_movies_filters_by_year(db_session: AsyncSession): await _seed_movies(db_session) out = await movie_service.list_movies( db_session, genre_aurora_id=None, keyword_aurora_id=None, year=2020, status=None, sort_by="popularity", sort_dir="desc", page=1, page_size=20, ) assert [m.title for m in out.results] == ["C Comedy Movie", "A Action Movie"] @pytest.mark.asyncio async def test_list_movies_filters_by_status(db_session: AsyncSession): await _seed_movies(db_session) out = await movie_service.list_movies( db_session, genre_aurora_id=None, keyword_aurora_id=None, year=None, status="planned", sort_by="popularity", sort_dir="desc", page=1, page_size=20, ) assert [m.title for m in out.results] == ["D Planned Movie"] @pytest.mark.asyncio async def test_list_movies_sorts_by_title_ascending(db_session: AsyncSession): await _seed_movies(db_session) out = await movie_service.list_movies( db_session, genre_aurora_id=None, keyword_aurora_id=None, year=None, status=None, sort_by="title", sort_dir="asc", page=1, page_size=20, ) assert [m.title for m in out.results] == [ "A Action Movie", "B Action Movie", "C Comedy Movie", "D Planned Movie", ] @pytest.mark.asyncio async def test_list_movies_paginates(db_session: AsyncSession): await _seed_movies(db_session) page1 = await movie_service.list_movies( db_session, genre_aurora_id=None, keyword_aurora_id=None, year=None, status=None, sort_by="popularity", sort_dir="desc", page=1, page_size=2, ) page2 = await movie_service.list_movies( db_session, genre_aurora_id=None, keyword_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 [m.title for m in page1.results] == ["B Action Movie", "C Comedy Movie"] assert [m.title for m in page2.results] == ["D Planned Movie", "A Action Movie"] @pytest.mark.asyncio async def test_list_movies_rejects_malformed_genre_filter(db_session: AsyncSession): with pytest.raises(movie_service.InvalidMovieFilterError): await movie_service.list_movies( db_session, genre_aurora_id="not-an-aurora-id", keyword_aurora_id=None, year=None, status=None, sort_by="popularity", sort_dir="desc", page=1, page_size=20, ) @pytest.mark.asyncio async def test_list_movies_rejects_unknown_status_filter(db_session: AsyncSession): with pytest.raises(movie_service.InvalidMovieFilterError): await movie_service.list_movies( db_session, genre_aurora_id=None, keyword_aurora_id=None, year=None, status="not_a_real_status", sort_by="popularity", sort_dir="desc", page=1, page_size=20, ) @pytest.mark.asyncio async def test_list_movies_returns_empty_page_when_nothing_matches(db_session: AsyncSession): await _seed_movies(db_session) out = await movie_service.list_movies( db_session, genre_aurora_id=None, keyword_aurora_id=None, year=1899, status=None, sort_by="popularity", sort_dir="desc", page=1, page_size=20, ) assert out.results == [] assert out.total_results == 0 assert out.total_pages == 0