package org.raddatz.familienarchiv.person;

import org.junit.jupiter.api.Test;
import org.raddatz.familienarchiv.PostgresContainerConfig;
import org.raddatz.familienarchiv.config.FlywayConfig;
import org.raddatz.familienarchiv.document.Document;
import org.raddatz.familienarchiv.document.DocumentStatus;
import org.raddatz.familienarchiv.person.Person;
import org.raddatz.familienarchiv.person.PersonNameAlias;
import org.raddatz.familienarchiv.person.PersonNameAliasType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
import org.springframework.context.annotation.Import;

import org.raddatz.familienarchiv.person.PersonSummaryDTO;
import org.raddatz.familienarchiv.document.DocumentRepository;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import java.util.List;
import java.util.Optional;
import java.util.Set;

import static org.assertj.core.api.Assertions.assertThat;

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import({PostgresContainerConfig.class, FlywayConfig.class})
class PersonRepositoryTest {

    @Autowired
    private PersonRepository personRepository;

    @Autowired
    private PersonNameAliasRepository aliasRepository;

    @Autowired
    private DocumentRepository documentRepository;

    @PersistenceContext
    private EntityManager entityManager;

    // ─── save and findById ────────────────────────────────────────────────────

    @Test
    void save_persistsPerson_andFindByIdReturnsSamePerson() {
        Person person = Person.builder()
                .firstName("Anna")
                .lastName("Schmidt")
                .build();

        Person saved = personRepository.save(person);
        Optional<Person> found = personRepository.findById(saved.getId());

        assertThat(found).isPresent();
        assertThat(found.get().getFirstName()).isEqualTo("Anna");
        assertThat(found.get().getLastName()).isEqualTo("Schmidt");
    }

    // ─── searchByName ─────────────────────────────────────────────────────────

    @Test
    void searchByName_findsByFirstName() {
        personRepository.save(Person.builder().firstName("Hans").lastName("Müller").build());
        personRepository.save(Person.builder().firstName("Anna").lastName("Schmidt").build());

        List<Person> results = personRepository.searchByName("Hans");

        assertThat(results).hasSize(1);
        assertThat(results.get(0).getFirstName()).isEqualTo("Hans");
    }

    @Test
    void searchByName_findsByLastName() {
        personRepository.save(Person.builder().firstName("Hans").lastName("Müller").build());
        personRepository.save(Person.builder().firstName("Anna").lastName("Schmidt").build());

        List<Person> results = personRepository.searchByName("Schmidt");

        assertThat(results).hasSize(1);
        assertThat(results.get(0).getLastName()).isEqualTo("Schmidt");
    }

    @Test
    void searchByName_isCaseInsensitive() {
        personRepository.save(Person.builder().firstName("Hans").lastName("Müller").build());

        List<Person> results = personRepository.searchByName("hans");

        assertThat(results).hasSize(1);
    }

    @Test
    void searchByName_findsByAlias() {
        personRepository.save(Person.builder()
                .firstName("Hans").lastName("Müller").alias("Opa Hans").build());

        List<Person> results = personRepository.searchByName("Opa Hans");

        assertThat(results).hasSize(1);
    }

    // ─── findAllByOrderByLastNameAscFirstNameAsc ──────────────────────────────

    @Test
    void findAllByOrderByLastNameAscFirstNameAsc_returnsSortedByLastNameThenFirstName() {
        personRepository.save(Person.builder().firstName("Bernd").lastName("Ziegler").build());
        personRepository.save(Person.builder().firstName("Anna").lastName("Müller").build());
        personRepository.save(Person.builder().firstName("Clara").lastName("Müller").build());

        List<Person> sorted = personRepository.findAllByOrderByLastNameAscFirstNameAsc();

        assertThat(sorted).extracting(Person::getLastName)
                .startsWith("Müller", "Müller");
        assertThat(sorted.stream()
                .filter(p -> p.getLastName().equals("Müller"))
                .map(Person::getFirstName)
                .toList())
                .containsExactly("Anna", "Clara");
    }

    // ─── findByAliasIgnoreCase ────────────────────────────────────────────────

