package org.raddatz.familienarchiv.person.relationship;

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.Person;
import org.raddatz.familienarchiv.person.relationship.dto.InferredRelationshipDTO;
import org.raddatz.familienarchiv.person.relationship.dto.InferredRelationshipWithPersonDTO;
import org.raddatz.familienarchiv.person.PersonService;

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

import static java.util.Collections.emptyList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyCollection;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.when;
import static org.raddatz.familienarchiv.person.relationship.RelationToken.*;
import static org.raddatz.familienarchiv.person.relationship.RelationType.*;

/**
 * Felix Brandt — TDD red phase for RelationshipInferenceService.
 * <p>
 * 18 unit tests, one per LABEL_MAP entry plus one no-path case. Each setup wires
 * a small graph through the mocked repository and asserts the exact abstract
 * token sequence emitted by BFS — except {@code distant_label_for_long_chain}
 * which asserts the fallback label, and {@code returns_empty_when_no_path}
 * which asserts no result.
 */
@ExtendWith(MockitoExtension.class)
class RelationshipInferenceServiceTest {

    @Mock PersonRelationshipRepository relationshipRepository;
    @Mock PersonService personService;
    @InjectMocks RelationshipInferenceService service;

    // --- 1: parent ---
    @Test
    void parent_path_emits_UP() {
        Person parent = person();
        Person child = person();
        givenEdges(parentOf(parent, child));

        assertThat(service.findShortestPath(child.getId(), parent.getId()))
                .hasValue(List.of(UP));
    }

    // --- 2: child ---
    @Test
    void child_path_emits_DOWN() {
        Person parent = person();
        Person child = person();
        givenEdges(parentOf(parent, child));

        assertThat(service.findShortestPath(parent.getId(), child.getId()))
                .hasValue(List.of(DOWN));
    }

    // --- 3: spouse ---
    @Test
    void spouse_path_emits_SPOUSE() {
        Person a = person();
        Person b = person();
        givenEdges(spouseOf(a, b));

        assertThat(service.findShortestPath(a.getId(), b.getId()))
                .hasValue(List.of(SPOUSE));
    }

    // --- 4: sibling ---
    @Test
    void sibling_path_emits_SIBLING() {
        Person a = person();
        Person b = person();
        givenEdges(siblingOf(a, b));

        assertThat(service.findShortestPath(a.getId(), b.getId()))
                .hasValue(List.of(SIBLING));
    }

    // --- 5: grandparent (UP, UP) ---
    @Test
    void grandparent_path_emits_UP_UP() {
        Person grandparent = person();
        Person parent = person();
        Person grandchild = person();
        givenEdges(parentOf(grandparent, parent), parentOf(parent, grandchild));

        assertThat(service.findShortestPath(grandchild.getId(), grandparent.getId()))
                .hasValue(List.of(UP, UP));
    }

    // --- 6: grandchild (DOWN, DOWN) ---
    @Test
    void grandchild_path_emits_DOWN_DOWN() {
        Person grandparent = person();
        Person parent = person();
        Person grandchild = person();
        givenEdges(parentOf(grandparent, parent), parentOf(parent, grandchild));

        assertThat(service.findShortestPath(grandparent.getId(), grandchild.getId()))
                .hasValue(List.of(DOWN, DOWN));
    }

    // --- 7: great-grandparent (UP, UP, UP) ---
    @Test
    void great_grandparent_path_emits_UP_UP_UP() {
        Person g = person();
        Person p = person();
        Person c = person();
        Person gc = person();
        givenEdges(parentOf(g, p), parentOf(p, c), parentOf(c, gc));

        assertThat(service.findShortestPath(gc.getId(), g.getId()))
                .hasValue(List.of(UP, UP, UP));
    }

