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.raddatz.familienarchiv.document.Document;
import org.raddatz.familienarchiv.document.DocumentStatus;
import org.raddatz.familienarchiv.person.Person;
import org.raddatz.familienarchiv.tag.Tag;
import org.raddatz.familienarchiv.tag.Tag;
import org.raddatz.familienarchiv.tag.TagRepository;
import org.raddatz.familienarchiv.person.PersonRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.domain.Specification;

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

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

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import({PostgresContainerConfig.class, FlywayConfig.class})
class DocumentSpecificationsTest {

    @Autowired DocumentRepository documentRepository;
    @Autowired PersonRepository personRepository;
    @Autowired TagRepository tagRepository;

    private Person sender;
    private Person receiver;
    private Document briefEarly;
    private Document briefLate;
    private Document photoDoc;

    @BeforeEach
    void setUp() {
        documentRepository.deleteAll();
        personRepository.deleteAll();
        tagRepository.deleteAll();

        sender   = personRepository.save(Person.builder().firstName("Walter").lastName("Müller").build());
        receiver = personRepository.save(Person.builder().firstName("Anna").lastName("Schmidt").build());

        Tag tagFamilie = tagRepository.save(Tag.builder().name("Familie").build());
        Tag tagUrlaub  = tagRepository.save(Tag.builder().name("Urlaub").build());

        briefEarly = documentRepository.save(Document.builder()
                .title("Alter Brief")
                .originalFilename("brief_early.pdf")
                .status(DocumentStatus.UPLOADED)
                .documentDate(LocalDate.of(1940, 5, 1))
                .transcription("Liebe Anna, ich schreibe dir aus dem Krieg")
                .location("Berlin")
                .sender(sender)
                .receivers(Set.of(receiver))
                .tags(Set.of(tagFamilie))
                .build());

        briefLate = documentRepository.save(Document.builder()
                .title("Neuerer Brief")
                .originalFilename("brief_late.pdf")
                .status(DocumentStatus.UPLOADED)
                .documentDate(LocalDate.of(1960, 8, 15))
                .sender(sender)
                .tags(Set.of(tagUrlaub))
                .build());

        photoDoc = documentRepository.save(Document.builder()
                .title("Familienfoto")
                .originalFilename("familienfoto.jpg")
                .status(DocumentStatus.PLACEHOLDER)
                .build());
    }

    // ─── hasSender ────────────────────────────────────────────────────────────

    @Test
    void hasSender_returnsAllDocuments_whenPersonIdIsNull() {
        List<Document> result = documentRepository.findAll(Specification.where(hasSender(null)));
        assertThat(result).hasSize(3);
    }

    @Test
    void hasSender_filtersDocumentsBySender() {
        List<Document> result = documentRepository.findAll(Specification.where(hasSender(sender.getId())));
        assertThat(result).extracting(Document::getTitle)
                .containsExactlyInAnyOrder("Alter Brief", "Neuerer Brief");
    }

    @Test
    void hasSender_returnsEmpty_whenSenderHasNoDocuments() {
        List<Document> result = documentRepository.findAll(Specification.where(hasSender(receiver.getId())));
        assertThat(result).isEmpty();
    }

    // ─── hasReceiver ──────────────────────────────────────────────────────────

    @Test
    void hasReceiver_returnsAllDocuments_whenPersonIdIsNull() {
        List<Document> result = documentRepository.findAll(Specification.where(hasReceiver(null)));
        assertThat(result).hasSize(3);
    }

    @Test
    void hasReceiver_filtersDocumentsByReceiver() {
        List<Document> result = documentRepository.findAll(Specification.where(hasReceiver(receiver.getId())));
        assertThat(result).extracting(Document::getTitle).containsExactly("Alter Brief");
    }

    @Test
    void hasReceiver_returnsEmpty_whenReceiverHasNoDocuments() {
        List<Document> result = documentRepository.findAll(Specification.where(hasReceiver(sender.getId())));
        assertThat(result).isEmpty();
    }

    // ─── isBetween ────────────────────────────────────────────────────────────

    @Test
    void isBetween_returnsAllDocuments_whenBothDatesAreNull() {
        List<Document> result = documentRepository.findAll(Specification.where(isBetween(null, null)));
        assertThat(result).hasSize(3);
    }

    @Test
    void isBetween_filtersByBothDates() {
        List<Document> result = documentRepository.findAll(
                Specification.where(isBetween(LocalDate.of(1939, 1, 1), LocalDate.of(1945, 12, 31))));
        assertThat(result).extracting(Document::getTitle).containsExactly("Alter Brief");
    }

    @Test
    void isBetween_filtersByStartDateOnly() {
        List<Document> result = documentRepository.findAll(
                Specification.where(isBetween(LocalDate.of(1950, 1, 1), null)));
        assertThat(result).extracting(Document::getTitle).containsExactly("Neuerer Brief");
    }

    @Test
    void isBetween_filtersByEndDateOnly() {
        List<Document> result = documentRepository.findAll(
                Specification.where(isBetween(null, LocalDate.of(1945, 12, 31))));
        assertThat(result).extracting(Document::getTitle).containsExactly("Alter Brief");
    }

    @Test
    void isBetween_returnsEmpty_whenNoDatesInRange() {
        List<Document> result = documentRepository.findAll(
                Specification.where(isBetween(LocalDate.of(1970, 1, 1), LocalDate.of(1980, 12, 31))));
        assertThat(result).isEmpty();
    }

