package org.raddatz.familienarchiv.document;

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.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.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;

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

import static org.assertj.core.api.Assertions.assertThat;
import static org.raddatz.familienarchiv.document.DocumentSpecifications.isBetween;
import static org.raddatz.familienarchiv.document.DocumentSpecifications.undatedOnly;

/**
 * Real-Postgres assertions for issue #668. H2 disagrees with Postgres on
 * {@code NULLS FIRST/LAST} defaults and on whether {@code BETWEEN} excludes
 * NULL, so these guarantees MUST run against {@code postgres:16-alpine}, never
 * an in-memory database.
 */
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import({PostgresContainerConfig.class, FlywayConfig.class})
class UndatedDocumentOrderingIntegrationTest {

    @Autowired DocumentRepository documentRepository;

    @BeforeEach
    void setUp() {
        documentRepository.deleteAll();
        save("1916", LocalDate.of(1916, 6, 15));
        save("1943", LocalDate.of(1943, 12, 24));
        save("undated-a", null);
        save("undated-b", null);
    }

    private void save(String title, LocalDate date) {
        documentRepository.save(Document.builder()
                .title(title)
                .originalFilename(title + ".pdf")
                .status(DocumentStatus.UPLOADED)
                .metaDatePrecision(date == null ? DatePrecision.UNKNOWN : DatePrecision.DAY)
                .documentDate(date)
                .build());
    }

    @Test
    void dateAscWithNullsLast_returnsDatedFirstUndatedLast() {
        Sort sort = Sort.by(new Sort.Order(Sort.Direction.ASC, "documentDate").nullsLast());

        List<Document> result = documentRepository.findAll(sort);

        assertThat(result).hasSize(4);
        assertThat(result.get(0).getDocumentDate()).isEqualTo(LocalDate.of(1916, 6, 15));
        assertThat(result.get(1).getDocumentDate()).isEqualTo(LocalDate.of(1943, 12, 24));
        assertThat(result.get(2).getDocumentDate()).isNull();
        assertThat(result.get(3).getDocumentDate()).isNull();
    }

    @Test
    void sameDate_tiebreaksByTitleAsc_notCreatedAt_forBothDirections() throws Exception {
        // Owner decision (#668): equal-date rows tie-break by title ASC, NOT
        // createdAt. Insert two same-date docs so that createdAt order (insertion
        // order) is the OPPOSITE of title order: the first-saved doc gets the later
        // title ("zzz-first"), the second-saved doc gets the earlier title
        // ("aaa-second"). If the tiebreaker were still createdAt-asc the first-saved
        // row would lead; because it is title-asc the "aaa-second" row must lead —
        // and it must lead in BOTH ASC and DESC date directions, since the date is
        // equal so only the title tiebreaker decides.
        //
        // The Sort under test is built by the PRODUCTION resolveSort(DATE, dir) (via
        // reflection — it is private), not hand-rolled here, so this test proves the
        // real Postgres ordering that production emits, on real same-date rows.
        documentRepository.deleteAll();
        LocalDate sameDate = LocalDate.of(1920, 3, 3);
        save("zzz-first", sameDate);   // saved first  → earlier createdAt
        save("aaa-second", sameDate);  // saved second → later createdAt

        List<Document> asc = documentRepository.findAll(resolveProductionSort("ASC"));
        assertThat(asc).extracting(Document::getTitle)
                .containsExactly("aaa-second", "zzz-first");

        List<Document> desc = documentRepository.findAll(resolveProductionSort("DESC"));
        assertThat(desc).extracting(Document::getTitle)
                .containsExactly("aaa-second", "zzz-first");
    }

    /**
     * Invokes the production {@link DocumentService#resolveSort(DocumentSort, String)}
     * for the DATE sort so the integration assertions exercise the real tiebreaker
     * choice rather than a sort hand-built in the test.
     */
    private Sort resolveProductionSort(String dir) throws Exception {
        // resolveSort is a pure function of its arguments (uses no instance state), so a
        // bean instance with null collaborators is sufficient to exercise it.
        var ctor = DocumentService.class.getDeclaredConstructors()[0];
        ctor.setAccessible(true);
        Object[] args = new Object[ctor.getParameterCount()];
        DocumentService service = (DocumentService) ctor.newInstance(args);
        var m = DocumentService.class.getDeclaredMethod("resolveSort", DocumentSort.class, String.class);
        m.setAccessible(true);
        return (Sort) m.invoke(service, DocumentSort.DATE, dir);
    }

    @Test
    void undatedOnly_returnsExactlyTheNullDatedRows() {
        List<Document> result = documentRepository.findAll(undatedOnly(true));

        assertThat(result).hasSize(2);
        assertThat(result).allMatch(d -> d.getDocumentDate() == null);
    }

    @Test
    void undatedOnly_false_returnsAllRows() {
        Specification<Document> spec = Specification.where(undatedOnly(false));

        List<Document> result = documentRepository.findAll(spec);

        assertThat(result).hasSize(4);
    }

    @Test
    void dateRange_excludesUndatedRows() {
        List<Document> result = documentRepository.findAll(isBetween(
                LocalDate.of(1900, 1, 1), LocalDate.of(2000, 12, 31)));

        assertThat(result).hasSize(2);
        assertThat(result).allMatch(d -> d.getDocumentDate() != null);
    }

    @Test
    void undatedOnly_combinedWithDateRange_returnsEmpty() {
        // The collision rule (#668): a from/to range and undated=true are mutually
        // exclusive — a row cannot both have a null date and fall inside a range.
        Specification<Document> spec = Specification
                .where(undatedOnly(true))
                .and(isBetween(LocalDate.of(1900, 1, 1), LocalDate.of(2000, 12, 31)));

        List<Document> result = documentRepository.findAll(spec);

        assertThat(result).isEmpty();
    }
}