    // --- 8: great-grandchild (DOWN, DOWN, DOWN) ---
    @Test
    void great_grandchild_path_emits_DOWN_DOWN_DOWN() {
        Person g = person();
        Person p = person();
        Person c = person();
        Person gc = person();
        givenEdges(parentOf(g, p), parentOf(p, c), parentOf(c, gc));

        assertThat(service.findShortestPath(g.getId(), gc.getId()))
                .hasValue(List.of(DOWN, DOWN, DOWN));
    }

    // --- 9: uncle/aunt (UP, SIBLING) ---
    @Test
    void uncle_aunt_path_emits_UP_SIBLING() {
        Person grandparent = person();
        Person parent = person();
        Person uncle = person();
        Person me = person();
        // grandparent has two children: parent and uncle. me is parent's child.
        givenEdges(
                parentOf(grandparent, parent),
                parentOf(grandparent, uncle),
                parentOf(parent, me));

        assertThat(service.findShortestPath(me.getId(), uncle.getId()))
                .hasValue(List.of(UP, SIBLING));
    }

    // --- 10: niece/nephew (SIBLING, DOWN) ---
    @Test
    void niece_nephew_path_emits_SIBLING_DOWN() {
        Person grandparent = person();
        Person uncle = person();
        Person sibling = person();
        Person niece = person();
        // grandparent has uncle + sibling; sibling has niece.
        givenEdges(
                parentOf(grandparent, uncle),
                parentOf(grandparent, sibling),
                parentOf(sibling, niece));

        assertThat(service.findShortestPath(uncle.getId(), niece.getId()))
                .hasValue(List.of(SIBLING, DOWN));
    }

    // --- 11: great uncle/aunt (UP, UP, SIBLING) ---
    @Test
    void great_uncle_aunt_path_emits_UP_UP_SIBLING() {
        Person ggp = person();
        Person grandparent = person();
        Person greatUncle = person();
        Person parent = person();
        Person me = person();
        givenEdges(
                parentOf(ggp, grandparent),
                parentOf(ggp, greatUncle),
                parentOf(grandparent, parent),
                parentOf(parent, me));

        assertThat(service.findShortestPath(me.getId(), greatUncle.getId()))
                .hasValue(List.of(UP, UP, SIBLING));
    }

    // --- 12: great niece/nephew (SIBLING, DOWN, DOWN) ---
    @Test
    void great_niece_nephew_path_emits_SIBLING_DOWN_DOWN() {
        Person grandparent = person();
        Person sibling = person();
        Person greatUncle = person();
        Person niece = person();
        Person greatNiece = person();
        givenEdges(
                parentOf(grandparent, sibling),
                parentOf(grandparent, greatUncle),
                parentOf(sibling, niece),
                parentOf(niece, greatNiece));

        assertThat(service.findShortestPath(greatUncle.getId(), greatNiece.getId()))
                .hasValue(List.of(SIBLING, DOWN, DOWN));
    }

    // --- 13: parent-in-law (SPOUSE, UP) ---
    @Test
    void inlaw_parent_path_emits_SPOUSE_UP() {
        Person inlaw = person();
        Person spouse = person();
        Person me = person();
        givenEdges(
                parentOf(inlaw, spouse),
                spouseOf(me, spouse));

        assertThat(service.findShortestPath(me.getId(), inlaw.getId()))
                .hasValue(List.of(SPOUSE, UP));
    }

    // --- 14: child-in-law (DOWN, SPOUSE) ---
    @Test
    void inlaw_child_path_emits_DOWN_SPOUSE() {
        Person me = person();
        Person child = person();
        Person inlawChild = person();
        givenEdges(
                parentOf(me, child),
                spouseOf(child, inlawChild));

        assertThat(service.findShortestPath(me.getId(), inlawChild.getId()))
                .hasValue(List.of(DOWN, SPOUSE));
    }