    @Test
    void findByAliasIgnoreCase_returnsMatchingPerson() {
        personRepository.save(Person.builder()
                .firstName("Karl").lastName("Brandt").alias("Opa Karl").build());

        Optional<Person> found = personRepository.findByAliasIgnoreCase("opa karl");

        assertThat(found).isPresent();
        assertThat(found.get().getFirstName()).isEqualTo("Karl");
    }

    @Test
    void findByAliasIgnoreCase_returnsEmpty_whenAliasDoesNotMatch() {
        Optional<Person> found = personRepository.findByAliasIgnoreCase("nobody");

        assertThat(found).isEmpty();
    }

    // ─── findByFirstNameIgnoreCaseAndLastNameIgnoreCase ───────────────────────

    @Test
    void findByFirstNameIgnoreCaseAndLastNameIgnoreCase_returnsMatch() {
        personRepository.save(Person.builder().firstName("Maria").lastName("Raddatz").build());

        Optional<Person> found = personRepository.findByFirstNameIgnoreCaseAndLastNameIgnoreCase(
                "maria", "raddatz");

        assertThat(found).isPresent();
        assertThat(found.get().getFirstName()).isEqualTo("Maria");
    }

    // ─── findCorrespondents ───────────────────────────────────────────────────

    @Test
    void findCorrespondents_returnsPersonsWhoSharedDocumentsWith() {
        Person walter = personRepository.save(Person.builder().firstName("Walter").lastName("Müller").build());
        Person anna   = personRepository.save(Person.builder().firstName("Anna").lastName("Schmidt").build());
        Person clara  = personRepository.save(Person.builder().firstName("Clara").lastName("Cram").build());

        // Walter sends to Anna (1 document)
        documentRepository.save(Document.builder()
                .title("Brief 1").originalFilename("brief1.pdf")
                .status(DocumentStatus.UPLOADED)
                .sender(walter).receivers(Set.of(anna)).build());

        // Walter sends to Clara (2 documents — Clara should rank higher)
        documentRepository.save(Document.builder()
                .title("Brief 2").originalFilename("brief2.pdf")
                .status(DocumentStatus.UPLOADED)
                .sender(walter).receivers(Set.of(clara)).build());
        documentRepository.save(Document.builder()
                .title("Brief 3").originalFilename("brief3.pdf")
                .status(DocumentStatus.UPLOADED)
                .sender(walter).receivers(Set.of(clara)).build());

        List<Person> correspondents = personRepository.findCorrespondents(walter.getId());

        assertThat(correspondents).extracting(Person::getFirstName)
                .containsExactly("Clara", "Anna"); // Clara ranks first (2 documents)
    }

    @Test
    void findCorrespondents_returnsEmpty_whenPersonHasNoDocuments() {
        Person solo = personRepository.save(Person.builder().firstName("Solo").lastName("Mensch").build());

        List<Person> correspondents = personRepository.findCorrespondents(solo.getId());

        assertThat(correspondents).isEmpty();
    }

    // ─── findCorrespondentsWithFilter ─────────────────────────────────────────

    @Test
    void findCorrespondentsWithFilter_returnsOnlyMatchingCorrespondents() {
        Person walter = personRepository.save(Person.builder().firstName("Walter").lastName("Müller").build());
        Person anna   = personRepository.save(Person.builder().firstName("Anna").lastName("Schmidt").build());
        Person bernd  = personRepository.save(Person.builder().firstName("Bernd").lastName("Braun").build());

        documentRepository.save(Document.builder()
                .title("Brief an Anna").originalFilename("anna.pdf")
                .status(DocumentStatus.UPLOADED)
                .sender(walter).receivers(Set.of(anna)).build());
        documentRepository.save(Document.builder()
                .title("Brief an Bernd").originalFilename("bernd.pdf")
                .status(DocumentStatus.UPLOADED)
                .sender(walter).receivers(Set.of(bernd)).build());

        List<Person> filtered = personRepository.findCorrespondentsWithFilter(walter.getId(), "Anna");

        assertThat(filtered).extracting(Person::getFirstName).containsExactly("Anna");
    }

