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.document.DocumentSearchResult;
import org.raddatz.familienarchiv.document.DocumentSort;
import org.raddatz.familienarchiv.document.Document;
import org.raddatz.familienarchiv.document.DocumentStatus;
import org.raddatz.familienarchiv.document.DocumentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.data.domain.PageRequest;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import software.amazon.awssdk.services.s3.S3Client;

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

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

/**
 * End-to-end paged search test with real PostgreSQL (Testcontainers). Covers the
 * Specification→Pageable→Page→DTO path that unit tests mock around. Seeds 120
 * UPLOADED documents and asserts the slice/total/totalPages arithmetic holds
 * against the actual JPA query.
 *
 * <p>Closes the integration-coverage gap Sara flagged on PR #316.
 */
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@ActiveProfiles("test")
@Import(PostgresContainerConfig.class)
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
class DocumentSearchPagedIntegrationTest {

    private static final int FIXTURE_SIZE = 120;

    @MockitoBean S3Client s3Client;
    @Autowired DocumentService documentService;
    @Autowired DocumentRepository documentRepository;

    @BeforeEach
    void seed() {
        // Deterministic date spread so DATE-DESC order is predictable:
        // document #0 has the oldest date, document #119 has the newest.
        for (int i = 0; i < FIXTURE_SIZE; i++) {
            Document doc = Document.builder()
                    .title("Dok-" + String.format("%03d", i))
                    .originalFilename("dok-" + i + ".pdf")
                    .status(DocumentStatus.UPLOADED)
                    .documentDate(LocalDate.of(1900, 1, 1).plusDays(i))
                    .build();
            documentRepository.save(doc);
        }
        assertThat(documentRepository.count()).isEqualTo(FIXTURE_SIZE);
    }

    @Test
    void search_firstPage_returnsExactlyPageSizeItems_andCorrectTotalElements() {
        DocumentSearchResult result = documentService.searchDocuments(
                null, null, null, null, null, null, null, null,
                DocumentSort.DATE, "DESC", null,
                PageRequest.of(0, 50));

        assertThat(result.items()).hasSize(50);
        assertThat(result.totalElements()).isEqualTo(FIXTURE_SIZE);
        assertThat(result.pageNumber()).isZero();
        assertThat(result.pageSize()).isEqualTo(50);
        assertThat(result.totalPages()).isEqualTo(3); // ceil(120 / 50)
    }

    @Test
    void search_lastPartialPage_returnsRemainingItems() {
        DocumentSearchResult result = documentService.searchDocuments(
                null, null, null, null, null, null, null, null,
                DocumentSort.DATE, "DESC", null,
                PageRequest.of(2, 50));

        // Page 2 (offset 100) of 120 docs → exactly 20 items on the tail.
        assertThat(result.items()).hasSize(20);
        assertThat(result.totalElements()).isEqualTo(FIXTURE_SIZE);
        assertThat(result.pageNumber()).isEqualTo(2);
    }

    @Test
    void search_pageBeyondLast_returnsEmptyContent_totalElementsStillCorrect() {
        DocumentSearchResult result = documentService.searchDocuments(
                null, null, null, null, null, null, null, null,
                DocumentSort.DATE, "DESC", null,
                PageRequest.of(99, 50));

        assertThat(result.items()).isEmpty();
        assertThat(result.totalElements()).isEqualTo(FIXTURE_SIZE);
    }

    @Test
    void search_senderSort_pageOne_slicesInMemory_withCorrectTotal() {
        // SENDER sort path fetches all + sorts + slices in-memory (see scaling
        // comment in DocumentService). Proves that the in-memory slice path
        // returns the correct total from a real repository fetch.
        DocumentSearchResult result = documentService.searchDocuments(
                null, null, null, null, null, null, null, null,
                DocumentSort.SENDER, "asc", null,
                PageRequest.of(1, 50));

        assertThat(result.items()).hasSize(50);
        assertThat(result.totalElements()).isEqualTo(FIXTURE_SIZE);
        assertThat(result.pageNumber()).isEqualTo(1);
        assertThat(result.totalPages()).isEqualTo(3);
    }

    @Test
    void search_differentPagesReturnDisjointSlices() {
        DocumentSearchResult page0 = documentService.searchDocuments(
                null, null, null, null, null, null, null, null,
                DocumentSort.DATE, "DESC", null,
                PageRequest.of(0, 50));
        DocumentSearchResult page1 = documentService.searchDocuments(
                null, null, null, null, null, null, null, null,
                DocumentSort.DATE, "DESC", null,
                PageRequest.of(1, 50));

        // No document id should appear on both pages — slicing must be exclusive.
        var idsOnPage0 = page0.items().stream()
                .map(item -> item.document().getId())
                .toList();
        var idsOnPage1 = page1.items().stream()
                .map(item -> item.document().getId())
                .toList();
        for (UUID id : idsOnPage0) {
            assertThat(idsOnPage1).doesNotContain(id);
        }
    }
}
