"""Shared fixtures for UI integration tests.""" from __future__ import annotations import asyncio from collections.abc import Callable from collections.abc import Generator from datetime import UTC from datetime import datetime from pathlib import Path from uuid import UUID import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from sqlmodel import delete from transcription.app import create_app from transcription.config import Settings from transcription.config import SqliteSettings from transcription.db import create_all from transcription.db import initialize_database_runtime from transcription.db import session_scope from transcription.db.models import Document from transcription.db.models import DocumentPerson from transcription.db.models import Job from transcription.db.models import JobSource from transcription.db.models import JobSourceStatus from transcription.db.models import JobStatus from transcription.db.models import Person from transcription.db.models import Source RevisionSeed = str @pytest.fixture(scope="session") def app_client(tmp_path_factory: pytest.TempPathFactory) -> Generator[tuple[FastAPI, TestClient]]: """Provide a real application and test client backed by in-memory SQLite.""" tmp_path = tmp_path_factory.mktemp("ui") settings = Settings( openrouter_api_key="test-key", database=SqliteSettings(path=":memory:"), environment="test", bootstrap_schema_on_startup=True, upload_dir=tmp_path / "uploads", prompt_dir=tmp_path / "prompts", ) app = create_app() app.state.runtime = initialize_database_runtime(settings=settings) asyncio.run(create_all(engine=app.state.runtime.engine)) with TestClient(app) as client: yield app, client @pytest.fixture(autouse=True) def clear_ui_database(app_client: tuple[FastAPI, TestClient]) -> None: """Reset UI-facing tables before each test for isolation.""" app, _ = app_client async def _clear() -> None: async with session_scope() as session: await session.exec(delete(JobSource)) await session.exec(delete(DocumentPerson)) await session.exec(delete(Source)) await session.exec(delete(Job)) await session.exec(delete(Document)) await session.exec(delete(Person)) await session.commit() asyncio.run(_clear()) @pytest.fixture def seed_job(app_client: tuple[FastAPI, TestClient]) -> Callable[..., UUID]: """Return a helper for inserting a document/job/source/(optional revision) tuple.""" app, _ = app_client fixtures_dir = Path(__file__).resolve().parents[1] / "fixtures" / "images" / "valid" def _seed( *, filename: str = "sample.pdf", status: JobStatus = JobStatus.TRANSCRIBED, transcription_text: str | None = "Sample transcript text", error_detail: str | None = None, revision_text: RevisionSeed | None = None, source_file: Path | None = None, ) -> UUID: async def _insert() -> UUID: async with session_scope() as session: stored_path = app.state.settings.upload_dir / filename stored_path.parent.mkdir(parents=True, exist_ok=True) source_path = source_file or fixtures_dir / "small_png.png" stored_path.write_bytes(source_path.read_bytes()) document = Document(name=filename) session.add(document) await session.flush() job = Job( document_id=document.id, status=status, retry_count=0, provider="openrouter", model="google/gemini-2.5-flash", prompt_name="transcribe_document.md", ) session.add(job) await session.flush() source = Source( document_id=document.id, upload_name=filename, filename=filename, file_path=str(stored_path), ) session.add(source) await session.flush() if transcription_text is not None or error_detail is not None: session.add( JobSource( job_id=job.id, source_id=source.id, status=JobSourceStatus.TRANSCRIBED if transcription_text is not None else JobSourceStatus.FAILED, raw_transcription=transcription_text, error_detail=error_detail, ) ) if revision_text is not None: source.revised_text = revision_text source.date_revised = datetime.now(UTC) session.add(source) await session.commit() return job.id return asyncio.run(_insert()) return _seed