import pytest from sqlalchemy import func, select from backend.app.models import ImageRecord, TagKind from backend.app.models.series_page import SeriesPage from backend.app.services.series_service import SeriesError, SeriesService from backend.app.services.tag_service import TagService pytestmark = pytest.mark.integration _SEQ = 0 async def _img(db): global _SEQ _SEQ += 1 rec = ImageRecord( path=f"/tmp/fc_ss_{_SEQ}.png", sha256=f"{_SEQ:064d}", size_bytes=1, mime="image/png", origin="uploaded", ) db.add(rec) await db.flush() return rec.id async def _order(db, sid): rows = ( await db.execute( select(SeriesPage.image_id, SeriesPage.page_number) .where(SeriesPage.series_tag_id == sid) .order_by(SeriesPage.page_number) ) ).all() return [r.image_id for r in rows] @pytest.mark.asyncio async def test_add_appends_in_order(db): s = await TagService(db).find_or_create("S", TagKind.series) i1, i2 = await _img(db), await _img(db) n = await SeriesService(db).add_images(s.id, [i1, i2]) assert n == 2 assert await _order(db, s.id) == [i1, i2] @pytest.mark.asyncio async def test_add_moves_from_other_series(db): svc = SeriesService(db) a = await TagService(db).find_or_create("A", TagKind.series) b = await TagService(db).find_or_create("B", TagKind.series) i1 = await _img(db) await svc.add_images(a.id, [i1]) moved = await svc.add_images(b.id, [i1]) assert moved == 1 assert await _order(db, a.id) == [] assert await _order(db, b.id) == [i1] cnt = await db.scalar( select(func.count()) .select_from(SeriesPage).where(SeriesPage.image_id == i1) ) assert cnt == 1 @pytest.mark.asyncio async def test_add_idempotent_for_same_series(db): s = await TagService(db).find_or_create("S2", TagKind.series) i1 = await _img(db) svc = SeriesService(db) await svc.add_images(s.id, [i1]) again = await svc.add_images(s.id, [i1]) assert again == 0 assert await _order(db, s.id) == [i1] @pytest.mark.asyncio async def test_remove(db): s = await TagService(db).find_or_create("S3", TagKind.series) i1, i2 = await _img(db), await _img(db) svc = SeriesService(db) await svc.add_images(s.id, [i1, i2]) removed = await svc.remove_images(s.id, [i1]) assert removed == 1 assert await _order(db, s.id) == [i2] @pytest.mark.asyncio async def test_set_page_number_sparse_with_gaps(db): s = await TagService(db).find_or_create("S4", TagKind.series) i1, i2, i3 = await _img(db), await _img(db), await _img(db) svc = SeriesService(db) await svc.add_images(s.id, [i1, i2, i3]) # numbered 1, 2, 3 # set a sparse number, leaving a gap await svc.set_page_number(s.id, i3, 5) out = await svc.list_pages(s.id) assert [p["page_number"] for p in out["pages"]] == [1, 2, 5] assert len(out["gaps"]) == 1 assert out["gaps"][0]["from"] == 3 assert out["gaps"][0]["to"] == 4 # unnumber a page → it sorts to the tail await svc.set_page_number(s.id, i1, None) out = await svc.list_pages(s.id) assert [p["page_number"] for p in out["pages"]] == [2, 5, None] with pytest.raises(SeriesError): await svc.set_page_number(s.id, 99999, 1) @pytest.mark.asyncio async def test_set_cover_makes_page_one_preserving_rest(db): s = await TagService(db).find_or_create("S5", TagKind.series) i1, i2, i3 = await _img(db), await _img(db), await _img(db) svc = SeriesService(db) await svc.add_images(s.id, [i1, i2, i3]) await svc.set_cover(s.id, i3) assert await _order(db, s.id) == [i3, i1, i2] @pytest.mark.asyncio async def test_list_pages_shape_and_guards(db): svc = SeriesService(db) s = await TagService(db).find_or_create("S6", TagKind.series) i1 = await _img(db) await svc.add_images(s.id, [i1]) out = await svc.list_pages(s.id) assert out["series"]["id"] == s.id assert out["pages"][0]["image_id"] == i1 assert out["pages"][0]["page_number"] == 1 assert out["pages"][0]["thumbnail_url"] assert out["pages"][0]["image_url"].startswith("/images/") g = await TagService(db).find_or_create("notser", TagKind.general) with pytest.raises(SeriesError): await svc.list_pages(g.id) with pytest.raises(SeriesError): await svc.add_images(g.id, [i1]) with pytest.raises(SeriesError): await svc.add_images(s.id, list(range(1, 502)))