package org.raddatz.familienarchiv.document;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.raddatz.familienarchiv.audit.AuditLogQueryService;
import org.raddatz.familienarchiv.document.annotation.AnnotationService;
import org.raddatz.familienarchiv.document.transcription.TranscriptionBlockQueryService;
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.raddatz.familienarchiv.filestorage.FileService;
import org.raddatz.familienarchiv.tag.TagService;
import org.raddatz.familienarchiv.person.PersonService;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;

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

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class DocumentServiceSortTest {

    private static final Pageable PAGE = org.springframework.data.domain.PageRequest.of(0, 10_000);

    @Mock DocumentRepository documentRepository;
    @Mock PersonService personService;
    @Mock FileService fileService;
    @Mock TagService tagService;
    @Mock DocumentVersionService documentVersionService;
    @Mock AnnotationService annotationService;
    @Mock AuditLogQueryService auditLogQueryService;
    @Mock TranscriptionBlockQueryService transcriptionBlockQueryService;
    @InjectMocks DocumentService documentService;

    // ─── DATE sort ────────────────────────────────────────────────────────────

    @Test
    void searchDocuments_with_DATE_sort_and_text_sorts_chronologically_not_by_relevance() {
        UUID id1 = UUID.randomUUID();  // higher relevance, older doc
        UUID id2 = UUID.randomUUID();  // lower relevance, newer doc

        Document older = Document.builder().id(id1)
                .title("Brief").status(DocumentStatus.UPLOADED)
                .documentDate(LocalDate.of(1940, 1, 1)).build();
        Document newer = Document.builder().id(id2)
                .title("Brief").status(DocumentStatus.UPLOADED)
                .documentDate(LocalDate.of(1960, 1, 1)).build();

        when(documentRepository.findAllMatchingIdsByFts("Brief")).thenReturn(List.of(id1, id2));
        when(documentRepository.findAll(any(Specification.class), any(Pageable.class)))
                .thenReturn(new PageImpl<>(List.of(newer, older)));

        DocumentSearchResult result = documentService.searchDocuments(
                new SearchFilters("Brief", null, null, null, null, null, null, null, null, false),
                DocumentSort.DATE, "DESC", PAGE);

        assertThat(result.items()).hasSize(2);
        assertThat(result.items().get(0).id()).isEqualTo(id2);  // newer first
    }

    // ─── RELEVANCE sort — pure text (no filters) ──────────────────────────────

    @Test
    void searchDocuments_relevance_pureText_calls_findFtsPageRaw_not_findAllMatchingIds() {
        UUID id1 = UUID.randomUUID();
        List<Object[]> ftsRows = ftsRows(id1, 0.5d, 1L);
        when(documentRepository.findFtsPageRaw(anyString(), anyInt(), anyInt())).thenReturn(ftsRows);
        when(documentRepository.findByIdIn(any()))
                .thenReturn(List.of(doc(id1)));

        documentService.searchDocuments(
                new SearchFilters("Brief", null, null, null, null, null, null, null, null, false),
                DocumentSort.RELEVANCE, null, PAGE);

        verify(documentRepository).findFtsPageRaw(anyString(), anyInt(), anyInt());
        verify(documentRepository, never()).findAllMatchingIdsByFts(anyString());
    }

    @Test
    void searchDocuments_with_RELEVANCE_sort_and_text_preserves_fts_rank_order() {
        UUID id1 = UUID.randomUUID();  // higher rank — must appear first
        UUID id2 = UUID.randomUUID();  // lower rank

        List<Object[]> ftsRows = new ArrayList<>();
        ftsRows.add(new Object[]{id1, 0.8d, 2L});
        ftsRows.add(new Object[]{id2, 0.3d, 2L});
        when(documentRepository.findFtsPageRaw(anyString(), anyInt(), anyInt())).thenReturn(ftsRows);
        when(documentRepository.findByIdIn(any())).thenReturn(List.of(doc(id2), doc(id1))); // unordered from JPA

        DocumentSearchResult result = documentService.searchDocuments(
                new SearchFilters("Brief", null, null, null, null, null, null, null, null, false),
                DocumentSort.RELEVANCE, null, PAGE);

        assertThat(result.items().get(0).id()).isEqualTo(id1);
    }

    @Test
    void searchDocuments_with_null_sort_and_text_defaults_to_fts_rank_order() {
        UUID id1 = UUID.randomUUID();
        UUID id2 = UUID.randomUUID();

        List<Object[]> ftsRows = new ArrayList<>();
        ftsRows.add(new Object[]{id1, 0.8d, 2L});
        ftsRows.add(new Object[]{id2, 0.3d, 2L});
        when(documentRepository.findFtsPageRaw(anyString(), anyInt(), anyInt())).thenReturn(ftsRows);
        when(documentRepository.findByIdIn(any())).thenReturn(List.of(doc(id2), doc(id1)));

        DocumentSearchResult result = documentService.searchDocuments(
                new SearchFilters("Brief", null, null, null, null, null, null, null, null, false),
                null, null, PAGE);

        assertThat(result.items().get(0).id()).isEqualTo(id1);
    }

    // ─── RELEVANCE sort — overflow guard ─────────────────────────────────────

    @Test
    void searchDocuments_relevance_returns_empty_when_offset_exceeds_maxInt() {
        // offset = pageNumber * pageSize; choose values so offset > Integer.MAX_VALUE
        Pageable hugePage = org.springframework.data.domain.PageRequest.of(Integer.MAX_VALUE / 10 + 1, 10);

        DocumentSearchResult result = documentService.searchDocuments(
                new SearchFilters("Brief", null, null, null, null, null, null, null, null, false),
                DocumentSort.RELEVANCE, null, hugePage);

        assertThat(result.items()).isEmpty();
        verify(documentRepository, never()).findFtsPageRaw(anyString(), anyInt(), anyInt());
    }

    // ─── toFtsPage — UUID-as-String JDBC driver variance ────────────────────

    @Test
    void searchDocuments_relevance_handles_string_uuid_from_jdbc_driver() {
        String stringId = "11111111-1111-1111-1111-111111111111";
        UUID uuidId = UUID.fromString(stringId);
        // Simulate a JDBC driver that returns the id column as String instead of UUID
        List<Object[]> ftsRows = new ArrayList<>();
        ftsRows.add(new Object[]{stringId, 0.5d, 1L});
        when(documentRepository.findFtsPageRaw(anyString(), anyInt(), anyInt())).thenReturn(ftsRows);
        when(documentRepository.findByIdIn(any())).thenReturn(List.of(doc(uuidId)));

        DocumentSearchResult result = documentService.searchDocuments(
                new SearchFilters("Brief", null, null, null, null, null, null, null, null, false),
                DocumentSort.RELEVANCE, null, PAGE);

        assertThat(result.items()).hasSize(1);
        assertThat(result.items().get(0).id()).isEqualTo(uuidId);
    }

    // ─── RELEVANCE sort — text + active filter ────────────────────────────────

    @Test
    void searchDocuments_relevance_with_active_filter_uses_inMemory_path() {
        UUID id1 = UUID.randomUUID();
        UUID id2 = UUID.randomUUID();

        when(documentRepository.findAllMatchingIdsByFts("Brief")).thenReturn(List.of(id1, id2));
        when(documentRepository.findAll(any(Specification.class)))
                .thenReturn(List.of(doc(id2), doc(id1)));

        // sender filter is active → triggers in-memory path, not findFtsPageRaw
        LocalDate from = LocalDate.of(1900, 1, 1);
        documentService.searchDocuments(
                new SearchFilters("Brief", from, null, null, null, null, null, null, null, false),
                DocumentSort.RELEVANCE, null, PAGE);

        verify(documentRepository, never()).findFtsPageRaw(anyString(), anyInt(), anyInt());
        verify(documentRepository).findAllMatchingIdsByFts("Brief");
    }

    // ─── Helpers ──────────────────────────────────────────────────────────────

    private static Document doc(UUID id) {
        return Document.builder().id(id).title("Brief").status(DocumentStatus.UPLOADED).build();
    }

    private static List<Object[]> ftsRows(UUID id, double rank, long total) {
        List<Object[]> rows = new ArrayList<>();
        rows.add(new Object[]{id, rank, total});
        return rows;
    }
}
