package org.raddatz.familienarchiv.timeline;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
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.exception.DomainException;
import org.raddatz.familienarchiv.exception.ErrorCode;
import org.raddatz.familienarchiv.person.Person;
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 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;

/**
 * Service-level integration scope the entity/DB tests ({@link TimelineEventTest}) don't reach: the
 * in-transaction view assembly survives a context clear (the {@code @Transactional(readOnly = true)}
 * LazyInit guard), the serialized view leaks no curator-internal fields, and the {@code @Version}
 * optimistic lock engages end-to-end (the Mockito test only proves the catch branch). Real Postgres
 * (V77 CHECK constraints are Postgres-specific) via {@link PostgresContainerConfig}.
 */
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@ActiveProfiles("test")
@Import(PostgresContainerConfig.class)
@Transactional
class TimelineEventServiceIntegrationTest {

    @MockitoBean S3Client s3Client;
    @Autowired TimelineEventService service;
    @Autowired PersonRepository personRepository;
    @Autowired DocumentRepository documentRepository;
    @PersistenceContext EntityManager em;

    // Built locally — the webEnvironment=NONE context has no auto-configured ObjectMapper bean.
    // findAndRegisterModules() pulls in JavaTimeModule so the view's LocalDate/LocalDateTime serialize.
    private final ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules();

    private TimelineEventRequest request(String title, Long version, List<UUID> personIds, List<UUID> documentIds) {
        return new TimelineEventRequest(title, EventType.PERSONAL, LocalDate.of(1914, 7, 28),
                null, null, null, version, personIds, documentIds);
    }

    @Test
    void getEvent_after_context_clear_populates_links_and_leaks_no_internal_fields() throws Exception {
        Person anna = personRepository.save(Person.builder()
                .firstName("Anna").lastName("Müller").notes("GEHEIM-NOTIZ").provisional(true).build());
        Document letter = documentRepository.save(Document.builder()
                .title("Brief an Anna").originalFilename("brief.pdf").status(DocumentStatus.UPLOADED).build());
        UUID eventId = service.create(
                request("Hochzeit", null, List.of(anna.getId()), List.of(letter.getId())), UUID.randomUUID()).id();
        em.flush();
        em.clear();

        // Fresh read — NOT the create return value (that view was assembled while the entity was
        // managed). Only a separate read after the clear proves the readOnly LazyInit guard.
        TimelineEventView fresh = service.getEvent(eventId);

        assertThat(fresh.persons()).singleElement().satisfies(p -> {
            assertThat(p.firstName()).isEqualTo("Anna");
            assertThat(p.lastName()).isEqualTo("Müller");
        });
        assertThat(fresh.documents()).singleElement().satisfies(d ->
                assertThat(d.title()).isEqualTo("Brief an Anna"));

        // Assert on the SERIALIZED JSON: a getter re-introducing a leaked field later would slip
        // past a field-level check.
        String json = objectMapper.writeValueAsString(fresh);
        assertThat(json)
                .doesNotContain("GEHEIM-NOTIZ")
                .doesNotContain("notes")
                .doesNotContain("provisional")
                .doesNotContain("password");
    }

    @Test
    void concurrent_update_with_stale_version_yields_TIMELINE_EVENT_CONFLICT() {
        UUID editorA = UUID.randomUUID();
        UUID editorB = UUID.randomUUID();
        UUID eventId = service.create(request("Original", null, null, null), editorA).id();
        em.flush();
        em.clear();

        // Editor A saves with the version they last saw (0) → succeeds, version advances to 1.
        service.update(eventId, request("Edit A", 0L, null, null), editorA);
        em.flush();
        em.clear();

        // Editor B still holds the stale version 0 → the versioned UPDATE matches no row → 409.
        assertThatThrownBy(() -> service.update(eventId, request("Edit B", 0L, null, null), editorB))
                .isInstanceOf(DomainException.class)
                .extracting(e -> ((DomainException) e).getCode())
                .isEqualTo(ErrorCode.TIMELINE_EVENT_CONFLICT);
    }
}