    @Test
    void findCorrespondentsWithFilter_isCaseInsensitive() {
        Person walter = personRepository.save(Person.builder().firstName("Walter").lastName("Müller").build());
        Person anna   = personRepository.save(Person.builder().firstName("Anna").lastName("Schmidt").build());

        documentRepository.save(Document.builder()
                .title("Brief").originalFilename("brief.pdf")
                .status(DocumentStatus.UPLOADED)
                .sender(walter).receivers(Set.of(anna)).build());

        List<Person> filtered = personRepository.findCorrespondentsWithFilter(walter.getId(), "schmidt");

        assertThat(filtered).hasSize(1);
        assertThat(filtered.get(0).getLastName()).isEqualTo("Schmidt");
    }

    // ─── reassignSender ───────────────────────────────────────────────────────

    @Test
    void reassignSender_updatesDocumentsSenderFromSourceToTarget() {
        Person source = personRepository.save(Person.builder().firstName("Alt").lastName("Person").build());
        Person target = personRepository.save(Person.builder().firstName("Neu").lastName("Person").build());

        documentRepository.save(Document.builder()
                .title("Brief").originalFilename("brief.pdf")
                .status(DocumentStatus.UPLOADED)
                .sender(source).build());

        personRepository.reassignSender(source.getId(), target.getId());
        entityManager.flush();
        entityManager.clear();

        List<Document> docs = documentRepository.findBySenderId(target.getId());
        assertThat(docs).hasSize(1);
        assertThat(documentRepository.findBySenderId(source.getId())).isEmpty();
    }

    // ─── insertMissingReceiverReference ──────────────────────────────────────

    @Test
    void insertMissingReceiverReference_addsTargetWhereSourceWasReceiver() {
        Person source = personRepository.save(Person.builder().firstName("Alt").lastName("Person").build());
        Person target = personRepository.save(Person.builder().firstName("Neu").lastName("Person").build());
        Person sender = personRepository.save(Person.builder().firstName("Send").lastName("Er").build());

        Document doc = documentRepository.save(Document.builder()
                .title("Brief").originalFilename("brief.pdf")
                .status(DocumentStatus.UPLOADED)
                .sender(sender).receivers(Set.of(source)).build());

        personRepository.insertMissingReceiverReference(source.getId(), target.getId());
        entityManager.flush();
        entityManager.clear();

        Document reloaded = documentRepository.findById(doc.getId()).orElseThrow();
        assertThat(reloaded.getReceivers())
                .extracting(Person::getId)
                .contains(target.getId());
    }

    @Test
    void insertMissingReceiverReference_doesNotCreateDuplicate_whenTargetAlreadyReceiver() {
        Person source = personRepository.save(Person.builder().firstName("Alt").lastName("Person").build());
        Person target = personRepository.save(Person.builder().firstName("Neu").lastName("Person").build());
        Person sender = personRepository.save(Person.builder().firstName("Send").lastName("Er").build());

        // target is already a receiver together with source
        Document doc = documentRepository.save(Document.builder()
                .title("Brief").originalFilename("brief.pdf")
                .status(DocumentStatus.UPLOADED)
                .sender(sender).receivers(Set.of(source, target)).build());

        personRepository.insertMissingReceiverReference(source.getId(), target.getId());
        entityManager.flush();
        entityManager.clear();

        Document reloaded = documentRepository.findById(doc.getId()).orElseThrow();
        long targetCount = reloaded.getReceivers().stream()
                .filter(p -> p.getId().equals(target.getId())).count();
        assertThat(targetCount).isEqualTo(1); // no duplicate
    }

    // ─── Phase 3.2: findAllWithDocumentCount ──────────────────────────────────

