"""FC-6.1 — chapter layer over series_page.""" import pytest from sqlalchemy import func, select from backend.app.models import ImageRecord, TagKind from backend.app.models.series_chapter import SeriesChapter 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_sc_{_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 _series(db, name): return (await TagService(db).find_or_create(name, TagKind.series)).id async def _chapter_numbers(db, sid): rows = ( await db.execute( select(SeriesChapter.id, SeriesChapter.chapter_number) .where(SeriesChapter.series_tag_id == sid) .order_by(SeriesChapter.chapter_number) ) ).all() return [(r.id, r.chapter_number) for r in rows] async def _page_order(db, chapter_id): rows = ( await db.execute( select(SeriesPage.image_id) .where(SeriesPage.chapter_id == chapter_id) .order_by(SeriesPage.page_number) ) ).scalars().all() return list(rows) @pytest.mark.asyncio async def test_add_images_creates_default_chapter(db): svc = SeriesService(db) sid = await _series(db, "C1") i1, i2 = await _img(db), await _img(db) await svc.add_images(sid, [i1, i2]) chapters = await _chapter_numbers(db, sid) assert len(chapters) == 1 assert chapters[0][1] == 1 assert await _page_order(db, chapters[0][0]) == [i1, i2] @pytest.mark.asyncio async def test_create_and_target_specific_chapter(db): svc = SeriesService(db) sid = await _series(db, "C2") i1, i2 = await _img(db), await _img(db) await svc.add_images(sid, [i1]) # default chapter 1 ch2 = await svc.create_chapter(sid, title="Two") assert ch2["chapter_number"] == 2 await svc.add_images(sid, [i2], chapter_id=ch2["id"]) assert await _page_order(db, ch2["id"]) == [i2] # i1 stayed in chapter 1. chapters = await _chapter_numbers(db, sid) assert len(chapters) == 2 @pytest.mark.asyncio async def test_reorder_chapters(db): svc = SeriesService(db) sid = await _series(db, "C3") c1 = (await svc.create_chapter(sid))["id"] c2 = (await svc.create_chapter(sid))["id"] c3 = (await svc.create_chapter(sid))["id"] await svc.reorder_chapters(sid, [c3, c1, c2]) assert await _chapter_numbers(db, sid) == [(c3, 1), (c1, 2), (c2, 3)] with pytest.raises(SeriesError): await svc.reorder_chapters(sid, [c1, c2]) @pytest.mark.asyncio async def test_reorder_pages_within_chapter(db): svc = SeriesService(db) sid = await _series(db, "C4") i1, i2, i3 = await _img(db), await _img(db), await _img(db) await svc.add_images(sid, [i1, i2, i3]) cid = (await _chapter_numbers(db, sid))[0][0] await svc.reorder_pages(sid, cid, [i3, i1, i2]) assert await _page_order(db, cid) == [i3, i1, i2] @pytest.mark.asyncio async def test_legacy_reorder_rejects_multi_chapter(db): svc = SeriesService(db) sid = await _series(db, "C5") i1 = await _img(db) await svc.add_images(sid, [i1]) await svc.create_chapter(sid) # now 2 chapters with pytest.raises(SeriesError): await svc.reorder(sid, [i1]) @pytest.mark.asyncio async def test_merge_chapter_moves_pages_and_renumbers(db): svc = SeriesService(db) sid = await _series(db, "C6") i1, i2, i3 = await _img(db), await _img(db), await _img(db) await svc.add_images(sid, [i1]) # chapter 1 c2 = (await svc.create_chapter(sid))["id"] await svc.add_images(sid, [i2, i3], chapter_id=c2) c1 = (await _chapter_numbers(db, sid))[0][0] moved = await svc.merge_chapter(sid, c2, c1) assert moved == 2 assert await _page_order(db, c1) == [i1, i2, i3] # source chapter gone; remaining renumbered 1..N. assert await _chapter_numbers(db, sid) == [(c1, 1)] @pytest.mark.asyncio async def test_delete_chapter_cascades_pages_and_renumbers(db): svc = SeriesService(db) sid = await _series(db, "C7") i1, i2 = await _img(db), await _img(db) await svc.add_images(sid, [i1]) c2 = (await svc.create_chapter(sid))["id"] await svc.add_images(sid, [i2], chapter_id=c2) await svc.delete_chapter(sid, c2) assert [c for _, c in await _chapter_numbers(db, sid)] == [1] # i2's page cascaded away with its chapter. cnt = await db.scalar( select(func.count()).select_from(SeriesPage) .where(SeriesPage.image_id == i2) ) assert cnt == 0 @pytest.mark.asyncio async def test_set_cover_brings_chapter_and_page_to_front(db): svc = SeriesService(db) sid = await _series(db, "C8") i1, i2, i3 = await _img(db), await _img(db), await _img(db) await svc.add_images(sid, [i1]) # chapter 1 c2 = (await svc.create_chapter(sid))["id"] await svc.add_images(sid, [i2, i3], chapter_id=c2) await svc.set_cover(sid, i3) chapters = await _chapter_numbers(db, sid) assert chapters[0][0] == c2 # i3's chapter is now first assert (await _page_order(db, c2))[0] == i3 # i3 first in its chapter @pytest.mark.asyncio async def test_list_pages_flags_gaps_from_stated_ranges(db): svc = SeriesService(db) sid = await _series(db, "C9") await svc.create_chapter(sid, stated_page_start=1, stated_page_end=4) await svc.create_chapter(sid, stated_page_start=9, stated_page_end=12) out = await svc.list_pages(sid) assert len(out["chapters"]) == 2 assert len(out["gaps"]) == 1 assert out["gaps"][0]["start"] == 5 assert out["gaps"][0]["end"] == 8 @pytest.mark.asyncio async def test_add_with_stated_pages_records_them(db): svc = SeriesService(db) sid = await _series(db, "C10") i1, i2 = await _img(db), await _img(db) await svc.add_images(sid, [i1, i2], stated_pages={i1: 9, i2: 10}) rows = dict( ( await db.execute( select(SeriesPage.image_id, SeriesPage.stated_page) .where(SeriesPage.series_tag_id == sid) ) ).all() ) assert rows[i1] == 9 assert rows[i2] == 10