    // --- 15: sibling-in-law via my spouse's sibling (SPOUSE, SIBLING) ---
    @Test
    void sibling_inlaw_via_spouse_emits_SPOUSE_SIBLING() {
        Person me = person();
        Person spouse = person();
        Person spouseSibling = person();
        givenEdges(
                spouseOf(me, spouse),
                siblingOf(spouse, spouseSibling));

        assertThat(service.findShortestPath(me.getId(), spouseSibling.getId()))
                .hasValue(List.of(SPOUSE, SIBLING));
    }

    // --- 16: cousin (UP, SIBLING, DOWN) ---
    @Test
    void cousin_1_path_emits_UP_SIBLING_DOWN() {
        Person ggp = person();
        Person parentMine = person();
        Person uncle = person();
        Person me = person();
        Person cousin = person();
        givenEdges(
                parentOf(ggp, parentMine),
                parentOf(ggp, uncle),
                parentOf(parentMine, me),
                parentOf(uncle, cousin));

        assertThat(service.findShortestPath(me.getId(), cousin.getId()))
                .hasValue(List.of(UP, SIBLING, DOWN));
    }

    // --- 17: distant (label fallback for long chains) ---
    @Test
    void distant_label_for_long_chain() {
        // Seven-generation ancestor: chain of seven PARENT_OF edges.
        Person a0 = person();
        Person a1 = person();
        Person a2 = person();
        Person a3 = person();
        Person a4 = person();
        Person a5 = person();
        Person a6 = person();
        Person a7 = person();
        givenEdges(
                parentOf(a0, a1),
                parentOf(a1, a2),
                parentOf(a2, a3),
                parentOf(a3, a4),
                parentOf(a4, a5),
                parentOf(a5, a6),
                parentOf(a6, a7));

        Optional<InferredRelationshipDTO> inferred = service.infer(a7.getId(), a0.getId());
        assertThat(inferred).hasValueSatisfying(r -> {
            assertThat(r.hops()).isEqualTo(7);
            assertThat(r.labelFromA()).isEqualTo("distant");
            assertThat(r.labelFromB()).isEqualTo("distant");
        });
    }

    // --- 18: no path ---
    @Test
    void returns_empty_when_no_path() {
        Person a = person();
        Person b = person();
        // No edges between them.
        givenEdges(/* none */);

        assertThat(service.findShortestPath(a.getId(), b.getId())).isEmpty();
        assertThat(service.infer(a.getId(), b.getId())).isEmpty();
    }

    // --- 19: findAllFor delegates person resolution to PersonService ---
    @Test
    void findAllFor_resolves_persons_via_PersonService() {
        Person parent = person();
        Person child = person();
        givenEdges(parentOf(parent, child));
        when(personService.getAllById(anyList())).thenReturn(List.of(child));

        List<InferredRelationshipWithPersonDTO> results = service.findAllFor(parent.getId());

        assertThat(results).hasSize(1);
        assertThat(results.get(0).person().displayName()).isEqualTo(child.getDisplayName());
    }

    // --- helpers ---

    private void givenEdges(PersonRelationship... edges) {
        when(relationshipRepository.findAllByRelationTypeIn(anyCollection()))
                .thenReturn(edges.length == 0 ? emptyList() : List.of(edges));
    }

    private static Person person() {
        return Person.builder().id(UUID.randomUUID()).lastName("X").familyMember(true).build();
    }

    private static PersonRelationship parentOf(Person parent, Person child) {
        return edge(parent, child, PARENT_OF);
    }

    private static PersonRelationship spouseOf(Person a, Person b) {
        return edge(a, b, SPOUSE_OF);
    }

    private static PersonRelationship siblingOf(Person a, Person b) {
        return edge(a, b, SIBLING_OF);
    }

    private static PersonRelationship edge(Person a, Person b, RelationType type) {
        return PersonRelationship.builder()
                .id(UUID.randomUUID())
                .person(a)
                .relatedPerson(b)
                .relationType(type)
                .createdAt(Instant.now())
                .build();
    }
}
