package org.raddatz.familienarchiv.person.relationship;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.raddatz.familienarchiv.PostgresContainerConfig;
import org.raddatz.familienarchiv.config.FlywayConfig;
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.relationship.dto.RelationshipUpsertRequest;
import org.raddatz.familienarchiv.person.relationship.dto.NetworkDTO;
import org.raddatz.familienarchiv.person.relationship.dto.RelationshipDTO;
import org.raddatz.familienarchiv.person.PersonNameAliasRepository;
import org.raddatz.familienarchiv.person.PersonRepository;
import org.raddatz.familienarchiv.person.PersonService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase;
import org.springframework.context.annotation.Import;

import jakarta.persistence.EntityManager;

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

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

/**
 * Sara blocker 1 — service+DB integration over the family-network constraints.
 * Hits the real Postgres so unique_rel, ON DELETE CASCADE, and the partial
 * sibling index actually fire.
 */
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import({
        PostgresContainerConfig.class,
        FlywayConfig.class,
        RelationshipService.class,
        RelationshipInferenceService.class,
        PersonService.class
})
class RelationshipServiceIntegrationTest {

    @Autowired RelationshipService relationshipService;
    @Autowired PersonRepository personRepository;
    @Autowired PersonRelationshipRepository relationshipRepository;
    // PersonService → PersonNameAliasRepository; @DataJpaTest auto-loads it.
    @Autowired PersonNameAliasRepository aliasRepository;
    @Autowired EntityManager entityManager;

    Person alice;
    Person bob;
    Person charlie;

    @BeforeEach
    void seed() {
        relationshipRepository.deleteAll();
        aliasRepository.deleteAll();
        personRepository.deleteAll();
        alice = personRepository.save(Person.builder().firstName("Alice").lastName("Müller").familyMember(true).build());
        bob = personRepository.save(Person.builder().firstName("Bob").lastName("Müller").familyMember(true).build());
        charlie = personRepository.save(Person.builder().firstName("Charlie").lastName("Schmidt").familyMember(false).build());
    }

    @Test
    void addRelationship_stores_and_is_readable() {
        var dto = new RelationshipUpsertRequest(bob.getId(), RelationType.PARENT_OF,
                LocalDate.of(1900, 1, 1), DatePrecision.YEAR, null, null, null);

        RelationshipDTO created = relationshipService.addRelationship(alice.getId(), dto);

        assertThat(created.id()).isNotNull();
        assertThat(created.personId()).isEqualTo(alice.getId());
        assertThat(created.relatedPersonId()).isEqualTo(bob.getId());
        assertThat(created.fromDate()).isEqualTo(LocalDate.of(1900, 1, 1));
        assertThat(created.fromDatePrecision()).isEqualTo(DatePrecision.YEAR);
        assertThat(created.toDatePrecision()).isEqualTo(DatePrecision.UNKNOWN);

        List<RelationshipDTO> rels = relationshipService.getRelationships(alice.getId());
        assertThat(rels).hasSize(1);
        assertThat(rels.get(0).relationType()).isEqualTo(RelationType.PARENT_OF);
    }

    @Test
    void addRelationship_throws_409_when_duplicate() {
        var dto = new RelationshipUpsertRequest(bob.getId(), RelationType.PARENT_OF, null, null, null, null, null);
        relationshipService.addRelationship(alice.getId(), dto);

        assertThatThrownBy(() -> relationshipService.addRelationship(alice.getId(), dto))
                .isInstanceOf(DomainException.class)
                .extracting("code")
                .isEqualTo(ErrorCode.DUPLICATE_RELATIONSHIP);
    }

    @Test
    void addRelationship_throws_409_when_circular_parent() {
        // alice PARENT_OF bob; now try bob PARENT_OF alice → must be rejected.
        relationshipService.addRelationship(alice.getId(),
                new RelationshipUpsertRequest(bob.getId(), RelationType.PARENT_OF, null, null, null, null, null));

        var reverse = new RelationshipUpsertRequest(alice.getId(), RelationType.PARENT_OF, null, null, null, null, null);
        assertThatThrownBy(() -> relationshipService.addRelationship(bob.getId(), reverse))
                .isInstanceOf(DomainException.class)
                .extracting("code")
                .isEqualTo(ErrorCode.CIRCULAR_RELATIONSHIP);
    }

    @Test
    void deleteRelationship_throws_404_when_rel_belongs_to_different_person() {
        RelationshipDTO created = relationshipService.addRelationship(alice.getId(),
                new RelationshipUpsertRequest(bob.getId(), RelationType.PARENT_OF, null, null, null, null, null));

        // Charlie is unrelated to this row. Ownership mismatch is 404, not 403, so a
        // curator cannot enumerate relationship ids belonging to people they can't see
        // (anti-enumeration; aligned with the PUT endpoint — ADR-044).
        assertThatThrownBy(() -> relationshipService.deleteRelationship(charlie.getId(), created.id()))
                .isInstanceOf(DomainException.class)
                .extracting("code")
                .isEqualTo(ErrorCode.RELATIONSHIP_NOT_FOUND);

        // The row is still there.
        assertThat(relationshipRepository.findById(created.id())).isPresent();
    }

