from __future__ import annotations from datetime import UTC from datetime import datetime from uuid import uuid4 import pytest from sqlmodel import select from transcription.config import Settings from transcription.db.models import Document from transcription.db.models import DocumentPerson from transcription.db.models import DocumentPersonRole from transcription.db.models import DocumentType from transcription.db.models import Job from transcription.db.models import Person from transcription.db.models import PersonRole from transcription.db.models import Source from transcription.services.documents import DocumentDeleteBlockedError from transcription.services.documents import DocumentError from transcription.services.documents import DocumentService from transcription.services.people import PeopleError from transcription.services.people import PeopleService @pytest.mark.asyncio async def test_read_document_detail_allows_missing_sources(default_session_factory): service = DocumentService(session_factory=default_session_factory) created = await service.create_document( Document( id=uuid4(), name="detail-doc", document_type="letter", ) ) detail = await service.read_document_detail(created.id) assert detail.id == created.id assert detail.sources == [] assert detail.document_type_id is not None assert detail.document_type_ref is not None assert detail.document_type_ref.code == "letter" @pytest.mark.asyncio async def test_update_document_refreshes_updated_timestamp(default_session_factory): service = DocumentService(session_factory=default_session_factory) created = await service.create_document( Document( id=uuid4(), name="timestamp-doc", document_type="letter", updated_at=datetime(2000, 1, 1, tzinfo=UTC), ) ) original_updated_at = created.updated_at created.notes = "updated" updated = await service.update_document(created) assert updated.notes == "updated" assert updated.updated_at >= original_updated_at @pytest.mark.asyncio async def test_delete_document_blocks_when_dependencies_exist(default_session_factory): service = DocumentService(session_factory=default_session_factory) document = await service.create_document( Document( id=uuid4(), name="blocked-delete", document_type="record", ) ) async with service._session_scope() as session: session.add( Source( document_id=document.id, page_number=1, upload_name="001_page.png", filename="001_page.png", file_path="uploads/001_page.png", file_hash="a" * 64, file_size_bytes=1, ) ) session.add(Job(document_id=document.id)) await session.commit() with pytest.raises(DocumentDeleteBlockedError): await service.delete_document(document) @pytest.mark.asyncio async def test_delete_document_succeeds_when_unlinked(default_session_factory, tmp_path): settings = Settings(openrouter_api_key="test-key", upload_dir=tmp_path) service = DocumentService(session_factory=default_session_factory, settings=settings) document = await service.create_document( Document( id=uuid4(), name="free-delete", document_type="memo", ) ) document_dir = service.settings.upload_dir / "documents" / str(document.id) document_dir.mkdir(parents=True, exist_ok=True) (document_dir / "leftover.txt").write_text("orphan", encoding="utf-8") await service.delete_document(document) assert not document_dir.exists() with pytest.raises(DocumentError): await service.read_document_detail(document.id) @pytest.mark.asyncio async def test_delete_document_removes_person_links(default_session_factory, tmp_path): settings = Settings(openrouter_api_key="test-key", upload_dir=tmp_path) service = DocumentService(session_factory=default_session_factory, settings=settings) people_service = PeopleService(session_factory=default_session_factory) document = await service.create_document( Document( id=uuid4(), name="person-linked-delete", document_type="memo", ) ) person = await people_service.create_person(Person(full_name="Linked Person")) await people_service.create_document_person( DocumentPerson( document_id=document.id, person_id=person.id, role=DocumentPersonRole.AUTHOR, ) ) links_before_delete = await people_service.list_document_people(document_id=document.id) assert len(links_before_delete) == 1 assert links_before_delete[0].role_id is not None assert links_before_delete[0].role_ref is not None assert links_before_delete[0].role_ref.code == "author" document_dir = service.settings.upload_dir / "documents" / str(document.id) document_dir.mkdir(parents=True, exist_ok=True) await service.delete_document(document) assert not document_dir.exists() assert await people_service.list_document_people(document_id=document.id) == [] with pytest.raises(DocumentError): await service.read_document_detail(document.id) @pytest.mark.asyncio async def test_delete_document_removes_populated_storage_tree(default_session_factory, tmp_path): settings = Settings(openrouter_api_key="test-key", upload_dir=tmp_path) service = DocumentService(session_factory=default_session_factory, settings=settings) document = await service.create_document( Document( id=uuid4(), name="tree-delete", document_type="memo", ) ) document_dir = service.settings.upload_dir / "documents" / str(document.id) (document_dir / "page-1.jpg").parent.mkdir(parents=True, exist_ok=True) (document_dir / "page-1.jpg").write_bytes(b"one") (document_dir / "page-2.jpg").write_bytes(b"two") (document_dir / "nested" / "manifest.json").parent.mkdir(parents=True, exist_ok=True) (document_dir / "nested" / "manifest.json").write_text('{"ok": true}', encoding="utf-8") assert document_dir.exists() await service.delete_document(document) assert not document_dir.exists() @pytest.mark.asyncio async def test_read_person_detail_loads_document_links(default_session_factory): service = DocumentService(session_factory=default_session_factory) people_service = PeopleService(session_factory=default_session_factory) document = await service.create_document( Document( id=uuid4(), name="linked-doc", document_type="letter", ) ) person = await people_service.create_person(Person(full_name="Linked Person")) await people_service.create_document_person( DocumentPerson( document_id=document.id, person_id=person.id, role=DocumentPersonRole.AUTHOR, ) ) detail = await people_service.read_person_detail(person.id) assert detail.id == person.id assert len(detail.document_people) == 1 assert detail.document_people[0].document is not None assert detail.document_people[0].document.name == "linked-doc" @pytest.mark.asyncio async def test_update_person_refreshes_updated_timestamp(default_session_factory): service = PeopleService(session_factory=default_session_factory) created = await service.create_person( Person( full_name="timestamp-person", updated_at=datetime(2000, 1, 1, tzinfo=UTC), ) ) original_updated_at = created.updated_at created.display_name = "updated" updated = await service.update_person(created) assert updated.display_name == "updated" assert updated.updated_at >= original_updated_at @pytest.mark.asyncio async def test_delete_person_removes_links_when_linked_documents_exist(default_session_factory): documents_service = DocumentService(session_factory=default_session_factory) service = PeopleService(session_factory=default_session_factory) document = await documents_service.create_document( Document( id=uuid4(), name="block-person-delete-doc", document_type="record", ) ) person = await service.create_person(Person(full_name="Blocked Person")) await service.create_document_person( DocumentPerson( document_id=document.id, person_id=person.id, role=DocumentPersonRole.AUTHOR, ) ) await service.delete_person(person) links = await service.list_document_people(person_id=person.id) assert links == [] with pytest.raises(PeopleError): await service.read_person_detail(person.id) @pytest.mark.asyncio async def test_delete_person_succeeds_when_unlinked(default_session_factory): service = PeopleService(session_factory=default_session_factory) person = await service.create_person(Person(full_name="Free Person")) await service.delete_person(person) with pytest.raises(PeopleError): await service.read_person_detail(person.id) @pytest.mark.asyncio async def test_create_document_reuses_existing_document_type_registry(default_session_factory): service = DocumentService(session_factory=default_session_factory) async with service._session_scope() as session: existing = DocumentType(code="record", label="Record") session.add(existing) await session.commit() await session.refresh(existing) created = await service.create_document(Document(id=uuid4(), name="typed-doc", document_type="record")) assert created.document_type_id is not None assert created.document_type_id == existing.id @pytest.mark.asyncio async def test_update_document_person_sets_role_id_from_legacy_role(default_session_factory): documents_service = DocumentService(session_factory=default_session_factory) service = PeopleService(session_factory=default_session_factory) document = await documents_service.create_document( Document(id=uuid4(), name="role-sync-doc", document_type="letter") ) person = await service.create_person(Person(full_name="Role Sync Person")) link = await service.create_document_person( DocumentPerson( document_id=document.id, person_id=person.id, role=DocumentPersonRole.AUTHOR, ) ) updated = await service.update_document_person( DocumentPerson( id=link.id, document_id=document.id, person_id=person.id, role=DocumentPersonRole.RECIPIENT, role_id=None, ) ) assert updated.role == DocumentPersonRole.RECIPIENT assert updated.role_id is not None async with service._session_scope() as session: recipient_role = (await session.exec(select(PersonRole).where(PersonRole.code == "recipient"))).first() assert recipient_role is not None assert updated.role_id == recipient_role.id