    // ─── hasTags ──────────────────────────────────────────────────────────────

    @Test
    void hasTags_returnsAllDocuments_whenTagSetListIsNull() {
        List<Document> result = documentRepository.findAll(Specification.where(hasTags(null, false)));
        assertThat(result).hasSize(3);
    }

    @Test
    void hasTags_returnsAllDocuments_whenTagSetListIsEmpty() {
        List<Document> result = documentRepository.findAll(Specification.where(hasTags(List.of(), false)));
        assertThat(result).hasSize(3);
    }

    @Test
    void hasTags_and_filtersDocumentsByTag() {
        Set<UUID> familieIds = new HashSet<>(tagRepository.findDescendantIdsByName("Familie"));
        List<Document> result = documentRepository.findAll(Specification.where(hasTags(List.of(familieIds), false)));
        assertThat(result).extracting(Document::getTitle).containsExactly("Alter Brief");
    }

    @Test
    void hasTags_and_requiresAllTagsToBePresent() {
        // briefEarly has "Familie" but not "Urlaub" — AND should return empty
        Set<UUID> familieIds = new HashSet<>(tagRepository.findDescendantIdsByName("Familie"));
        Set<UUID> urlaubIds = new HashSet<>(tagRepository.findDescendantIdsByName("Urlaub"));
        List<Document> result = documentRepository.findAll(
                Specification.where(hasTags(List.of(familieIds, urlaubIds), false)));
        assertThat(result).isEmpty();
    }

    @Test
    void hasTags_or_findsDocumentWithEitherTag() {
        Set<UUID> familieIds = new HashSet<>(tagRepository.findDescendantIdsByName("Familie"));
        Set<UUID> urlaubIds = new HashSet<>(tagRepository.findDescendantIdsByName("Urlaub"));
        List<Document> result = documentRepository.findAll(
                Specification.where(hasTags(List.of(familieIds, urlaubIds), true)));
        assertThat(result).extracting(Document::getTitle)
                .containsExactlyInAnyOrder("Alter Brief", "Neuerer Brief");
    }

    @Test
    void hasTags_returnsEmpty_whenTagIdSetIsEmpty() {
        // An empty ID set means the requested tag resolved to nothing — no docs can match
        List<Document> result = documentRepository.findAll(
                Specification.where(hasTags(List.of(new HashSet<>()), false)));
        assertThat(result).isEmpty();
    }

    @Test
    void hasTags_returnsEmpty_whenTagDoesNotExist() {
        // Non-existent tag → findDescendantIdsByName returns empty list → hasTags returns no results
        Set<UUID> unknownIds = new HashSet<>(tagRepository.findDescendantIdsByName("Unbekannt"));
        List<Document> result = documentRepository.findAll(Specification.where(hasTags(List.of(unknownIds), false)));
        assertThat(result).isEmpty();
    }

    // ─── hasTagPartial ────────────────────────────────────────────────────────

    @Test
    void hasTagPartial_returnsAllDocuments_whenTextIsNull() {
        List<Document> result = documentRepository.findAll(Specification.where(hasTagPartial(null)));
        assertThat(result).hasSize(3);
    }

    @Test
    void hasTagPartial_findsByPartialTagName() {
        List<Document> result = documentRepository.findAll(Specification.where(hasTagPartial("amili")));
        assertThat(result).extracting(Document::getTitle).containsExactly("Alter Brief");
    }

    @Test
    void hasTagPartial_isCaseInsensitive() {
        List<Document> result = documentRepository.findAll(Specification.where(hasTagPartial("URLAUB")));
        assertThat(result).extracting(Document::getTitle).containsExactly("Neuerer Brief");
    }

    @Test
    void hasTagPartial_returnsEmpty_whenNoTagMatches() {
        List<Document> result = documentRepository.findAll(Specification.where(hasTagPartial("xyz")));
        assertThat(result).isEmpty();
    }

    // ─── hasStatus ────────────────────────────────────────────────────────────

    @Test
    void hasStatus_returnsAllDocuments_whenStatusIsNull() {
        List<Document> result = documentRepository.findAll(Specification.where(hasStatus(null)));
        assertThat(result).hasSize(3);
    }

    @Test
    void hasStatus_returnsOnlyMatchingDocuments_whenStatusIsSet() {
        List<Document> result = documentRepository.findAll(Specification.where(hasStatus(DocumentStatus.PLACEHOLDER)));
        assertThat(result).extracting(Document::getTitle).containsExactly("Familienfoto");
    }

    @Test
    void hasStatus_returnsEmpty_whenNoDocumentMatchesStatus() {
        List<Document> result = documentRepository.findAll(Specification.where(hasStatus(DocumentStatus.REVIEWED)));
        assertThat(result).isEmpty();
    }

    // ─── undatedOnly ──────────────────────────────────────────────────────────

    @Test
    void undatedOnly_false_returnsAllDocuments() {
        // false → no predicate (null), so the filter is a no-op (issue #668).
        List<Document> result = documentRepository.findAll(Specification.where(undatedOnly(false)));
        assertThat(result).hasSize(3);
    }

    @Test
    void undatedOnly_true_returnsOnlyDocumentsWithoutADate() {
        // Only the placeholder photo has a null documentDate in the fixture.
        List<Document> result = documentRepository.findAll(Specification.where(undatedOnly(true)));
        assertThat(result).extracting(Document::getTitle).containsExactly("Familienfoto");
        assertThat(result).allMatch(d -> d.getDocumentDate() == null);
    }

}