    @Test
    void findAllWithDocumentCount_includesDocumentCountAsSenderAndReceiver() {
        Person walter = personRepository.save(Person.builder().firstName("Walter").lastName("Müller").build());
        Person anna   = personRepository.save(Person.builder().firstName("Anna").lastName("Schmidt").build());

        // Walter sends 2 docs to Anna (Anna receives 2)
        documentRepository.save(Document.builder()
                .title("Brief 1").originalFilename("b1.pdf")
                .status(DocumentStatus.UPLOADED)
                .sender(walter).receivers(Set.of(anna)).build());
        documentRepository.save(Document.builder()
                .title("Brief 2").originalFilename("b2.pdf")
                .status(DocumentStatus.UPLOADED)
                .sender(walter).receivers(Set.of(anna)).build());
        // Anna also sends 1 doc to Walter
        documentRepository.save(Document.builder()
                .title("Brief 3").originalFilename("b3.pdf")
                .status(DocumentStatus.UPLOADED)
                .sender(anna).receivers(Set.of(walter)).build());

        List<PersonSummaryDTO> result = personRepository.findAllWithDocumentCount();

        PersonSummaryDTO walterSummary = result.stream()
                .filter(p -> p.getId().equals(walter.getId())).findFirst().orElseThrow();
        PersonSummaryDTO annaSummary = result.stream()
                .filter(p -> p.getId().equals(anna.getId())).findFirst().orElseThrow();

        assertThat(walterSummary.getDocumentCount()).isEqualTo(3); // sent 2, received 1
        assertThat(annaSummary.getDocumentCount()).isEqualTo(3);   // sent 1, received 2
    }

    @Test
    void findAllWithDocumentCount_returnsZero_whenPersonHasNoDocuments() {
        Person solo = personRepository.save(Person.builder().firstName("Solo").lastName("Mensch").build());

        List<PersonSummaryDTO> result = personRepository.findAllWithDocumentCount();

        PersonSummaryDTO soloSummary = result.stream()
                .filter(p -> p.getId().equals(solo.getId())).findFirst().orElseThrow();
        assertThat(soloSummary.getDocumentCount()).isEqualTo(0);
    }

    @Test
    void searchWithDocumentCount_filtersAndIncludesCount() {
        Person hans = personRepository.save(Person.builder().firstName("Hans").lastName("Müller").build());
        Person anna = personRepository.save(Person.builder().firstName("Anna").lastName("Schmidt").build());

        documentRepository.save(Document.builder()
                .title("Brief").originalFilename("brief.pdf")
                .status(DocumentStatus.UPLOADED)
                .sender(hans).receivers(Set.of(anna)).build());

        List<PersonSummaryDTO> result = personRepository.searchWithDocumentCount("Hans");

        assertThat(result).hasSize(1);
        assertThat(result.get(0).getFirstName()).isEqualTo("Hans");
        assertThat(result.get(0).getDocumentCount()).isEqualTo(1);
    }

    @Test
    void searchWithDocumentCount_isCaseInsensitive() {
        personRepository.save(Person.builder().firstName("Hans").lastName("Müller").build());

        List<PersonSummaryDTO> result = personRepository.searchWithDocumentCount("hans");

        assertThat(result).hasSize(1);
    }

    // ─── deleteReceiverReferences ─────────────────────────────────────────────

    @Test
    void deleteReceiverReferences_removesPersonFromAllDocumentReceivers() {
        Person toDelete = personRepository.save(Person.builder().firstName("Weg").lastName("Person").build());
        Person sender   = personRepository.save(Person.builder().firstName("Send").lastName("Er").build());

        Document doc1 = documentRepository.save(Document.builder()
                .title("Brief 1").originalFilename("b1.pdf")
                .status(DocumentStatus.UPLOADED)
                .sender(sender).receivers(Set.of(toDelete)).build());
        Document doc2 = documentRepository.save(Document.builder()
                .title("Brief 2").originalFilename("b2.pdf")
                .status(DocumentStatus.UPLOADED)
                .sender(sender).receivers(Set.of(toDelete)).build());

        personRepository.deleteReceiverReferences(toDelete.getId());
        entityManager.flush();
        entityManager.clear();

        assertThat(documentRepository.findById(doc1.getId()).orElseThrow().getReceivers()).isEmpty();
        assertThat(documentRepository.findById(doc2.getId()).orElseThrow().getReceivers()).isEmpty();
    }

    // ─── searchByName with aliases ───────────────────────────────────────────

