package org.raddatz.familienarchiv.timeline;

import jakarta.persistence.EntityManager;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.raddatz.familienarchiv.PostgresContainerConfig;
import org.raddatz.familienarchiv.config.FlywayConfig;
import org.raddatz.familienarchiv.document.DatePrecision;
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.PersonRepository;
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 org.springframework.dao.DataIntegrityViolationException;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

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

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

/**
 * Persistence + DB-constraint tests for {@link TimelineEvent} against real Postgres (V77).
 * Mirrors {@code MigrationIntegrationTest}'s slice setup; never H2 — only the real DB proves
 * enum-as-varchar storage, the RANGE/UNKNOWN CHECK constraints, FK cascade, and {@code @Version}.
 */
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import({PostgresContainerConfig.class, FlywayConfig.class})
class TimelineEventTest {

    @Autowired TimelineEventRepository events;
    @Autowired PersonRepository persons;
    @Autowired DocumentRepository documents;
    @Autowired EntityManager em;

    /**
     * Sensible defaults; each test overrides only what it asserts. {@code createdBy}/{@code updatedBy}
     * default to random UUIDs — both columns are NOT NULL and not auto-populated, so without these
     * every test would fail at flush with the same constraint violation (red for the wrong reason).
     * Precision is intentionally left unset so {@code @Builder.Default YEAR} can be exercised.
     */
    private TimelineEvent.TimelineEventBuilder makeEvent() {
        return TimelineEvent.builder()
                .title("Hochzeit von Anna und Otto")
                .type(EventType.PERSONAL)
                .eventDate(LocalDate.of(1914, 7, 28))
                .createdBy(UUID.randomUUID())
                .updatedBy(UUID.randomUUID());
    }

    @Test
    void persists_and_loads_event_with_required_fields() {
        TimelineEvent saved = events.save(makeEvent().build());

        assertThat(saved.getId()).isNotNull();
    }

    @Test
    void precision_defaults_to_YEAR_when_not_set() {
        TimelineEvent saved = events.save(makeEvent().build());

        assertThat(saved.getPrecision()).isEqualTo(DatePrecision.YEAR);
    }

    @Test
    void persists_event_with_linked_persons() {
        Person anna = persons.save(Person.builder().firstName("Anna").lastName("Raddatz").build());
        TimelineEvent saved = events.save(makeEvent().persons(Set.of(anna)).build());
        em.flush();
        em.clear();

        TimelineEvent reloaded = events.findById(saved.getId()).orElseThrow();
        assertThat(reloaded.getPersons()).extracting(Person::getId).containsExactly(anna.getId());
    }

    @Test
    void persists_event_with_linked_documents() {
        Document letter = documents.save(Document.builder()
                .title("Brief").originalFilename("brief.pdf").status(DocumentStatus.UPLOADED).build());
        TimelineEvent saved = events.save(makeEvent().documents(Set.of(letter)).build());
        em.flush();
        em.clear();

        TimelineEvent reloaded = events.findById(saved.getId()).orElseThrow();
        assertThat(reloaded.getDocuments()).extracting(Document::getId).containsExactly(letter.getId());
    }

    @Test
    void eventDateEnd_round_trips_null_for_non_range() {
        TimelineEvent saved = events.save(makeEvent().build()); // YEAR precision, no end
        em.flush();
        em.clear();

        assertThat(events.findById(saved.getId()).orElseThrow().getEventDateEnd()).isNull();
    }

    @Test
    void eventDateEnd_round_trips_value_for_range() {
        TimelineEvent saved = events.save(makeEvent()
                .precision(DatePrecision.RANGE)
                .eventDate(LocalDate.of(1914, 1, 1))
                .eventDateEnd(LocalDate.of(1918, 12, 31))
                .build());
        em.flush();
        em.clear();

        assertThat(events.findById(saved.getId()).orElseThrow().getEventDateEnd())
                .isEqualTo(LocalDate.of(1918, 12, 31));
    }

    @Test
    void description_round_trips_null() {
        TimelineEvent saved = events.save(makeEvent().build());
        em.flush();
        em.clear();

        assertThat(events.findById(saved.getId()).orElseThrow().getDescription()).isNull();
    }

    @Test
    void description_round_trips_multi_kb_text() {
        // Proves TEXT has no length cap — @Column(columnDefinition = "TEXT") overrides
        // Hibernate's default VARCHAR(255).
        String longText = "Sommertage am See. ".repeat(500); // ~9.5 KB
        TimelineEvent saved = events.save(makeEvent().description(longText).build());
        em.flush();
        em.clear();

        assertThat(events.findById(saved.getId()).orElseThrow().getDescription()).isEqualTo(longText);
    }

    @Test
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    void range_invariant_rejects_non_null_end_without_range_precision() {
        // precision YEAR + non-null end violates chk_timeline_event_range.
        try {
            assertThatThrownBy(() -> events.saveAndFlush(makeEvent()
                    .eventDateEnd(LocalDate.of(1918, 12, 31))
                    .build()))
                    .isInstanceOf(DataIntegrityViolationException.class);
        } finally {
            events.deleteAll(); // NOT_SUPPORTED opts out of the rollback; clean any leaked row
        }
    }

    @Test
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    void range_invariant_rejects_range_precision_without_end_date() {
        // precision RANGE + null end violates chk_timeline_event_range.
        try {
            assertThatThrownBy(() -> events.saveAndFlush(makeEvent()
                    .precision(DatePrecision.RANGE)
                    .build()))
                    .isInstanceOf(DataIntegrityViolationException.class);
        } finally {
            events.deleteAll();
        }
    }

    @Test
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    void unknown_precision_is_rejected() {
        // chk_timeline_event_precision forbids UNKNOWN — curated events are never undated.
        try {
            assertThatThrownBy(() -> events.saveAndFlush(makeEvent()
                    .precision(DatePrecision.UNKNOWN)
                    .build()))
                    .isInstanceOf(DataIntegrityViolationException.class);
        } finally {
            events.deleteAll();
        }
    }

    @Test
    void version_is_null_before_persist_and_zero_after_save() {
        TimelineEvent fresh = makeEvent().build();
        assertThat(fresh.getVersion()).isNull(); // @Version Long is null pre-persist

        TimelineEvent saved = events.saveAndFlush(fresh);
        assertThat(saved.getVersion()).isEqualTo(0L); // Hibernate sets 0 on insert
    }

    @ParameterizedTest
    @EnumSource(value = DatePrecision.class, names = {"DAY", "MONTH", "SEASON", "YEAR", "APPROX"})
    void all_non_unknown_precisions_are_accepted(DatePrecision precision) {
        // Accept-side of chk_timeline_event_precision: every non-RANGE, non-UNKNOWN value persists.
        // Documents that SEASON ("Sommer 1914") and APPROX ("ca. 1914") are intentionally legal,
        // so an over-tight CHECK cannot ship green. (RANGE is covered by the round-trip test.)
        TimelineEvent saved = events.saveAndFlush(makeEvent().precision(precision).build());

        assertThat(saved.getId()).isNotNull();
    }
}