    @Test
    void updateRelationship_persists_new_type_dates_and_notes() {
        RelationshipDTO created = relationshipService.addRelationship(alice.getId(),
                new RelationshipUpsertRequest(bob.getId(), RelationType.FRIEND, null, null, null, null, null));

        RelationshipDTO updated = relationshipService.updateRelationship(alice.getId(), created.id(),
                new RelationshipUpsertRequest(bob.getId(), RelationType.SPOUSE_OF,
                        LocalDate.of(1923, 5, 12), DatePrecision.DAY, null, null, "wedding day"));

        assertThat(updated.id()).isEqualTo(created.id());
        assertThat(updated.relationType()).isEqualTo(RelationType.SPOUSE_OF);
        assertThat(updated.fromDate()).isEqualTo(LocalDate.of(1923, 5, 12));
        assertThat(updated.fromDatePrecision()).isEqualTo(DatePrecision.DAY);
        assertThat(updated.notes()).isEqualTo("wedding day");
    }

    @Test
    void updateRelationship_throws_404_when_rel_belongs_to_different_person() {
        RelationshipDTO created = relationshipService.addRelationship(alice.getId(),
                new RelationshipUpsertRequest(bob.getId(), RelationType.PARENT_OF, null, null, null, null, null));

        assertThatThrownBy(() -> relationshipService.updateRelationship(charlie.getId(), created.id(),
                new RelationshipUpsertRequest(bob.getId(), RelationType.FRIEND, null, null, null, null, null)))
                .isInstanceOf(DomainException.class)
                .extracting("code")
                .isEqualTo(ErrorCode.RELATIONSHIP_NOT_FOUND);
    }

    @Test
    void addRelationship_throws_409_when_reverse_SPOUSE_OF_pair_already_exists() {
        // V55 enforces symmetric uniqueness for SPOUSE_OF. Inserting (alice, bob, SPOUSE_OF)
        // and then (bob, alice, SPOUSE_OF) must be rejected, just like reverse SIBLING_OF.
        relationshipService.addRelationship(alice.getId(),
                new RelationshipUpsertRequest(bob.getId(), RelationType.SPOUSE_OF, null, null, null, null, null));

        var reverse = new RelationshipUpsertRequest(alice.getId(), RelationType.SPOUSE_OF, null, null, null, null, null);
        assertThatThrownBy(() -> relationshipService.addRelationship(bob.getId(), reverse))
                .isInstanceOf(DomainException.class)
                .extracting("code")
                .isEqualTo(ErrorCode.DUPLICATE_RELATIONSHIP);
    }

    @Test
    void deleteRelationship_succeeds_for_symmetric_type_from_either_side() {
        // alice SPOUSE_OF bob. Bob deletes from his side.
        RelationshipDTO created = relationshipService.addRelationship(alice.getId(),
                new RelationshipUpsertRequest(bob.getId(), RelationType.SPOUSE_OF, null, null, null, null, null));

        relationshipService.deleteRelationship(bob.getId(), created.id());

        assertThat(relationshipRepository.findById(created.id())).isEmpty();
    }

    @Test
    void setFamilyMember_true_makes_person_appear_in_network() {
        // addRelationship side-effects family_member=true on both endpoints for family-graph
        // edges (PARENT_OF/SPOUSE_OF/SIBLING_OF). Reset charlie so the explicit
        // setFamilyMember(true) call below is the thing under test, not the auto-flip.
        relationshipService.addRelationship(alice.getId(),
                new RelationshipUpsertRequest(charlie.getId(), RelationType.PARENT_OF, null, null, null, null, null));
        relationshipService.setFamilyMember(charlie.getId(), false);

        NetworkDTO before = relationshipService.getFamilyNetwork();
        assertThat(before.nodes()).extracting("id").doesNotContain(charlie.getId());

        relationshipService.setFamilyMember(charlie.getId(), true);

        NetworkDTO after = relationshipService.getFamilyNetwork();
        assertThat(after.nodes()).extracting("id").contains(charlie.getId());
        assertThat(after.edges())
                .anyMatch(e -> e.personId().equals(alice.getId()) && e.relatedPersonId().equals(charlie.getId()));
    }

    @Test
    void delete_person_cascades_to_relationships() {
        RelationshipDTO created = relationshipService.addRelationship(alice.getId(),
                new RelationshipUpsertRequest(bob.getId(), RelationType.PARENT_OF, null, null, null, null, null));
        UUID relId = created.id();
        assertThat(relationshipRepository.findById(relId)).isPresent();

        // Detach managed entities so deleteById's cascade isn't fought by the
        // persistence context (the rel row still references bob in memory).
        entityManager.flush();
        entityManager.clear();

        // Delete bob (the relatedPerson) — DB CASCADE must remove the row.
        personRepository.deleteById(bob.getId());
        personRepository.flush();

        assertThat(relationshipRepository.findById(relId)).isEmpty();
    }
}