    @Test
    void searchByName_findsByAliasLastName() {
        Person clara = personRepository.save(Person.builder().firstName("Clara").lastName("Cram").build());
        aliasRepository.save(PersonNameAlias.builder()
                .person(clara).lastName("de Gruyter").type(PersonNameAliasType.BIRTH).sortOrder(0).build());

        List<Person> results = personRepository.searchByName("de Gruyter");

        assertThat(results).hasSize(1);
        assertThat(results.get(0).getLastName()).isEqualTo("Cram");
    }

    @Test
    void searchByName_stillFindsByCurrentLastName_afterAliasAdded() {
        Person clara = personRepository.save(Person.builder().firstName("Clara").lastName("Cram").build());
        aliasRepository.save(PersonNameAlias.builder()
                .person(clara).lastName("de Gruyter").type(PersonNameAliasType.BIRTH).sortOrder(0).build());

        List<Person> results = personRepository.searchByName("Cram");

        assertThat(results).hasSize(1);
    }

    @Test
    void searchByName_doesNotReturnDuplicates_whenMultipleAliasesMatch() {
        Person clara = personRepository.save(Person.builder().firstName("Clara").lastName("Cram").build());
        aliasRepository.save(PersonNameAlias.builder()
                .person(clara).lastName("de Gruyter").type(PersonNameAliasType.BIRTH).sortOrder(0).build());
        aliasRepository.save(PersonNameAlias.builder()
                .person(clara).lastName("Gruyter-Cram").type(PersonNameAliasType.OTHER).sortOrder(1).build());

        List<Person> results = personRepository.searchByName("Gruyter");

        assertThat(results).hasSize(1);
    }

    // ─── searchWithDocumentCount with aliases ────────────────────────────────

    @Test
    void searchWithDocumentCount_findsByAliasLastName() {
        Person clara = personRepository.save(Person.builder().firstName("Clara").lastName("Cram").build());
        aliasRepository.save(PersonNameAlias.builder()
                .person(clara).lastName("de Gruyter").type(PersonNameAliasType.BIRTH).sortOrder(0).build());

        List<PersonSummaryDTO> results = personRepository.searchWithDocumentCount("de Gruyter");

        assertThat(results).hasSize(1);
        assertThat(results.get(0).getLastName()).isEqualTo("Cram");
    }

    // ─── null firstName handling ────────────────────────────────────────────

    @Test
    void searchByName_findsPersonWithNullFirstName() {
        personRepository.save(Person.builder().lastName("Gesellschafter des Verlages").build());

        List<Person> result = personRepository.searchByName("Gesellschafter");

        assertThat(result).hasSize(1);
        assertThat(result.get(0).getLastName()).isEqualTo("Gesellschafter des Verlages");
    }

    @Test
    void searchWithDocumentCount_findsPersonWithNullFirstName() {
        personRepository.save(Person.builder().lastName("Gesellschafter des Verlages").build());

        List<PersonSummaryDTO> result = personRepository.searchWithDocumentCount("Gesellschafter");

        assertThat(result).hasSize(1);
        assertThat(result.get(0).getLastName()).isEqualTo("Gesellschafter des Verlages");
    }

    // ─── #671: provisional must be SELECTed in all three native projections ───
    // Adding isProvisional() to the interface compiles even if a native query forgets
    // to SELECT p.provisional — it then silently returns false. These tests are the only
    // guard against that trap, so they must run against real Postgres.

    @Test
    void findAllWithDocumentCount_projectsProvisionalTrue() {
        personRepository.save(Person.builder()
                .firstName("Inferred").lastName("Person").provisional(true).build());

        List<PersonSummaryDTO> result = personRepository.findAllWithDocumentCount();

        assertThat(result).anyMatch(PersonSummaryDTO::isProvisional);
    }

    @Test
    void searchWithDocumentCount_projectsProvisionalTrue() {
        personRepository.save(Person.builder()
                .firstName("Provisorisch").lastName("Müller").provisional(true).build());

        List<PersonSummaryDTO> result = personRepository.searchWithDocumentCount("Provisorisch");

        assertThat(result).hasSize(1);
        assertThat(result.get(0).isProvisional()).isTrue();
    }

