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 java.util.Optional;
import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;
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.when;

@ExtendWith(MockitoExtension.class)
class PersonImportUpsertTest {

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

    @Test
    void upsertBySourceRef_insertsNewPerson_whenSourceRefUnknown() {
        when(personRepository.findBySourceRef("clara-cram")).thenReturn(Optional.empty());
        when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        PersonUpsertCommand cmd = PersonUpsertCommand.builder()
                .sourceRef("clara-cram").firstName("Clara").lastName("Cram")
                .personType(PersonType.PERSON).provisional(false).build();

        Person result = personService.upsertBySourceRef(cmd);

        assertThat(result.getSourceRef()).isEqualTo("clara-cram");
        assertThat(result.getFirstName()).isEqualTo("Clara");
        assertThat(result.getLastName()).isEqualTo("Cram");
        assertThat(result.isProvisional()).isFalse();
    }

    @Test
    void upsertBySourceRef_updatesInPlace_whenSourceRefExists() {
        Person existing = Person.builder()
                .id(UUID.randomUUID()).sourceRef("clara-cram")
                .firstName("Clara").lastName("Cram").build();
        when(personRepository.findBySourceRef("clara-cram")).thenReturn(Optional.of(existing));
        when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        PersonUpsertCommand cmd = PersonUpsertCommand.builder()
                .sourceRef("clara-cram").firstName("Clara").lastName("Cram")
                .notes("Updated note").personType(PersonType.PERSON).provisional(false).build();

        personService.upsertBySourceRef(cmd);

        verify(personRepository).save(argThat(p -> p.getId().equals(existing.getId())));
        verify(personRepository, never()).save(argThat(p -> p.getId() == null));
    }

    @Test
    void upsertBySourceRef_preservesHumanEditedNonBlankFields() {
        // A human renamed the maiden-name register person and added notes in-app.
        Person humanEdited = Person.builder()
                .id(UUID.randomUUID()).sourceRef("clara-cram")
                .firstName("Klara").lastName("Cram-Müller").notes("Verified by Marcel").build();
        when(personRepository.findBySourceRef("clara-cram")).thenReturn(Optional.of(humanEdited));
        when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        PersonUpsertCommand cmd = PersonUpsertCommand.builder()
                .sourceRef("clara-cram").firstName("Clara").lastName("Cram")
                .notes("Auto note").personType(PersonType.PERSON).provisional(false).build();

        Person result = personService.upsertBySourceRef(cmd);

        // Human edits survive the re-import.
        assertThat(result.getFirstName()).isEqualTo("Klara");
        assertThat(result.getLastName()).isEqualTo("Cram-Müller");
        assertThat(result.getNotes()).isEqualTo("Verified by Marcel");
    }

    @Test
    void upsertBySourceRef_fillsOnlyBlankFields_onReimport() {
        Person existing = Person.builder()
                .id(UUID.randomUUID()).sourceRef("clara-cram")
                .firstName("Clara").lastName("Cram").notes(null).build();
        when(personRepository.findBySourceRef("clara-cram")).thenReturn(Optional.of(existing));
        when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        PersonUpsertCommand cmd = PersonUpsertCommand.builder()
                .sourceRef("clara-cram").firstName("Clara").lastName("Cram")
                .notes("Nichte von Herbert").personType(PersonType.PERSON).provisional(false).build();

        Person result = personService.upsertBySourceRef(cmd);

        // Blank field gets filled by canonical value.
        assertThat(result.getNotes()).isEqualTo("Nichte von Herbert");
    }

