package org.raddatz.familienarchiv.person;

import org.junit.jupiter.api.Test;
import org.raddatz.familienarchiv.PostgresContainerConfig;
import org.raddatz.familienarchiv.document.Document;
import org.raddatz.familienarchiv.document.DocumentRepository;
import org.raddatz.familienarchiv.document.DocumentStatus;
import org.raddatz.familienarchiv.person.Person;
import org.raddatz.familienarchiv.person.PersonType;
import org.raddatz.familienarchiv.person.PersonRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.transaction.annotation.Transactional;
import software.amazon.awssdk.services.s3.S3Client;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;

import java.util.Set;

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

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@ActiveProfiles("test")
@Import(PostgresContainerConfig.class)
@Transactional
class PersonServiceIntegrationTest {

    @MockitoBean S3Client s3Client;
    @Autowired PersonService personService;
    @Autowired PersonRepository personRepository;
    @Autowired DocumentRepository documentRepository;

    @PersistenceContext EntityManager entityManager;

    @Test
    void findOrCreateByAlias_skipReturnsNull_noRecordCreated() {
        Person result = personService.findOrCreateByAlias("Briefumschlag aus Java");

        assertThat(result).isNull();
        assertThat(personRepository.findAll()).isEmpty();
    }

    @Test
    void findOrCreateByAlias_institutionStoresFullNameInLastName() {
        Person result = personService.findOrCreateByAlias("Arthur Collignon GmbH");

        assertThat(result).isNotNull();
        assertThat(result.getPersonType()).isEqualTo(PersonType.INSTITUTION);
        assertThat(result.getFirstName()).isNull();
        assertThat(result.getLastName()).isEqualTo("Arthur Collignon GmbH");
        assertThat(result.getDisplayName()).isEqualTo("Arthur Collignon GmbH");
    }

    @Test
    void findOrCreateByAlias_groupStoresFullNameInLastName() {
        Person result = personService.findOrCreateByAlias("Geschwister de Gruyter");

        assertThat(result).isNotNull();
        assertThat(result.getPersonType()).isEqualTo(PersonType.GROUP);
        assertThat(result.getFirstName()).isNull();
        assertThat(result.getLastName()).isEqualTo("Geschwister de Gruyter");
    }

    @Test
    void findOrCreateByAlias_personSplitsNameNormally() {
        Person result = personService.findOrCreateByAlias("Clara Cram");

        assertThat(result).isNotNull();
        assertThat(result.getPersonType()).isEqualTo(PersonType.PERSON);
        assertThat(result.getFirstName()).isEqualTo("Clara");
        assertThat(result.getLastName()).isEqualTo("Cram");
    }

    // ─── #667: confirm round-trip + reader-default semantics ──────────────────

    @Test
    void search_readerDefault_hidesProvisionalZeroDocumentPerson() {
        personRepository.save(Person.builder()
                .firstName("Unbe").lastName("Staetigt").provisional(true).build());

        PersonSearchResult result = personService.search(PersonFilter.cleanDefault(), 0, 50, null);

        assertThat(result.items()).noneMatch(p -> p.getLastName().equals("Staetigt"));
        assertThat(result.totalElements()).isEqualTo(result.items().size());
    }

    @Test
    void search_showAll_includesProvisionalZeroDocumentPerson() {
        personRepository.save(Person.builder()
                .firstName("Unbe").lastName("Staetigt").provisional(true).build());

        PersonSearchResult result = personService.search(PersonFilter.showAll(), 0, 50, null);

        assertThat(result.items()).anyMatch(p -> p.getLastName().equals("Staetigt"));
    }

    @Test
    void confirmPerson_clearsProvisional_andShowAllTreatsItAsConfirmed() {
        Person provisional = personRepository.save(Person.builder()
                .firstName("Bald").lastName("Bestaetigt").provisional(true).build());

        personService.confirmPerson(provisional.getId());

        Person reloaded = personRepository.findById(provisional.getId()).orElseThrow();
        assertThat(reloaded.isProvisional()).isFalse();

        PersonSearchResult showAll = personService.search(PersonFilter.showAll(), 0, 50, null);
        assertThat(showAll.items())
                .filteredOn(p -> p.getId().equals(provisional.getId()))
                .allMatch(p -> !p.isProvisional());
    }

