package org.raddatz.familienarchiv.person;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.raddatz.familienarchiv.person.PersonNameAliasDTO;
import org.raddatz.familienarchiv.person.PersonSummaryDTO;
import org.raddatz.familienarchiv.person.PersonUpdateDTO;
import org.raddatz.familienarchiv.document.DatePrecision;
import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.exception.ErrorCode;
import org.raddatz.familienarchiv.person.Person;
import org.raddatz.familienarchiv.person.PersonNameAlias;
import org.raddatz.familienarchiv.person.PersonNameAliasType;
import org.raddatz.familienarchiv.person.PersonType;
import org.raddatz.familienarchiv.person.PersonNameAliasRepository;
import org.raddatz.familienarchiv.person.PersonRepository;
import org.springframework.web.server.ResponseStatusException;

import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class PersonServiceTest {

    @Mock PersonRepository personRepository;
    @Mock PersonNameAliasRepository aliasRepository;
    @InjectMocks PersonService personService;

    // ─── getById ─────────────────────────────────────────────────────────────

    @Test
    void getById_throwsNotFound_whenMissing() {
        UUID id = UUID.randomUUID();
        when(personRepository.findById(id)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> personService.getById(id))
                .isInstanceOf(DomainException.class)
                .extracting(e -> ((DomainException) e).getStatus().value())
                .isEqualTo(404);
    }

    @Test
    void getById_returnsPerson_whenFound() {
        UUID id = UUID.randomUUID();
        Person person = Person.builder().id(id).firstName("Hans").lastName("Müller").build();
        when(personRepository.findById(id)).thenReturn(Optional.of(person));

        assertThat(personService.getById(id)).isEqualTo(person);
    }

    // ─── #667: search (filter + pagination) ──────────────────────────────────

    @Test
    void search_returnsPagedResult_withTotalsFromCountQuery() {
        PersonFilter filter = PersonFilter.cleanDefault();
        when(personRepository.countByFilter(null, null, null, null, true, null)).thenReturn(120L);
        when(personRepository.findByFilter(null, null, null, null, true, null, 50, 0))
                .thenReturn(List.of());

        PersonSearchResult result = personService.search(filter, 0, 50, null);

        assertThat(result.totalElements()).isEqualTo(120L);
        assertThat(result.pageNumber()).isEqualTo(0);
        assertThat(result.pageSize()).isEqualTo(50);
        assertThat(result.totalPages()).isEqualTo(3); // ceil(120 / 50)
    }

    @Test
    void search_passesTypeAsEnumName_toRepository() {
        PersonFilter filter = PersonFilter.builder().type(PersonType.INSTITUTION).build();
        when(personRepository.countByFilter("INSTITUTION", null, null, null, false, null)).thenReturn(0L);
        when(personRepository.findByFilter("INSTITUTION", null, null, null, false, null, 50, 0))
                .thenReturn(List.of());

        personService.search(filter, 0, 50, null);

        verify(personRepository).findByFilter("INSTITUTION", null, null, null, false, null, 50, 0);
    }

    @Test
    void search_computesOffset_fromPageAndSize() {
        PersonFilter filter = PersonFilter.showAll();
        when(personRepository.countByFilter(null, null, null, null, false, null)).thenReturn(0L);
        when(personRepository.findByFilter(null, null, null, null, false, null, 20, 40))
                .thenReturn(List.of());

        personService.search(filter, 2, 20, null); // offset = page * size = 40

        verify(personRepository).findByFilter(null, null, null, null, false, null, 20, 40);
    }

    @Test
    void search_trimsBlankQueryToNull() {
        PersonFilter filter = PersonFilter.showAll();
        when(personRepository.countByFilter(null, null, null, null, false, null)).thenReturn(0L);
        when(personRepository.findByFilter(null, null, null, null, false, null, 50, 0))
                .thenReturn(List.of());

        personService.search(filter, 0, 50, "   ");

        verify(personRepository).findByFilter(null, null, null, null, false, null, 50, 0);
    }

    // ─── #667: confirmPerson ──────────────────────────────────────────────────

    @Test
    void confirmPerson_clearsProvisionalFlag() {
        UUID id = UUID.randomUUID();
        Person provisional = Person.builder().id(id).firstName("Inferred").lastName("Person").provisional(true).build();
        when(personRepository.findById(id)).thenReturn(Optional.of(provisional));
        when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        Person result = personService.confirmPerson(id);

        assertThat(result.isProvisional()).isFalse();
        verify(personRepository).save(argThat(p -> !p.isProvisional()));
    }

    @Test
    void confirmPerson_throwsNotFound_whenMissing() {
        UUID id = UUID.randomUUID();
        when(personRepository.findById(id)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> personService.confirmPerson(id))
                .isInstanceOf(DomainException.class)
                .extracting(e -> ((DomainException) e).getStatus().value())
                .isEqualTo(404);
    }

    // ─── #667: deletePerson ───────────────────────────────────────────────────

    @Test
    void deletePerson_deletes_whenPersonExists() {
        UUID id = UUID.randomUUID();
        Person person = Person.builder().id(id).firstName("Weg").lastName("Person").build();
        when(personRepository.findById(id)).thenReturn(Optional.of(person));

        personService.deletePerson(id);

        // Integrity is enforced by V71's ON DELETE constraints — the service only checks
        // existence then deletes; it no longer detaches sender/receiver references itself.
        verify(personRepository).findById(id);
        verify(personRepository).deleteById(id);
        verifyNoMoreInteractions(personRepository);
    }

    @Test
    void deletePerson_throwsNotFound_whenMissing() {
        UUID id = UUID.randomUUID();
        when(personRepository.findById(id)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> personService.deletePerson(id))
                .isInstanceOf(DomainException.class)
                .extracting(e -> ((DomainException) e).getStatus().value())
                .isEqualTo(404);
    }

    // ─── createPerson ─────────────────────────────────────────────────────────

    @Test
    void createPerson_savesPersonWithNullAlias_whenAliasIsNull() {
        when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        Person result = personService.createPerson("Hans", "Müller", null);

        assertThat(result.getAlias()).isNull();
        verify(personRepository).save(argThat(p -> p.getFirstName().equals("Hans") && p.getAlias() == null));
    }

    @Test
    void createPerson_savesPersonWithNullAlias_whenAliasIsBlank() {
        when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        Person result = personService.createPerson("Hans", "Müller", "   ");

        assertThat(result.getAlias()).isNull();
    }

    @Test
    void createPerson_savesTrimmedAlias_whenAliasIsNonBlank() {
        when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        Person result = personService.createPerson("Hans", "Müller", "  Hans Müller  ");

        assertThat(result.getAlias()).isEqualTo("Hans Müller");
    }

    // ─── personType + title in createPerson(PersonUpdateDTO) ─────────────────

    @Test
    void createPerson_dto_persistsPersonType() {
        when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        PersonUpdateDTO dto = new PersonUpdateDTO();
        dto.setFirstName("Walter"); dto.setLastName("de Gruyter"); dto.setPersonType(PersonType.INSTITUTION);

        Person result = personService.createPerson(dto);

        assertThat(result.getPersonType()).isEqualTo(PersonType.INSTITUTION);
    }

    @Test
    void createPerson_dto_throwsInvalidPersonType_whenSkip() {
        PersonUpdateDTO dto = new PersonUpdateDTO();
        dto.setFirstName("Anna"); dto.setLastName("Test"); dto.setPersonType(PersonType.SKIP);

        assertThatThrownBy(() -> personService.createPerson(dto))
                .isInstanceOf(DomainException.class)
                .extracting(e -> ((DomainException) e).getStatus().value())
                .isEqualTo(400);
    }

    @Test
    void createPerson_dto_persistsTitle() {
        when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        PersonUpdateDTO dto = new PersonUpdateDTO();
        dto.setFirstName("Dr."); dto.setLastName("Müller"); dto.setTitle("Prof."); dto.setPersonType(PersonType.PERSON);

        Person result = personService.createPerson(dto);

        assertThat(result.getTitle()).isEqualTo("Prof.");
    }

    // ─── Phase 2.1: createPerson(PersonUpdateDTO) ─────────────────────────────

    @Test
    void createPerson_dto_persistsAllSixFields() {
        when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        PersonUpdateDTO dto = new PersonUpdateDTO();
        dto.setFirstName("Maria"); dto.setLastName("Raddatz"); dto.setAlias("Oma Maria");
        dto.setBirthDate(LocalDate.of(1901, 3, 14)); dto.setBirthDatePrecision(DatePrecision.DAY);
        dto.setDeathDate(LocalDate.of(1975, 11, 2)); dto.setDeathDatePrecision(DatePrecision.DAY);
        dto.setNotes("Some notes");

        Person result = personService.createPerson(dto);

        assertThat(result.getFirstName()).isEqualTo("Maria");
        assertThat(result.getLastName()).isEqualTo("Raddatz");
        assertThat(result.getAlias()).isEqualTo("Oma Maria");
        assertThat(result.getBirthDate()).isEqualTo(LocalDate.of(1901, 3, 14));
        assertThat(result.getBirthDatePrecision()).isEqualTo(DatePrecision.DAY);
        assertThat(result.getDeathDate()).isEqualTo(LocalDate.of(1975, 11, 2));
        assertThat(result.getDeathDatePrecision()).isEqualTo(DatePrecision.DAY);
        assertThat(result.getNotes()).isEqualTo("Some notes");
    }

    @Test
    void createPerson_dto_rejectsDateWithUnknownPrecision() {
        PersonUpdateDTO dto = new PersonUpdateDTO();
        dto.setFirstName("Anna"); dto.setLastName("Test");
        dto.setBirthDate(LocalDate.of(1901, 3, 14)); dto.setBirthDatePrecision(DatePrecision.UNKNOWN);

        assertThatThrownBy(() -> personService.createPerson(dto))
                .isInstanceOf(DomainException.class)
                .satisfies(e -> {
                    assertThat(((DomainException) e).getCode()).isEqualTo(ErrorCode.INVALID_DATE_PRECISION);
                    assertThat(((DomainException) e).getStatus().value()).isEqualTo(400);
                });
    }

    @Test
    void createPerson_dto_treatsNullPrecisionWithNullDateAsUnknown() {
        when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        PersonUpdateDTO dto = new PersonUpdateDTO();
        dto.setFirstName("Anna"); dto.setLastName("Test"); dto.setPersonType(PersonType.PERSON);

        Person result = personService.createPerson(dto);

        assertThat(result.getBirthDate()).isNull();
        assertThat(result.getBirthDatePrecision()).isEqualTo(DatePrecision.UNKNOWN);
        assertThat(result.getDeathDate()).isNull();
        assertThat(result.getDeathDatePrecision()).isEqualTo(DatePrecision.UNKNOWN);
    }

    @Test
    void createPerson_dto_persistsGeneration() {
        when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        PersonUpdateDTO dto = new PersonUpdateDTO();
        dto.setFirstName("Hans"); dto.setLastName("Raddatz");
        dto.setPersonType(PersonType.PERSON); dto.setGeneration(3);

        Person result = personService.createPerson(dto);

        assertThat(result.getGeneration()).isEqualTo(3);
    }

    @Test
    void updatePerson_writesGeneration_includingExplicitNullClear() {
        // The form path is the only place a human can clear generation back to null.
        UUID id = UUID.randomUUID();
        Person existing = Person.builder().id(id).firstName("Hans").lastName("Raddatz")
                .personType(PersonType.PERSON).generation(3).build();
        when(personRepository.findById(id)).thenReturn(Optional.of(existing));
        when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        PersonUpdateDTO dto = new PersonUpdateDTO();
        dto.setFirstName("Hans"); dto.setLastName("Raddatz");
        dto.setPersonType(PersonType.PERSON); dto.setGeneration(null);

        Person result = personService.updatePerson(id, dto);

        assertThat(result.getGeneration()).isNull();
    }

    @Test
    void updatePerson_writesGeneration_whenSet() {
        UUID id = UUID.randomUUID();
        Person existing = Person.builder().id(id).firstName("Hans").lastName("Raddatz")
                .personType(PersonType.PERSON).build();
        when(personRepository.findById(id)).thenReturn(Optional.of(existing));
        when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        PersonUpdateDTO dto = new PersonUpdateDTO();
        dto.setFirstName("Hans"); dto.setLastName("Raddatz");
        dto.setPersonType(PersonType.PERSON); dto.setGeneration(2);

        Person result = personService.updatePerson(id, dto);

        assertThat(result.getGeneration()).isEqualTo(2);
    }

    // ─── updatePerson (personType) ───────────────────────────────────────────

    @Test
    void updatePerson_throwsInvalidPersonType_whenSkip() {
        UUID id = UUID.randomUUID();

        PersonUpdateDTO dto = new PersonUpdateDTO();
        dto.setFirstName("Anna"); dto.setLastName("Alt"); dto.setPersonType(PersonType.SKIP);

        assertThatThrownBy(() -> personService.updatePerson(id, dto))
                .isInstanceOf(DomainException.class)
                .extracting(e -> ((DomainException) e).getStatus().value())
                .isEqualTo(400);
    }

    @Test
    void updatePerson_persistsPersonType() {
        UUID id = UUID.randomUUID();
        Person person = Person.builder().id(id).firstName("Anna").lastName("Alt").personType(PersonType.PERSON).build();
        when(personRepository.findById(id)).thenReturn(Optional.of(person));
        when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        PersonUpdateDTO dto = new PersonUpdateDTO();
        dto.setFirstName("Anna"); dto.setLastName("Alt"); dto.setPersonType(PersonType.INSTITUTION);

        Person result = personService.updatePerson(id, dto);

        assertThat(result.getPersonType()).isEqualTo(PersonType.INSTITUTION);
    }

    // ─── updatePerson (alias) ─────────────────────────────────────────────────

    @Test
    void updatePerson_setsNullAlias_whenAliasIsBlank() {
        UUID id = UUID.randomUUID();
        Person person = Person.builder().id(id).firstName("Anna").lastName("Alt").alias("old alias").build();
        when(personRepository.findById(id)).thenReturn(Optional.of(person));
        when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        PersonUpdateDTO dto = new PersonUpdateDTO();
        dto.setFirstName("Anna"); dto.setLastName("Alt"); dto.setAlias("   ");
        Person result = personService.updatePerson(id, dto);

        assertThat(result.getAlias()).isNull();
    }

    @Test
    void updatePerson_setsTrimmedAlias_whenAliasIsNonBlank() {
        UUID id = UUID.randomUUID();
        Person person = Person.builder().id(id).firstName("Anna").lastName("Alt").build();
        when(personRepository.findById(id)).thenReturn(Optional.of(person));
        when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        PersonUpdateDTO dto = new PersonUpdateDTO();
        dto.setFirstName("Anna"); dto.setLastName("Alt"); dto.setAlias("  Anna Alt  ");
        Person result = personService.updatePerson(id, dto);

        assertThat(result.getAlias()).isEqualTo("Anna Alt");
    }

    // ─── findOrCreateByAlias ─────────────────────────────────────────────────

    @Test
    void findOrCreateByAlias_returnsExactCaseMatch_overCaseInsensitiveSibling() {
        String alias = "müller";
        Person exact = Person.builder().id(UUID.randomUUID()).alias("müller").build();
        when(personRepository.findByAlias(alias)).thenReturn(Optional.of(exact));

        Person result = personService.findOrCreateByAlias(alias);

        assertThat(result).isEqualTo(exact);
        verify(personRepository, never()).findAllByAliasIgnoreCase(any());
        verify(personRepository, never()).save(any());
    }

    @Test
    void findOrCreateByAlias_returnsExactCaseMatch_evenWhenMultipleSiblingsCollide() {
        String alias = "Müller";
        Person exact = Person.builder().id(UUID.randomUUID()).alias("Müller").build();
        when(personRepository.findByAlias(alias)).thenReturn(Optional.of(exact));

        Person result = personService.findOrCreateByAlias(alias);

        assertThat(result).isEqualTo(exact);
        // exact-case short-circuits — the case-insensitive siblings are never consulted.
        verify(personRepository, never()).findAllByAliasIgnoreCase(any());
    }

    @Test
    void findOrCreateByAlias_usesSingleCaseInsensitiveMatch_whenNoExactCase() {
        String alias = "müller";
        Person only = Person.builder().id(UUID.randomUUID()).alias("Müller").build();
        when(personRepository.findByAlias(alias)).thenReturn(Optional.empty());
        when(personRepository.findAllByAliasIgnoreCase(alias)).thenReturn(List.of(only));

        Person result = personService.findOrCreateByAlias(alias);

        assertThat(result).isEqualTo(only);
        verify(personRepository, never()).save(any());
    }

    @Test
    void findOrCreateByAlias_returnsLowestIdDeterministically_whenMultipleCaseInsensitiveMatches() {
        String alias = "müller";
        Person lower = Person.builder().id(UUID.fromString("00000000-0000-0000-0000-000000000001")).alias("Müller").build();
        Person higher = Person.builder().id(UUID.fromString("00000000-0000-0000-0000-000000000002")).alias("müller").build();
        when(personRepository.findByAlias(alias)).thenReturn(Optional.empty());
        when(personRepository.findAllByAliasIgnoreCase(alias)).thenReturn(List.of(higher, lower)); // unordered

        Person first = personService.findOrCreateByAlias(alias);
        Person second = personService.findOrCreateByAlias(alias);

        assertThat(first.getId()).isEqualTo(lower.getId());      // lowest id wins
        assertThat(second.getId()).isEqualTo(first.getId());     // same result every call — never throws
        verify(personRepository, never()).save(any());
    }

    @Test
    void findOrCreateByAlias_createsNew_whenAliasNotFound() {
        String alias = "Clara Cram";
        Person saved = Person.builder().id(UUID.randomUUID()).alias(alias).firstName("Clara").lastName("Cram").build();
        when(personRepository.findByAlias(alias)).thenReturn(Optional.empty());
        when(personRepository.findAllByAliasIgnoreCase(alias)).thenReturn(List.of());
        when(personRepository.save(any())).thenReturn(saved);

        Person result = personService.findOrCreateByAlias(alias);

        assertThat(result).isEqualTo(saved);
        verify(personRepository).save(any());
    }

    @Test
    void findOrCreateByAlias_createsMaidenNameAlias_whenGebPresent() {
        String alias = "Clara Cram geb. de Gruyter";
        Person saved = Person.builder().id(UUID.randomUUID()).alias(alias).firstName("Clara").lastName("Cram").build();
        when(personRepository.findByAlias(alias)).thenReturn(Optional.empty());
        when(personRepository.findAllByAliasIgnoreCase(alias)).thenReturn(List.of());
        when(personRepository.save(any())).thenReturn(saved);
        when(aliasRepository.findMaxSortOrder(saved.getId())).thenReturn(0);
        when(aliasRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        personService.findOrCreateByAlias(alias);

        verify(aliasRepository).save(argThat(a ->
                a.getLastName().equals("de Gruyter") &&
                a.getType() == PersonNameAliasType.MAIDEN_NAME));
    }

    @Test
    void findOrCreateByAlias_returnsNull_whenSkip() {
        Person result = personService.findOrCreateByAlias("Briefumschlag aus Java");
        assertThat(result).isNull();
        verify(personRepository, never()).save(any());
    }

    @Test
    void findOrCreateByAlias_setsInstitutionType_withFullNameInLastName() {
        String alias = "Arthur Collignon GmbH";
        when(personRepository.findByAlias(alias)).thenReturn(Optional.empty());
        when(personRepository.findAllByAliasIgnoreCase(alias)).thenReturn(List.of());
        when(personRepository.save(any())).thenAnswer(inv -> {
            Person p = inv.getArgument(0);
            p.setId(UUID.randomUUID());
            return p;
        });

        Person result = personService.findOrCreateByAlias(alias);

        assertThat(result.getPersonType()).isEqualTo(PersonType.INSTITUTION);
        assertThat(result.getFirstName()).isNull();
        assertThat(result.getLastName()).isEqualTo("Arthur Collignon GmbH");
    }

    @Test
    void findOrCreateByAlias_setsGroupType_withFullNameInLastName() {
        String alias = "Geschwister de Gruyter";
        when(personRepository.findByAlias(alias)).thenReturn(Optional.empty());
        when(personRepository.findAllByAliasIgnoreCase(alias)).thenReturn(List.of());
        when(personRepository.save(any())).thenAnswer(inv -> {
            Person p = inv.getArgument(0);
            p.setId(UUID.randomUUID());
            return p;
        });

        Person result = personService.findOrCreateByAlias(alias);

        assertThat(result.getPersonType()).isEqualTo(PersonType.GROUP);
        assertThat(result.getFirstName()).isNull();
        assertThat(result.getLastName()).isEqualTo("Geschwister de Gruyter");
    }

    @Test
    void findOrCreateByAlias_noAlias_whenNoGeb() {
        String alias = "Clara Cram";
        Person saved = Person.builder().id(UUID.randomUUID()).alias(alias).firstName("Clara").lastName("Cram").build();
        when(personRepository.findByAlias(alias)).thenReturn(Optional.empty());
        when(personRepository.findAllByAliasIgnoreCase(alias)).thenReturn(List.of());
        when(personRepository.save(any())).thenReturn(saved);

        personService.findOrCreateByAlias(alias);

        verify(aliasRepository, never()).save(any());
    }

    @Test
    void findOrCreateByAlias_trimsInput() {
        String alias = "  Clara Cram  ";
        Person saved = Person.builder().id(UUID.randomUUID()).alias("Clara Cram").build();
        when(personRepository.findByAlias("Clara Cram")).thenReturn(Optional.of(saved));

        personService.findOrCreateByAlias(alias);

        verify(personRepository).findByAlias("Clara Cram");
    }

    // ─── findByName (filename-based sender resolution) ────────────────────────

    @Test
    void findByName_returnsExactCaseMatch_overCaseInsensitiveSibling() {
        Person exact = Person.builder().id(UUID.randomUUID()).firstName("Hans").lastName("Müller").build();
        when(personRepository.findByFirstNameAndLastName("Hans", "Müller")).thenReturn(Optional.of(exact));

        assertThat(personService.findByName("Hans", "Müller")).contains(exact);
        verify(personRepository, never()).findAllByFirstNameIgnoreCaseAndLastNameIgnoreCase(any(), any());
    }

    @Test
    void findByName_usesSingleCaseInsensitiveMatch_whenNoExactCase() {
        Person only = Person.builder().id(UUID.randomUUID()).firstName("Hans").lastName("Müller").build();
        when(personRepository.findByFirstNameAndLastName("hans", "müller")).thenReturn(Optional.empty());
        when(personRepository.findAllByFirstNameIgnoreCaseAndLastNameIgnoreCase("hans", "müller"))
                .thenReturn(List.of(only));

        assertThat(personService.findByName("hans", "müller")).contains(only);
    }

    @Test
    void findByName_bailsToEmpty_whenTwoOrMoreCaseInsensitiveMatches() {
        Person a = Person.builder().id(UUID.randomUUID()).firstName("Hans").lastName("Müller").build();
        Person b = Person.builder().id(UUID.randomUUID()).firstName("hans").lastName("müller").build();
        when(personRepository.findByFirstNameAndLastName("hans", "müller")).thenReturn(Optional.empty());
        when(personRepository.findAllByFirstNameIgnoreCaseAndLastNameIgnoreCase("hans", "müller"))
                .thenReturn(List.of(a, b));

        // Ambiguous sender → unset, never an arbitrary guess (provenance correctness over a
        // confidently-wrong pre-fill). This is the deliberate divergence from the alias path.
        assertThat(personService.findByName("hans", "müller")).isEmpty();
    }

    @Test
    void findByName_returnsEmpty_whenFirstNameNullFoldsToNoMatch() {
        when(personRepository.findByFirstNameAndLastName(null, "Müller")).thenReturn(Optional.empty());
        when(personRepository.findAllByFirstNameIgnoreCaseAndLastNameIgnoreCase(null, "Müller"))
                .thenReturn(List.of());

        assertThat(personService.findByName(null, "Müller")).isEmpty();
    }

    // ─── updatePerson (notes) ────────────────────────────────────────────────

    @Test
    void updatePerson_persistsNotes() {
        UUID id = UUID.randomUUID();
        Person person = Person.builder().id(id).firstName("Anna").lastName("Alt").build();
        when(personRepository.findById(id)).thenReturn(Optional.of(person));
        when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        PersonUpdateDTO dto = new PersonUpdateDTO();
        dto.setFirstName("Anna"); dto.setLastName("Alt"); dto.setNotes("Some notes here.");
        Person result = personService.updatePerson(id, dto);

        assertThat(result.getNotes()).isEqualTo("Some notes here.");
    }

    @Test
    void updatePerson_clearsNotes_whenBlank() {
        UUID id = UUID.randomUUID();
        Person person = Person.builder().id(id).firstName("Anna").lastName("Alt").notes("old notes").build();
        when(personRepository.findById(id)).thenReturn(Optional.of(person));
        when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        PersonUpdateDTO dto = new PersonUpdateDTO();
        dto.setFirstName("Anna"); dto.setLastName("Alt"); dto.setNotes("   ");
        Person result = personService.updatePerson(id, dto);

        assertThat(result.getNotes()).isNull();
    }

    // ─── updatePerson (birth/death dates) ────────────────────────────────────

    @Test
    void updatePerson_persistsBirthAndDeathDateWithPrecision() {
        UUID id = UUID.randomUUID();
        Person person = Person.builder().id(id).firstName("Anna").lastName("Alt").build();
        when(personRepository.findById(id)).thenReturn(Optional.of(person));
        when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        PersonUpdateDTO dto = new PersonUpdateDTO();
        dto.setFirstName("Anna"); dto.setLastName("Alt");
        dto.setBirthDate(LocalDate.of(1890, 1, 1)); dto.setBirthDatePrecision(DatePrecision.YEAR);
        dto.setDeathDate(LocalDate.of(1965, 6, 12)); dto.setDeathDatePrecision(DatePrecision.DAY);
        Person result = personService.updatePerson(id, dto);

        assertThat(result.getBirthDate()).isEqualTo(LocalDate.of(1890, 1, 1));
        assertThat(result.getBirthDatePrecision()).isEqualTo(DatePrecision.YEAR);
        assertThat(result.getDeathDate()).isEqualTo(LocalDate.of(1965, 6, 12));
        assertThat(result.getDeathDatePrecision()).isEqualTo(DatePrecision.DAY);
    }

    @Test
    void updatePerson_throwsBirthAfterDeath_whenBirthDateAfterDeathDate() {
        UUID id = UUID.randomUUID();

        PersonUpdateDTO dto = new PersonUpdateDTO();
        dto.setFirstName("Anna"); dto.setLastName("Alt");
        dto.setBirthDate(LocalDate.of(1970, 5, 1)); dto.setBirthDatePrecision(DatePrecision.DAY);
        dto.setDeathDate(LocalDate.of(1950, 5, 1)); dto.setDeathDatePrecision(DatePrecision.DAY);
        assertThatThrownBy(() -> personService.updatePerson(id, dto))
                .isInstanceOf(DomainException.class)
                .satisfies(e -> {
                    assertThat(((DomainException) e).getCode()).isEqualTo(ErrorCode.BIRTH_AFTER_DEATH);
                    assertThat(((DomainException) e).getStatus().value()).isEqualTo(400);
                });
    }

    @Test
    void updatePerson_throwsBirthAfterDeath_onMixedPrecisionLateBirthday() {
        // Known limitation (#773): DAY-precision birth late in the death's YEAR-precision year
        // compares against the year's backfilled Jan 1st and is rejected. The error message
        // carries the workaround hint via the BIRTH_AFTER_DEATH i18n key.
        UUID id = UUID.randomUUID();

        PersonUpdateDTO dto = new PersonUpdateDTO();
        dto.setFirstName("Anna"); dto.setLastName("Alt");
        dto.setBirthDate(LocalDate.of(1901, 11, 15)); dto.setBirthDatePrecision(DatePrecision.DAY);
        dto.setDeathDate(LocalDate.of(1901, 1, 1)); dto.setDeathDatePrecision(DatePrecision.YEAR);
        assertThatThrownBy(() -> personService.updatePerson(id, dto))
                .isInstanceOf(DomainException.class)
                .extracting(e -> ((DomainException) e).getCode())
                .isEqualTo(ErrorCode.BIRTH_AFTER_DEATH);
    }

    @Test
    void updatePerson_doesNotThrow_whenBirthDateSetButDeathDateNull() {
        UUID id = UUID.randomUUID();
        Person person = Person.builder().id(id).firstName("Anna").lastName("Alt").build();
        when(personRepository.findById(id)).thenReturn(Optional.of(person));
        when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        PersonUpdateDTO dto = new PersonUpdateDTO();
        dto.setFirstName("Anna"); dto.setLastName("Alt");
        dto.setBirthDate(LocalDate.of(1890, 1, 1)); dto.setBirthDatePrecision(DatePrecision.YEAR);
        Person result = personService.updatePerson(id, dto);

        assertThat(result.getBirthDate()).isEqualTo(LocalDate.of(1890, 1, 1));
        assertThat(result.getDeathDate()).isNull();
        assertThat(result.getDeathDatePrecision()).isEqualTo(DatePrecision.UNKNOWN);
    }

    @Test
    void updatePerson_allowsEqualBirthAndDeathDate() {
        UUID id = UUID.randomUUID();
        Person person = Person.builder().id(id).firstName("Anna").lastName("Alt").build();
        when(personRepository.findById(id)).thenReturn(Optional.of(person));
        when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        PersonUpdateDTO dto = new PersonUpdateDTO();
        dto.setFirstName("Anna"); dto.setLastName("Alt");
        dto.setBirthDate(LocalDate.of(1900, 1, 1)); dto.setBirthDatePrecision(DatePrecision.YEAR);
        dto.setDeathDate(LocalDate.of(1900, 1, 1)); dto.setDeathDatePrecision(DatePrecision.YEAR);
        Person result = personService.updatePerson(id, dto);

        assertThat(result.getBirthDate()).isEqualTo(LocalDate.of(1900, 1, 1));
        assertThat(result.getDeathDate()).isEqualTo(LocalDate.of(1900, 1, 1));
    }

    // ─── Date/precision coherence (V76 CHECK constraint mirror) ─────────────

    @Test
    void updatePerson_throwsInvalidDatePrecision_whenDatePresentButPrecisionUnknown() {
        UUID id = UUID.randomUUID();

        PersonUpdateDTO dto = new PersonUpdateDTO();
        dto.setFirstName("Anna"); dto.setLastName("Alt");
        dto.setDeathDate(LocalDate.of(1944, 11, 2)); dto.setDeathDatePrecision(DatePrecision.UNKNOWN);
        assertThatThrownBy(() -> personService.updatePerson(id, dto))
                .isInstanceOf(DomainException.class)
                .satisfies(e -> {
                    assertThat(((DomainException) e).getCode()).isEqualTo(ErrorCode.INVALID_DATE_PRECISION);
                    assertThat(((DomainException) e).getStatus().value()).isEqualTo(400);
                });
    }

    @Test
    void updatePerson_throwsInvalidDatePrecision_whenDatePresentButPrecisionNull() {
        UUID id = UUID.randomUUID();

        PersonUpdateDTO dto = new PersonUpdateDTO();
        dto.setFirstName("Anna"); dto.setLastName("Alt");
        dto.setBirthDate(LocalDate.of(1901, 3, 14));
        assertThatThrownBy(() -> personService.updatePerson(id, dto))
                .isInstanceOf(DomainException.class)
                .extracting(e -> ((DomainException) e).getCode())
                .isEqualTo(ErrorCode.INVALID_DATE_PRECISION);
    }

    @Test
    void updatePerson_throwsInvalidDatePrecision_whenPrecisionSetWithoutDate() {
        UUID id = UUID.randomUUID();

        PersonUpdateDTO dto = new PersonUpdateDTO();
        dto.setFirstName("Anna"); dto.setLastName("Alt");
        dto.setBirthDatePrecision(DatePrecision.DAY);
        assertThatThrownBy(() -> personService.updatePerson(id, dto))
                .isInstanceOf(DomainException.class)
                .extracting(e -> ((DomainException) e).getCode())
                .isEqualTo(ErrorCode.INVALID_DATE_PRECISION);
    }

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

    @Test
    void findCorrespondents_delegatesToRepository_withoutFilter() {
        UUID personId = UUID.randomUUID();
        List<Person> expected = List.of(Person.builder().id(UUID.randomUUID()).firstName("Anna").lastName("Muster").build());
        when(personRepository.findCorrespondents(personId)).thenReturn(expected);

        assertThat(personService.findCorrespondents(personId, null)).isEqualTo(expected);
        verify(personRepository).findCorrespondents(personId);
        verify(personRepository, never()).findCorrespondentsWithFilter(any(), any());
    }

    @Test
    void findCorrespondents_delegatesToRepository_withFilter() {
        UUID personId = UUID.randomUUID();
        List<Person> expected = List.of(Person.builder().id(UUID.randomUUID()).firstName("Anna").lastName("Muster").build());
        when(personRepository.findCorrespondentsWithFilter(personId, "Anna")).thenReturn(expected);

        assertThat(personService.findCorrespondents(personId, "Anna")).isEqualTo(expected);
        verify(personRepository).findCorrespondentsWithFilter(personId, "Anna");
        verify(personRepository, never()).findCorrespondents(any());
    }

    @Test
    void findCorrespondents_delegatesToRepository_withBlankFilter() {
        UUID personId = UUID.randomUUID();
        when(personRepository.findCorrespondents(personId)).thenReturn(List.of());

        personService.findCorrespondents(personId, "   ");

        verify(personRepository).findCorrespondents(personId);
        verify(personRepository, never()).findCorrespondentsWithFilter(any(), any());
    }

    // ─── mergePersons ─────────────────────────────────────────────────────────

    @Test
    void mergePersons_throwsBadRequest_whenSourceEqualsTarget() {
        UUID id = UUID.randomUUID();

        assertThatThrownBy(() -> personService.mergePersons(id, id))
                .isInstanceOf(ResponseStatusException.class)
                .extracting(e -> ((ResponseStatusException) e).getStatusCode().value())
                .isEqualTo(400);
    }

    @Test
    void mergePersons_throwsNotFound_whenSourceMissing() {
        UUID sourceId = UUID.randomUUID();
        UUID targetId = UUID.randomUUID();
        when(personRepository.findById(sourceId)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> personService.mergePersons(sourceId, targetId))
                .isInstanceOf(DomainException.class)
                .extracting(e -> ((DomainException) e).getStatus().value())
                .isEqualTo(404);
    }

    @Test
    void mergePersons_throwsNotFound_whenTargetMissing() {
        UUID sourceId = UUID.randomUUID();
        UUID targetId = UUID.randomUUID();
        Person source = Person.builder().id(sourceId).firstName("Anna").lastName("Alt").build();
        when(personRepository.findById(sourceId)).thenReturn(Optional.of(source));
        when(personRepository.findById(targetId)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> personService.mergePersons(sourceId, targetId))
                .isInstanceOf(DomainException.class)
                .extracting(e -> ((DomainException) e).getStatus().value())
                .isEqualTo(404);
    }

    @Test
    void mergePersons_reassignsDocumentsAndDeletesSource() {
        UUID sourceId = UUID.randomUUID();
        UUID targetId = UUID.randomUUID();
        Person source = Person.builder().id(sourceId).firstName("Anna").lastName("Alt").build();
        Person target = Person.builder().id(targetId).firstName("Anna").lastName("Neu").build();
        when(personRepository.findById(sourceId)).thenReturn(Optional.of(source));
        when(personRepository.findById(targetId)).thenReturn(Optional.of(target));

        personService.mergePersons(sourceId, targetId);

        verify(personRepository).findById(sourceId);
        verify(personRepository).findById(targetId);
        verify(personRepository).reassignSender(sourceId, targetId);
        verify(personRepository).insertMissingReceiverReference(sourceId, targetId);
        verify(personRepository).deleteById(sourceId);
        // The source's leftover receiver rows cascade-drop via V71's ON DELETE CASCADE on
        // deleteById — merge no longer deletes them explicitly.
        verifyNoMoreInteractions(personRepository);
    }

    // ─── getAliases ─────────────────────────────────────────────────────────

    @Test
    void getAliases_returnsSortedAliases() {
        UUID personId = UUID.randomUUID();
        when(personRepository.findById(personId)).thenReturn(Optional.of(
                Person.builder().id(personId).firstName("Clara").lastName("Cram").build()));
        List<PersonNameAlias> aliases = List.of(
                PersonNameAlias.builder().id(UUID.randomUUID()).lastName("de Gruyter").type(PersonNameAliasType.BIRTH).sortOrder(0).build());
        when(aliasRepository.findByPersonIdOrderBySortOrderAscCreatedAtAsc(personId)).thenReturn(aliases);

        List<PersonNameAlias> result = personService.getAliases(personId);

        assertThat(result).hasSize(1);
        assertThat(result.get(0).getLastName()).isEqualTo("de Gruyter");
    }

    @Test
    void getAliases_throwsNotFound_whenPersonMissing() {
        UUID personId = UUID.randomUUID();
        when(personRepository.findById(personId)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> personService.getAliases(personId))
                .isInstanceOf(DomainException.class);
    }

    // ─── addAlias ───────────────────────────────────────────────────────────

    @Test
    void addAlias_savesWithAutoIncrementedSortOrder() {
        UUID personId = UUID.randomUUID();
        Person person = Person.builder().id(personId).firstName("Clara").lastName("Cram").build();
        when(personRepository.findById(personId)).thenReturn(Optional.of(person));
        when(aliasRepository.findMaxSortOrder(personId)).thenReturn(2);
        when(aliasRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        PersonNameAliasDTO dto = new PersonNameAliasDTO("de Gruyter", null, PersonNameAliasType.BIRTH);
        PersonNameAlias result = personService.addAlias(personId, dto);

        assertThat(result.getSortOrder()).isEqualTo(3);
        assertThat(result.getLastName()).isEqualTo("de Gruyter");
        assertThat(result.getPerson()).isEqualTo(person);
    }

    @Test
    void addAlias_throwsNotFound_whenPersonMissing() {
        UUID personId = UUID.randomUUID();
        when(personRepository.findById(personId)).thenReturn(Optional.empty());

        PersonNameAliasDTO dto = new PersonNameAliasDTO("de Gruyter", null, PersonNameAliasType.BIRTH);

        assertThatThrownBy(() -> personService.addAlias(personId, dto))
                .isInstanceOf(DomainException.class);
    }

    // ─── removeAlias ────────────────────────────────────────────────────────

    @Test
    void removeAlias_deletesAlias_whenBelongsToPerson() {
        UUID personId = UUID.randomUUID();
        UUID aliasId = UUID.randomUUID();
        Person person = Person.builder().id(personId).firstName("Clara").lastName("Cram").build();
        PersonNameAlias alias = PersonNameAlias.builder().id(aliasId).person(person).lastName("de Gruyter").build();
        when(aliasRepository.findById(aliasId)).thenReturn(Optional.of(alias));

        personService.removeAlias(personId, aliasId);

        verify(aliasRepository).delete(alias);
    }

    @Test
    void removeAlias_throwsNotFound_whenAliasMissing() {
        UUID personId = UUID.randomUUID();
        UUID aliasId = UUID.randomUUID();
        when(aliasRepository.findById(aliasId)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> personService.removeAlias(personId, aliasId))
                .isInstanceOf(DomainException.class);
    }

    @Test
    void removeAlias_throwsForbidden_whenAliasDoesNotBelongToPerson() {
        UUID personId = UUID.randomUUID();
        UUID otherPersonId = UUID.randomUUID();
        UUID aliasId = UUID.randomUUID();
        Person otherPerson = Person.builder().id(otherPersonId).firstName("Other").lastName("Person").build();
        PersonNameAlias alias = PersonNameAlias.builder().id(aliasId).person(otherPerson).lastName("de Gruyter").build();
        when(aliasRepository.findById(aliasId)).thenReturn(Optional.of(alias));

        assertThatThrownBy(() -> personService.removeAlias(personId, aliasId))
                .isInstanceOf(DomainException.class)
                .extracting(e -> ((DomainException) e).getStatus().value())
                .isEqualTo(403);
    }

    @Test
    void findByDisplayNameContaining_delegatesToSearchByName() {
        Person walter = Person.builder().id(UUID.randomUUID()).firstName("Walter").lastName("Müller").build();
        when(personRepository.searchByName("Walter")).thenReturn(List.of(walter));

        List<Person> result = personService.findByDisplayNameContaining("Walter");

        assertThat(result).containsExactly(walter);
        verify(personRepository).searchByName("Walter");
    }

    // ─── tokenize (name-match contract) ───────────────────────────────────────

    @Test
    void tokenize_hyphenatedName_splitsOnHyphen() {
        assertThat(PersonService.tokenize("Anna-Maria")).containsExactly("anna", "maria");
    }

    @Test
    void tokenize_apostropheName_splitsOnApostrophe() {
        assertThat(PersonService.tokenize("D'Angelo")).containsExactly("d", "angelo");
    }

    @Test
    void tokenize_umlautName_lowercasesToSingleToken() {
        assertThat(PersonService.tokenize("Müller")).containsExactly("müller");
    }

    @Test
    void tokenize_doubleSpace_dropsEmptyTokens() {
        assertThat(PersonService.tokenize("Clara  Cram")).containsExactly("clara", "cram");
    }

    @Test
    void tokenize_allWhitespace_returnsEmpty() {
        assertThat(PersonService.tokenize("   ")).isEmpty();
    }

    @Test
    void tokenize_null_returnsEmpty() {
        assertThat(PersonService.tokenize(null)).isEmpty();
    }

    // ─── resolveByName (direct / partial classification) ──────────────────────

    @Test
    void resolveByName_singleDirectMatch_classifiesAsDirect() {
        Person clara = Person.builder().id(UUID.randomUUID()).firstName("Clara").lastName("Cram").build();
        when(personRepository.searchByName("clara")).thenReturn(List.of(clara));
        when(personRepository.searchByName("cram")).thenReturn(List.of(clara));

        NameMatches result = personService.resolveByName("Clara Cram");

        assertThat(result.direct()).containsExactly(clara);
    }

    @Test
    void resolveByName_maidenAliasToken_classifiesAsDirect() {
        Person clara = Person.builder().id(UUID.randomUUID()).firstName("Clara").lastName("Müller")
                .nameAliases(List.of(PersonNameAlias.builder().lastName("Cram")
                        .type(PersonNameAliasType.MAIDEN_NAME).build()))
                .build();
        when(personRepository.searchByName("clara")).thenReturn(List.of(clara));
        when(personRepository.searchByName("cram")).thenReturn(List.of(clara));

        NameMatches result = personService.resolveByName("Clara Cram");

        assertThat(result.direct()).containsExactly(clara);
    }

    @Test
    void resolveByName_aliasFirstNameToken_isFetchedAndClassified() {
        Person clara = Person.builder().id(UUID.randomUUID()).firstName("Clara").lastName("Cram")
                .nameAliases(List.of(PersonNameAlias.builder().firstName("Wilhelmina").lastName("de Gruyter")
                        .type(PersonNameAliasType.BIRTH).build()))
                .build();
        when(personRepository.searchByName("wilhelmina")).thenReturn(List.of(clara));

        NameMatches result = personService.resolveByName("Wilhelmina");

        assertThat(result.direct()).containsExactly(clara);
    }

    @Test
    void resolveByName_middleName_stillDirect() {
        Person clara = Person.builder().id(UUID.randomUUID()).firstName("Clara Maria").lastName("Cram").build();
        when(personRepository.searchByName("clara")).thenReturn(List.of(clara));
        when(personRepository.searchByName("cram")).thenReturn(List.of(clara));

        NameMatches result = personService.resolveByName("Clara Cram");

        assertThat(result.direct()).containsExactly(clara);
    }

    @Test
    void resolveByName_reorderedTokens_stillDirect() {
        Person clara = Person.builder().id(UUID.randomUUID()).firstName("Clara").lastName("Cram").build();
        when(personRepository.searchByName("cram")).thenReturn(List.of(clara));
        when(personRepository.searchByName("clara")).thenReturn(List.of(clara));

        NameMatches result = personService.resolveByName("Cram Clara");

        assertThat(result.direct()).containsExactly(clara);
    }

    @Test
    void resolveByName_cramVsCramer_classifiesAsPartial() {
        Person cramer = Person.builder().id(UUID.randomUUID()).firstName("Clara").lastName("Cramer").build();
        when(personRepository.searchByName("clara")).thenReturn(List.of(cramer));
        when(personRepository.searchByName("cram")).thenReturn(List.of(cramer));

        NameMatches result = personService.resolveByName("Clara Cram");

        assertThat(result.partial()).containsExactly(cramer);
    }

    @Test
    void resolveByName_emptyAfterTokenizing_returnsNoCandidates() {
        NameMatches result = personService.resolveByName("  -  ");

        assertThat(result.direct()).isEmpty();
        verify(personRepository, never()).searchByName(any());
    }

    @Test
    void resolveByName_directSortsBeyondCap_stillReturnedAsDirect() {
        List<Person> pool = new java.util.ArrayList<>();
        for (int i = 0; i < 10; i++) {
            pool.add(Person.builder().id(UUID.randomUUID()).firstName("Clara").lastName("Cramer").build());
        }
        Person direct = Person.builder().id(UUID.randomUUID()).firstName("Clara").lastName("Cram").build();
        pool.add(direct);
        when(personRepository.searchByName("clara")).thenReturn(pool);
        when(personRepository.searchByName("cram")).thenReturn(pool);

        NameMatches result = personService.resolveByName("Clara Cram");

        assertThat(result.direct()).containsExactly(direct);
    }

    @Test
    void resolveByName_over8Tokens_issuesAtMost8Fetches() {
        personService.resolveByName("a b c d e f g h i j");

        verify(personRepository, org.mockito.Mockito.atMost(8)).searchByName(any());
    }

    @Test
    void resolveByName_samePersonFromTwoTokens_appearsOnce() {
        // Both token fetches return the same person id — fetchPool's putIfAbsent must dedup so the
        // candidate is classified once, not twice.
        Person clara = Person.builder().id(UUID.randomUUID()).firstName("Clara").lastName("Cram").build();
        when(personRepository.searchByName("clara")).thenReturn(List.of(clara));
        when(personRepository.searchByName("cram")).thenReturn(List.of(clara));

        NameMatches result = personService.resolveByName("Clara Cram");

        assertThat(result.direct()).hasSize(1);
        assertThat(result.partial()).isEmpty();
    }

    // --- getPersonsByGeneration ---

    @Test
    void getPersonsByGeneration_delegates_to_repository() {
        Person p = Person.builder().id(UUID.randomUUID()).lastName("Müller").generation(2).build();
        when(personRepository.findByGeneration(2)).thenReturn(List.of(p));

        List<Person> result = personService.getPersonsByGeneration(2);

        assertThat(result).containsExactly(p);
    }

    @Test
    void getPersonsByGeneration_returns_emptyList_when_no_match() {
        when(personRepository.findByGeneration(99)).thenReturn(List.of());

        List<Person> result = personService.getPersonsByGeneration(99);

        assertThat(result).isEmpty();
    }
}
