package org.raddatz.familienarchiv.timeline;

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 org.raddatz.familienarchiv.document.DocumentService;
import org.raddatz.familienarchiv.person.Person;
import org.raddatz.familienarchiv.person.PersonService;
import org.raddatz.familienarchiv.person.relationship.PersonRelationship;
import org.raddatz.familienarchiv.person.relationship.RelationshipService;
import org.raddatz.familienarchiv.person.relationship.RelationType;

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

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class DerivedEventsAssemblyTest {

    @Mock private TimelineEventRepository events;
    @Mock private PersonService personService;
    @Mock private DocumentService documentService;
    @Mock private RelationshipService relationshipService;

    @InjectMocks private TimelineEventService service;

    // --- factory helpers ---

    private Person makePerson(LocalDate birthDate, DatePrecision birthPrecision) {
        return Person.builder()
                .id(UUID.randomUUID())
                .firstName("Anna")
                .lastName("Müller")
                .familyMember(true)
                .birthDate(birthDate)
                .birthDatePrecision(birthPrecision)
                .build();
    }

    private Person makePersonWithDeath(LocalDate deathDate, DatePrecision deathPrecision) {
        return Person.builder()
                .id(UUID.randomUUID())
                .firstName("Hans")
                .lastName("Raddatz")
                .familyMember(true)
                .deathDate(deathDate)
                .deathDatePrecision(deathPrecision)
                .build();
    }

    private Person makePersonWithBoth(
            LocalDate birthDate, DatePrecision birthPrecision,
            LocalDate deathDate, DatePrecision deathPrecision) {
        return Person.builder()
                .id(UUID.randomUUID())
                .firstName("Anna")
                .lastName("Müller")
                .familyMember(true)
                .birthDate(birthDate)
                .birthDatePrecision(birthPrecision)
                .deathDate(deathDate)
                .deathDatePrecision(deathPrecision)
                .build();
    }

    private Person makeNonFamilyPerson(LocalDate birthDate, DatePrecision precision) {
        return Person.builder()
                .id(UUID.randomUUID())
                .firstName("Anna")
                .lastName("Müller")
                .familyMember(false)
                .birthDate(birthDate)
                .birthDatePrecision(precision)
                .build();
    }

    private PersonRelationship makeSpouseEdge(Person a, Person b, Integer fromYear) {
        return makeSpouseEdgeWithDate(a, b,
                fromYear != null ? LocalDate.of(fromYear, 1, 1) : null,
                fromYear != null ? DatePrecision.YEAR : DatePrecision.UNKNOWN);
    }

    private PersonRelationship makeSpouseEdgeWithDate(Person a, Person b, LocalDate fromDate, DatePrecision precision) {
        return PersonRelationship.builder()
                .id(UUID.randomUUID())
                .person(a)
                .relatedPerson(b)
                .relationType(RelationType.SPOUSE_OF)
                .fromDate(fromDate)
                .fromDatePrecision(precision)
                .build();
    }

    // --- REQ-001: birth events ---

    @Test
    void should_emit_one_geburt_for_person_with_birthdate() {
        Person anna = makePerson(LocalDate.of(1901, 3, 12), DatePrecision.DAY);
        when(personService.findAllFamilyMembers()).thenReturn(List.of(anna));
        when(relationshipService.findAllSpouseEdges()).thenReturn(List.of());

        List<TimelineEntryDTO> result = service.assembleDerivedEvents();

        assertThat(result).hasSize(1);
        TimelineEntryDTO event = result.get(0);
        assertThat(event.derived()).isTrue();
        assertThat(event.type()).isEqualTo(EventType.PERSONAL);
        assertThat(event.derivedType()).isEqualTo(DerivedEventType.BIRTH);
        assertThat(event.eventDate()).isEqualTo(LocalDate.of(1901, 3, 12));
        assertThat(event.precision()).isEqualTo(DatePrecision.DAY);
        assertThat(event.title()).isEqualTo(anna.getDisplayName());
    }

    // --- REQ-003: null birthDate → no Geburt event ---

    @Test
    void should_emit_zero_tod_when_person_has_birthdate_but_no_deathdate() {
        Person anna = makePerson(LocalDate.of(1901, 3, 12), DatePrecision.DAY);
        when(personService.findAllFamilyMembers()).thenReturn(List.of(anna));
        when(relationshipService.findAllSpouseEdges()).thenReturn(List.of());

        List<TimelineEntryDTO> result = service.assembleDerivedEvents();

        long todCount = result.stream()
                .filter(e -> e.derivedType() == DerivedEventType.DEATH)
                .count();
        assertThat(todCount).isZero();
    }

    // --- REQ-004: null deathDate → no Tod event ---

    @Test
    void should_emit_no_events_for_person_with_neither_date() {
        Person nobody = Person.builder()
                .id(UUID.randomUUID())
                .firstName("Hans")
                .lastName("Raddatz")
                .familyMember(true)
                .build();
        when(personService.findAllFamilyMembers()).thenReturn(List.of(nobody));
        when(relationshipService.findAllSpouseEdges()).thenReturn(List.of());

        List<TimelineEntryDTO> result = service.assembleDerivedEvents();

        assertThat(result).isEmpty();
    }

    // --- REQ-002: death events ---

    @Test
    void should_emit_one_tod_for_person_with_deathdate() {
        Person hans = makePersonWithDeath(LocalDate.of(1965, 7, 4), DatePrecision.DAY);
        when(personService.findAllFamilyMembers()).thenReturn(List.of(hans));
        when(relationshipService.findAllSpouseEdges()).thenReturn(List.of());

        List<TimelineEntryDTO> result = service.assembleDerivedEvents();

        assertThat(result).hasSize(1);
        TimelineEntryDTO event = result.get(0);
        assertThat(event.derived()).isTrue();
        assertThat(event.type()).isEqualTo(EventType.PERSONAL);
        assertThat(event.derivedType()).isEqualTo(DerivedEventType.DEATH);
        assertThat(event.eventDate()).isEqualTo(LocalDate.of(1965, 7, 4));
        assertThat(event.precision()).isEqualTo(DatePrecision.DAY);
        assertThat(event.title()).isEqualTo(hans.getDisplayName());
    }

    // --- REQ-002 + REQ-003 combined ---

    @Test
    void should_emit_one_tod_and_zero_geburt_for_person_with_deathdate_only() {
        Person hans = makePersonWithDeath(LocalDate.of(1965, 7, 4), DatePrecision.YEAR);
        when(personService.findAllFamilyMembers()).thenReturn(List.of(hans));
        when(relationshipService.findAllSpouseEdges()).thenReturn(List.of());

        List<TimelineEntryDTO> result = service.assembleDerivedEvents();

        assertThat(result).hasSize(1);
        assertThat(result.get(0).derivedType()).isEqualTo(DerivedEventType.DEATH);
    }

    // --- REQ-005: Heirat with fromYear ---

    @Test
    void should_emit_one_heirat_for_spouse_edge_with_fromYear() {
        Person anna = makePerson(null, DatePrecision.UNKNOWN);
        Person hans = makePersonWithDeath(null, DatePrecision.UNKNOWN);
        PersonRelationship edge = makeSpouseEdge(anna, hans, 1930);
        when(personService.findAllFamilyMembers()).thenReturn(List.of(anna, hans));
        when(relationshipService.findAllSpouseEdges()).thenReturn(List.of(edge));

        List<TimelineEntryDTO> result = service.assembleDerivedEvents();

        List<TimelineEntryDTO> heiraten = result.stream()
                .filter(e -> e.derivedType() == DerivedEventType.MARRIAGE)
                .toList();
        assertThat(heiraten).hasSize(1);
        TimelineEntryDTO heirat = heiraten.get(0);
        assertThat(heirat.derived()).isTrue();
        assertThat(heirat.type()).isEqualTo(EventType.PERSONAL);
        assertThat(heirat.derivedType()).isEqualTo(DerivedEventType.MARRIAGE);
        assertThat(heirat.eventDate()).isEqualTo(LocalDate.of(1930, 1, 1));
        assertThat(heirat.precision()).isEqualTo(DatePrecision.YEAR);
    }

    // --- REQ-006: Heirat with null fromYear → emitted with UNKNOWN precision ---

    @Test
    void should_emit_unknown_precision_heirat_when_fromYear_is_null() {
        Person anna = makePerson(null, DatePrecision.UNKNOWN);
        Person hans = makePersonWithDeath(null, DatePrecision.UNKNOWN);
        PersonRelationship edge = makeSpouseEdge(anna, hans, null);
        when(personService.findAllFamilyMembers()).thenReturn(List.of(anna, hans));
        when(relationshipService.findAllSpouseEdges()).thenReturn(List.of(edge));

        List<TimelineEntryDTO> result = service.assembleDerivedEvents();

        List<TimelineEntryDTO> heiraten = result.stream()
                .filter(e -> e.derivedType() == DerivedEventType.MARRIAGE)
                .toList();
        assertThat(heiraten).hasSize(1);
        TimelineEntryDTO heirat = heiraten.get(0);
        assertThat(heirat.eventDate()).isNull();
        assertThat(heirat.precision()).isEqualTo(DatePrecision.UNKNOWN);
    }

    // --- REQ-017 (#837): derived Heirat sources SPOUSE_OF.fromDate at its stored precision ---

    @Test
    void should_emit_day_precision_heirat_from_spouse_fromDate() {
        Person anna = makePerson(null, DatePrecision.UNKNOWN);
        Person hans = makePersonWithDeath(null, DatePrecision.UNKNOWN);
        PersonRelationship edge = makeSpouseEdgeWithDate(anna, hans, LocalDate.of(1923, 5, 12), DatePrecision.DAY);
        when(personService.findAllFamilyMembers()).thenReturn(List.of(anna, hans));
        when(relationshipService.findAllSpouseEdges()).thenReturn(List.of(edge));

        TimelineEntryDTO heirat = service.assembleDerivedEvents().stream()
                .filter(e -> e.derivedType() == DerivedEventType.MARRIAGE)
                .findFirst().orElseThrow();

        assertThat(heirat.eventDate()).isEqualTo(LocalDate.of(1923, 5, 12));
        assertThat(heirat.precision()).isEqualTo(DatePrecision.DAY);
    }

    // --- REQ-007: exactly one Heirat per SPOUSE_OF edge (dedup) ---

    @Test
    void should_emit_exactly_one_heirat_when_both_spouses_in_scope() {
        Person anna = makePerson(null, DatePrecision.UNKNOWN);
        Person hans = makePerson(null, DatePrecision.UNKNOWN);
        PersonRelationship edge = makeSpouseEdge(anna, hans, 1930);
        when(personService.findAllFamilyMembers()).thenReturn(List.of(anna, hans));
        when(relationshipService.findAllSpouseEdges()).thenReturn(List.of(edge));

        List<TimelineEntryDTO> result = service.assembleDerivedEvents();

        long heiratCount = result.stream()
                .filter(e -> e.derivedType() == DerivedEventType.MARRIAGE)
                .count();
        assertThat(heiratCount).isEqualTo(1);
    }

    @Test
    void should_emit_two_heirat_for_person_married_to_two_partners() {
        Person anna = makePerson(null, DatePrecision.UNKNOWN);
        Person hans = makePerson(null, DatePrecision.UNKNOWN);
        Person karl = makePerson(null, DatePrecision.UNKNOWN);
        PersonRelationship edge1 = makeSpouseEdge(anna, hans, 1930);
        PersonRelationship edge2 = makeSpouseEdge(anna, karl, 1945);
        when(personService.findAllFamilyMembers()).thenReturn(List.of(anna, hans, karl));
        when(relationshipService.findAllSpouseEdges()).thenReturn(List.of(edge1, edge2));

        List<TimelineEntryDTO> result = service.assembleDerivedEvents();

        long heiratCount = result.stream()
                .filter(e -> e.derivedType() == DerivedEventType.MARRIAGE)
                .count();
        assertThat(heiratCount).isEqualTo(2);
    }

    // --- REQ-001 precision pass-through ---

    @Test
    void should_pass_birth_precision_through_unchanged() {
        Person anna = makePerson(LocalDate.of(1901, 3, 12), DatePrecision.DAY);
        when(personService.findAllFamilyMembers()).thenReturn(List.of(anna));
        when(relationshipService.findAllSpouseEdges()).thenReturn(List.of());

        List<TimelineEntryDTO> result = service.assembleDerivedEvents();

        assertThat(result).hasSize(1);
        assertThat(result.get(0).precision()).isEqualTo(DatePrecision.DAY);
    }

    // --- REQ-008: synthetic prefixed ids, never UUID ---

    @Test
    void should_mint_prefixed_synthetic_ids_never_uuid() {
        Person anna = makePerson(LocalDate.of(1901, 3, 12), DatePrecision.DAY);
        when(personService.findAllFamilyMembers()).thenReturn(List.of(anna));
        when(relationshipService.findAllSpouseEdges()).thenReturn(List.of());

        List<TimelineEntryDTO> result = service.assembleDerivedEvents();

        assertThat(result).hasSize(1);
        TimelineEntryDTO entry = result.get(0);
        assertThat(entry.derived()).isTrue();
        assertThat(entry.eventId()).isNull();
        assertThat(entry.documentId()).isNull();
    }

    // --- REQ-010: display names on Heirat ---

    @Test
    void should_emit_heirat_with_displayname_for_both_spouses() {
        Person anna = makePerson(null, DatePrecision.UNKNOWN);
        Person hans = makePersonWithDeath(null, DatePrecision.UNKNOWN);
        PersonRelationship edge = makeSpouseEdge(anna, hans, 1930);
        when(personService.findAllFamilyMembers()).thenReturn(List.of(anna, hans));
        when(relationshipService.findAllSpouseEdges()).thenReturn(List.of(edge));

        List<TimelineEntryDTO> heiraten = service.assembleDerivedEvents().stream()
                .filter(e -> e.derivedType() == DerivedEventType.MARRIAGE)
                .toList();

        assertThat(heiraten).hasSize(1);
        TimelineEntryDTO heirat = heiraten.get(0);
        assertThat(heirat.title()).isNotNull().isNotBlank();
        assertThat(heirat.linkedPersonIds()).hasSize(2);
    }

    // --- REQ-007 note: assumption/documentation test ---

    @Test
    void self_spouse_edge_invariant_is_enforced_by_db_constraint() {
        // Assumption test — documents that the DB constraint prevents self-edges;
        // the service does not guard this itself.
        // The unique_spouse_pair index (V55) using LEAST/GREATEST is the authoritative guard.
        // This test verifies that if an edge were somehow inserted (impossible in prod),
        // the service would still produce one event (not zero or an exception).
        Person anna = makePerson(null, DatePrecision.UNKNOWN);
        PersonRelationship selfEdge = makeSpouseEdge(anna, anna, 1930);
        when(personService.findAllFamilyMembers()).thenReturn(List.of(anna));
        when(relationshipService.findAllSpouseEdges()).thenReturn(List.of(selfEdge));

        List<TimelineEntryDTO> result = service.assembleDerivedEvents();

        long heiratCount = result.stream()
                .filter(e -> e.derivedType() == DerivedEventType.MARRIAGE)
                .count();
        assertThat(heiratCount).isEqualTo(1);
    }

    // --- REQ-012: non-family-member persons excluded from Geburt/Tod ---

    @Test
    void should_exclude_non_family_member_persons_from_derived_events() {
        Person nonMember = makeNonFamilyPerson(LocalDate.of(1901, 3, 12), DatePrecision.DAY);
        when(personService.findAllFamilyMembers()).thenReturn(List.of());
        when(relationshipService.findAllSpouseEdges()).thenReturn(List.of());

        List<TimelineEntryDTO> result = service.assembleDerivedEvents();

        assertThat(result).isEmpty();
    }

    // --- REQ-013: Heirat emitted even when one spouse has familyMember=false ---

    @Test
    void should_emit_heirat_when_one_spouse_is_not_family_member() {
        Person anna = makePerson(null, DatePrecision.UNKNOWN);
        Person nonMember = makeNonFamilyPerson(null, DatePrecision.UNKNOWN);
        PersonRelationship edge = makeSpouseEdge(anna, nonMember, 1930);
        when(personService.findAllFamilyMembers()).thenReturn(List.of(anna));
        when(relationshipService.findAllSpouseEdges()).thenReturn(List.of(edge));

        List<TimelineEntryDTO> result = service.assembleDerivedEvents();

        long heiratCount = result.stream()
                .filter(e -> e.derivedType() == DerivedEventType.MARRIAGE)
                .count();
        assertThat(heiratCount).isEqualTo(1);
    }

    // --- REQ-014: empty family-member list → empty result, no error ---

    @Test
    void should_emit_zero_events_when_no_family_members() {
        when(personService.findAllFamilyMembers()).thenReturn(List.of());
        when(relationshipService.findAllSpouseEdges()).thenReturn(List.of());

        List<TimelineEntryDTO> result = service.assembleDerivedEvents();

        assertThat(result).isEmpty();
    }
}