    @Test
    void deletePerson_removesPerson() {
        Person target = personRepository.save(Person.builder()
                .firstName("Weg").lastName("Person").provisional(true).build());

        personService.deletePerson(target.getId());

        assertThat(personRepository.findById(target.getId())).isEmpty();
    }

    // ─── generation full-stack round-trip (#689) ──────────────────────────────

    @Test
    void updatePerson_clearGenerationToNull_readsBackNullFromDb() {
        // Sara's QA concern: pin the full PUT→DB→GET round-trip for the
        // null-clear path. Without this we only have the WebMvcTest mocked
        // boundary; nothing proved the JPA flush actually wrote SQL NULL.
        Person seeded = personRepository.save(Person.builder()
                .firstName("Hans").lastName("Raddatz")
                .personType(PersonType.PERSON).generation(3).build());
        entityManager.flush();
        entityManager.clear();

        PersonUpdateDTO dto = new PersonUpdateDTO();
        dto.setPersonType(PersonType.PERSON);
        dto.setFirstName("Hans");
        dto.setLastName("Raddatz");
        dto.setGeneration(null);

        personService.updatePerson(seeded.getId(), dto);
        entityManager.flush();
        entityManager.clear();

        Person reloaded = personRepository.findById(seeded.getId()).orElseThrow();
        assertThat(reloaded.getGeneration()).isNull();
    }

    @Test
    void updatePerson_setGenerationToZero_readsBackZeroFromDb() {
        // Pin the G 0 case end-to-end. The form-action spec covers that 0
        // doesn't get spread-dropped at the SvelteKit boundary; this test
        // covers that the controller + service + JPA chain preserves the
        // primitive zero (not coerced to null somewhere along the way).
        Person seeded = personRepository.save(Person.builder()
                .firstName("Walter").lastName("Raddatz")
                .personType(PersonType.PERSON).build());
        entityManager.flush();
        entityManager.clear();

        PersonUpdateDTO dto = new PersonUpdateDTO();
        dto.setPersonType(PersonType.PERSON);
        dto.setFirstName("Walter");
        dto.setLastName("Raddatz");
        dto.setGeneration(0);

        personService.updatePerson(seeded.getId(), dto);
        entityManager.flush();
        entityManager.clear();

        Person reloaded = personRepository.findById(seeded.getId()).orElseThrow();
        assertThat(reloaded.getGeneration()).isEqualTo(0);
    }

    @Test
    void deletePerson_detachesSentAndReceivedReferences_beforeDelete_noOrphan() {
        // A person referenced as BOTH a document sender and a document receiver must delete
        // cleanly: deletePerson nulls the sender_id FK and removes the receiver join row first
        // (reassignSenderToNull → deleteReceiverReferences → deleteById), so no FK orphan and
        // the documents themselves survive.
        Person target = personRepository.save(Person.builder()
                .firstName("Weg").lastName("Person").provisional(true).build());
        Person bystander = personRepository.save(Person.builder()
                .firstName("Bleibt").lastName("Hier").build());

        Document sent = documentRepository.save(Document.builder()
                .title("Sent letter").originalFilename("sent.pdf")
                .status(DocumentStatus.UPLOADED).sender(target).build());
        Document received = documentRepository.save(Document.builder()
                .title("Received letter").originalFilename("received.pdf")
                .status(DocumentStatus.UPLOADED).sender(bystander)
                .receivers(new java.util.HashSet<>(Set.of(target))).build());

        // Persist the fixture and detach everything so the native @Modifying deletes operate on
        // the database directly without the persistence context holding stale references that
        // would re-flush a now-deleted person as a transient association.
        entityManager.flush();
        entityManager.clear();

        personService.deletePerson(target.getId());

        // Native @Modifying queries bypass the persistence context — clear it so the asserting
        // reads observe the post-delete database state, not stale managed entities.
        entityManager.flush();
        entityManager.clear();

        assertThat(personRepository.findById(target.getId())).isEmpty();

        Document reloadedSent = documentRepository.findById(sent.getId()).orElseThrow();
        assertThat(reloadedSent.getSender()).isNull();

        Document reloadedReceived = documentRepository.findById(received.getId()).orElseThrow();
        assertThat(reloadedReceived.getReceivers())
                .noneMatch(p -> p.getId().equals(target.getId()));
        // The other person and the documents themselves survive the delete.
        assertThat(personRepository.findById(bystander.getId())).isPresent();
    }
}