    @Test
    void findTopByDocumentCount_projectsProvisionalTrue() {
        Person provisional = personRepository.save(Person.builder()
                .firstName("Top").lastName("Provisional").provisional(true).build());
        documentRepository.save(Document.builder()
                .title("Brief").originalFilename("b.pdf")
                .status(DocumentStatus.UPLOADED)
                .sender(provisional).build());

        List<PersonSummaryDTO> result = personRepository.findTopByDocumentCount(10);

        PersonSummaryDTO summary = result.stream()
                .filter(p -> p.getId().equals(provisional.getId())).findFirst().orElseThrow();
        assertThat(summary.isProvisional()).isTrue();
    }

    // ─── #667: filter-aware paged slice + paired COUNT (Postgres-only) ────────
    // The slice query (findByFilter) and the count query (countByFilter) MUST share one
    // WHERE clause so totalElements can never drift from the rendered page. These tests run
    // against real Postgres because the slice ORDER BY uses a computed alias that fails on H2.

    private void seedDirectoryFixture() {
        // Register family member, no documents — visible by reader default (familyMember)
        personRepository.save(Person.builder().firstName("Karl").lastName("Register").familyMember(true).build());
        // Person with one document — visible by reader default (documentCount > 0)
        Person hasDoc = personRepository.save(Person.builder().firstName("Doku").lastName("Person").build());
        documentRepository.save(Document.builder().title("B").originalFilename("b.pdf")
                .status(DocumentStatus.UPLOADED).sender(hasDoc).build());
        // Provisional, zero-document, non-family — hidden by reader default
        personRepository.save(Person.builder().firstName("Unbe").lastName("Staetigt").provisional(true).build());
        // An institution with no documents, non-family, non-provisional
        personRepository.save(Person.builder().lastName("Verlag GmbH").personType(PersonType.INSTITUTION).build());
    }

    @Test
    void findByFilter_readerDefault_returnsOnlyFamilyOrWithDocuments() {
        seedDirectoryFixture();

        List<PersonSummaryDTO> slice = personRepository.findByFilter(
                null, null, null, null, true, null, 50, 0);

        assertThat(slice).extracting(PersonSummaryDTO::getLastName)
                .containsExactlyInAnyOrder("Register", "Person");
    }

    @Test
    void countByFilter_readerDefault_matchesSliceSize() {
        seedDirectoryFixture();

        long count = personRepository.countByFilter(null, null, null, null, true, null);

        assertThat(count).isEqualTo(2);
    }

    @Test
    void findByFilter_showAll_returnsEveryone() {
        seedDirectoryFixture();

        List<PersonSummaryDTO> slice = personRepository.findByFilter(
                null, null, null, null, false, null, 50, 0);

        assertThat(slice).hasSize(4);
    }

    @Test
    void findByFilter_typeInstitution_returnsOnlyInstitutions() {
        seedDirectoryFixture();

        List<PersonSummaryDTO> slice = personRepository.findByFilter(
                "INSTITUTION", null, null, null, false, null, 50, 0);

        assertThat(slice).extracting(PersonSummaryDTO::getLastName).containsExactly("Verlag GmbH");
    }

    @Test
    void findByFilter_familyOnly_returnsOnlyFamilyMembers() {
        seedDirectoryFixture();

        List<PersonSummaryDTO> slice = personRepository.findByFilter(
                null, true, null, null, false, null, 50, 0);

        assertThat(slice).extracting(PersonSummaryDTO::getLastName).containsExactly("Register");
    }

    @Test
    void findByFilter_hasDocuments_returnsOnlyPersonsWithDocuments() {
        seedDirectoryFixture();

        List<PersonSummaryDTO> slice = personRepository.findByFilter(
                null, null, true, null, false, null, 50, 0);

        assertThat(slice).extracting(PersonSummaryDTO::getLastName).containsExactly("Person");
    }