    @Test
    void upsertBySourceRef_fillsBlankYears_butPreservesHumanEditedYears_onReimport() {
        // Existing has a human-set birthYear and a blank deathYear.
        Person existing = Person.builder()
                .id(UUID.randomUUID()).sourceRef("clara-cram")
                .lastName("Cram").birthYear(1890).deathYear(null).build();
        when(personRepository.findBySourceRef("clara-cram")).thenReturn(Optional.of(existing));
        when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        PersonUpsertCommand cmd = PersonUpsertCommand.builder()
                .sourceRef("clara-cram").lastName("Cram")
                .birthYear(1888).deathYear(1965)
                .personType(PersonType.PERSON).provisional(false).build();

        Person result = personService.upsertBySourceRef(cmd);

        assertThat(result.getBirthYear()).isEqualTo(1890); // human value kept
        assertThat(result.getDeathYear()).isEqualTo(1965); // blank filled from canonical
    }

    @Test
    void upsertBySourceRef_neverFlipsProvisionalBackToTrue_onceHumanConfirmed() {
        // A human confirmed this provisional importer-created person (provisional -> false).
        Person confirmed = Person.builder()
                .id(UUID.randomUUID()).sourceRef("schwester-hanni")
                .firstName(null).lastName("Schwester Hanni").provisional(false).build();
        when(personRepository.findBySourceRef("schwester-hanni")).thenReturn(Optional.of(confirmed));
        when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        PersonUpsertCommand cmd = PersonUpsertCommand.builder()
                .sourceRef("schwester-hanni").lastName("Schwester Hanni")
                .personType(PersonType.PERSON).provisional(true).build();

        Person result = personService.upsertBySourceRef(cmd);

        assertThat(result.isProvisional()).isFalse();
    }

    @Test
    void upsertBySourceRef_setsProvisionalTrue_forNewProvisionalPerson() {
        when(personRepository.findBySourceRef("noise-geschirr")).thenReturn(Optional.empty());
        when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        PersonUpsertCommand cmd = PersonUpsertCommand.builder()
                .sourceRef("noise-geschirr").lastName("Tante Tüten")
                .personType(PersonType.PERSON).provisional(true).build();

        Person result = personService.upsertBySourceRef(cmd);

        assertThat(result.isProvisional()).isTrue();
    }

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

    @Test
    void upsertBySourceRef_writesGeneration_onFirstImport() {
        when(personRepository.findBySourceRef("herbert-cram")).thenReturn(Optional.empty());
        when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        PersonUpsertCommand cmd = PersonUpsertCommand.builder()
                .sourceRef("herbert-cram").firstName("Herbert").lastName("Cram")
                .generation(3).personType(PersonType.PERSON).provisional(false).build();

        Person result = personService.upsertBySourceRef(cmd);

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

    @Test
    void upsertBySourceRef_preservesHumanEditedGeneration_onReimport() {
        Person humanEdited = Person.builder()
                .id(UUID.randomUUID()).sourceRef("herbert-cram")
                .firstName("Herbert").lastName("Cram").generation(4).build();
        when(personRepository.findBySourceRef("herbert-cram")).thenReturn(Optional.of(humanEdited));
        when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        PersonUpsertCommand cmd = PersonUpsertCommand.builder()
                .sourceRef("herbert-cram").firstName("Herbert").lastName("Cram")
                .generation(2).personType(PersonType.PERSON).provisional(false).build();

        Person result = personService.upsertBySourceRef(cmd);

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

    @Test
    void mergeCanonical_overwrites_human_null_with_canonical_value_documenting_known_limitation() {
        // If preferHuman gains explicit-null-vs-unset semantics, delete this test (see issue #689).
        Person existing = Person.builder()
                .id(UUID.randomUUID()).sourceRef("herbert-cram")
                .firstName("Herbert").lastName("Cram").generation(null).build();
        when(personRepository.findBySourceRef("herbert-cram")).thenReturn(Optional.of(existing));
        when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        PersonUpsertCommand cmd = PersonUpsertCommand.builder()
                .sourceRef("herbert-cram").firstName("Herbert").lastName("Cram")
                .generation(3).personType(PersonType.PERSON).provisional(false).build();

        Person result = personService.upsertBySourceRef(cmd);

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