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.document.DatePrecision;

import java.time.LocalDate;
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");
    }

    // ─── life dates (ADR-025 extension via preferHumanDate, #773) ─────────────

    @Test
    void upsertBySourceRef_preservesDayPrecisionDate_onReimportWithDifferentYear() {
        // A human entered the exact birthday in-app; the spreadsheet only knows a year.
        Person handDated = Person.builder()
                .id(UUID.randomUUID()).sourceRef("clara-cram").lastName("Cram")
                .birthDate(LocalDate.of(1890, 3, 14)).birthDatePrecision(DatePrecision.DAY).build();
        when(personRepository.findBySourceRef("clara-cram")).thenReturn(Optional.of(handDated));
        when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

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

        Person result = personService.upsertBySourceRef(cmd);

        assertThat(result.getBirthDate()).isEqualTo(LocalDate.of(1890, 3, 14));
        assertThat(result.getBirthDatePrecision()).isEqualTo(DatePrecision.DAY);
    }

    @Test
    void upsertBySourceRef_preservesMonthPrecisionDate_onReimport() {
        Person handDated = Person.builder()
                .id(UUID.randomUUID()).sourceRef("clara-cram").lastName("Cram")
                .deathDate(LocalDate.of(1944, 11, 1)).deathDatePrecision(DatePrecision.MONTH).build();
        when(personRepository.findBySourceRef("clara-cram")).thenReturn(Optional.of(handDated));
        when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

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

        Person result = personService.upsertBySourceRef(cmd);

        assertThat(result.getDeathDate()).isEqualTo(LocalDate.of(1944, 11, 1));
        assertThat(result.getDeathDatePrecision()).isEqualTo(DatePrecision.MONTH);
    }

    @Test
    void upsertBySourceRef_refreshesYearPrecisionDate_whenSpreadsheetYearChanges() {
        // YEAR precision means "the importer's year" — a corrected spreadsheet year wins.
        Person yearOnly = Person.builder()
                .id(UUID.randomUUID()).sourceRef("clara-cram").lastName("Cram")
                .birthDate(LocalDate.of(1890, 1, 1)).birthDatePrecision(DatePrecision.YEAR).build();
        when(personRepository.findBySourceRef("clara-cram")).thenReturn(Optional.of(yearOnly));
        when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

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

        Person result = personService.upsertBySourceRef(cmd);

        assertThat(result.getBirthDate()).isEqualTo(LocalDate.of(1888, 1, 1));
        assertThat(result.getBirthDatePrecision()).isEqualTo(DatePrecision.YEAR);
    }

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

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

        Person result = personService.upsertBySourceRef(cmd);

        assertThat(result.getDeathDate()).isEqualTo(LocalDate.of(1965, 1, 1));
        assertThat(result.getDeathDatePrecision()).isEqualTo(DatePrecision.YEAR);
    }

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

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

        Person result = personService.upsertBySourceRef(cmd);

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

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

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

        Person result = personService.upsertBySourceRef(cmd);

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

    @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);
    }

    // ─── conflicting canonical life dates degrade instead of hitting the DB CHECK ──
    // (chk_person_birth_before_death would abort the whole batch — REQ-IMP-001)

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

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

        Person result = personService.upsertBySourceRef(cmd);

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

    @Test
    void upsertBySourceRef_keepsHandEnteredBirth_andDropsConflictingCanonicalDeath() {
        // A human entered an exact birthday; the spreadsheet's death year lies before it.
        // The hand-entered side must survive, the conflicting canonical refresh is dropped.
        Person handDated = Person.builder()
                .id(UUID.randomUUID()).sourceRef("clara-cram").lastName("Cram")
                .birthDate(LocalDate.of(1950, 6, 1)).birthDatePrecision(DatePrecision.DAY).build();
        when(personRepository.findBySourceRef("clara-cram")).thenReturn(Optional.of(handDated));
        when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

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

        Person result = personService.upsertBySourceRef(cmd);

        assertThat(result.getBirthDate()).isEqualTo(LocalDate.of(1950, 6, 1));
        assertThat(result.getBirthDatePrecision()).isEqualTo(DatePrecision.DAY);
        assertThat(result.getDeathDate()).isNull();
        assertThat(result.getDeathDatePrecision()).isEqualTo(DatePrecision.UNKNOWN);
    }

    @Test
    void upsertBySourceRef_keepsExistingYearDates_whenCanonicalRefreshConflicts() {
        Person existing = Person.builder()
                .id(UUID.randomUUID()).sourceRef("clara-cram").lastName("Cram")
                .birthDate(LocalDate.of(1900, 1, 1)).birthDatePrecision(DatePrecision.YEAR)
                .deathDate(LocalDate.of(1980, 1, 1)).deathDatePrecision(DatePrecision.YEAR).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(1990).deathYear(1985)
                .personType(PersonType.PERSON).provisional(false).build();

        Person result = personService.upsertBySourceRef(cmd);

        assertThat(result.getBirthDate()).isEqualTo(LocalDate.of(1900, 1, 1));
        assertThat(result.getBirthDatePrecision()).isEqualTo(DatePrecision.YEAR);
        assertThat(result.getDeathDate()).isEqualTo(LocalDate.of(1980, 1, 1));
        assertThat(result.getDeathDatePrecision()).isEqualTo(DatePrecision.YEAR);
    }
}