    @Test
    void findByFilter_provisionalTrue_returnsOnlyProvisional() {
        seedDirectoryFixture();

        List<PersonSummaryDTO> slice = personRepository.findByFilter(
                null, null, null, true, false, null, 50, 0);

        assertThat(slice).extracting(PersonSummaryDTO::getLastName).containsExactly("Staetigt");
    }

    @Test
    void findByFilter_combinedFilters_andTogether() {
        seedDirectoryFixture();
        // family + has-documents → intersection is empty (Register has no docs, Doku is not family)
        List<PersonSummaryDTO> slice = personRepository.findByFilter(
                null, true, true, null, false, null, 50, 0);

        assertThat(slice).isEmpty();
    }

    @Test
    void findByFilter_query_combinesWithFilters() {
        seedDirectoryFixture();

        List<PersonSummaryDTO> slice = personRepository.findByFilter(
                null, null, null, null, false, "Verlag", 50, 0);

        assertThat(slice).extracting(PersonSummaryDTO::getLastName).containsExactly("Verlag GmbH");
    }

    @Test
    void findByFilter_pageBeyondRange_returnsEmptySlice() {
        seedDirectoryFixture();

        List<PersonSummaryDTO> slice = personRepository.findByFilter(
                null, null, null, null, false, null, 50, 999 * 50);

        assertThat(slice).isEmpty();
    }

    @Test
    void findByFilter_respectsPageSize() {
        seedDirectoryFixture();

        List<PersonSummaryDTO> firstPage = personRepository.findByFilter(
                null, null, null, null, false, null, 2, 0);
        List<PersonSummaryDTO> secondPage = personRepository.findByFilter(
                null, null, null, null, false, null, 2, 2);

        assertThat(firstPage).hasSize(2);
        assertThat(secondPage).hasSize(2);
        assertThat(firstPage).extracting(PersonSummaryDTO::getId)
                .doesNotContainAnyElementsOf(secondPage.stream().map(PersonSummaryDTO::getId).toList());
    }

    @Test
    void countByFilter_typeInstitution_matchesSlice() {
        seedDirectoryFixture();

        long count = personRepository.countByFilter("INSTITUTION", null, null, null, false, null);

        assertThat(count).isEqualTo(1);
    }

    @Test
    void countByFilter_query_matchesSliceSize() {
        // The whole point of the shared FILTER_WHERE is that the slice and the count can never
        // drift. Pin the query (LIKE) path explicitly: countByFilter must equal the slice size
        // so a future edit to one query's LIKE clause is caught.
        seedDirectoryFixture();

        List<PersonSummaryDTO> slice = personRepository.findByFilter(
                null, null, null, null, false, "Verlag", 50, 0);
        long count = personRepository.countByFilter(null, null, null, null, false, "Verlag");

        assertThat(count).isEqualTo(slice.size());
        assertThat(count).isEqualTo(1);
    }

    @Test
    void findByFilter_projectsDocumentCount() {
        seedDirectoryFixture();

        List<PersonSummaryDTO> slice = personRepository.findByFilter(
                null, null, true, null, false, null, 50, 0);

        assertThat(slice.get(0).getDocumentCount()).isEqualTo(1);
    }

    // ─── generation column (#689) ──────────────────────────────────────────────

    @Test
    void save_persistsGeneration_andFindByIdReturnsSameGeneration() {
        Person person = Person.builder()
                .firstName("Walter")
                .lastName("Raddatz")
                .generation(3)
                .build();

        Person saved = personRepository.save(person);
        entityManager.flush();
        entityManager.clear();

        Optional<Person> found = personRepository.findById(saved.getId());
        assertThat(found).isPresent();
        assertThat(found.get().getGeneration()).isEqualTo(3);
    }

    @Test
    void save_allowsNullGeneration_existingRowsRemainNull() {
        Person person = Person.builder()
                .firstName("Anonym")
                .lastName("Person")
                .build();

        Person saved = personRepository.save(person);
        entityManager.flush();
        entityManager.clear();

        Optional<Person> found = personRepository.findById(saved.getId());
        assertThat(found).isPresent();
        assertThat(found.get().getGeneration()).isNull();
    }
}
