package org.raddatz.familienarchiv.document;

import org.junit.jupiter.api.Test;
import org.raddatz.familienarchiv.PostgresContainerConfig;
import org.raddatz.familienarchiv.tag.Tag;
import org.raddatz.familienarchiv.tag.TagRepository;
import org.raddatz.familienarchiv.tag.TagService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.transaction.annotation.Transactional;
import software.amazon.awssdk.services.s3.S3Client;

import java.time.LocalDate;
import java.util.Comparator;
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.assertj.core.api.Assertions.assertThatCode;

/**
 * #730 — tag-name resolution against a real Postgres. A mocked repo can't prove the two things that
 * actually break: that {@code findAllByNameIgnoreCase} folds case the way Postgres {@code LOWER()}
 * does (critical for umlauts like {@code ü}), and that saving a document tagged with a case-colliding
 * tag no longer throws {@code NonUniqueResultException}. H2 folds case differently, so this pins the
 * behaviour on {@code postgres:16-alpine}. The four-branch resolution logic itself is covered faster
 * by the mocked {@code TagServiceTest}.
 */
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@ActiveProfiles("test")
@Import(PostgresContainerConfig.class)
@Transactional
class TagCaseCollisionIntegrationTest {

    @MockitoBean S3Client s3Client;
    @Autowired DocumentService documentService;
    @Autowired DocumentRepository documentRepository;
    @Autowired TagRepository tagRepository;
    @Autowired TagService tagService;

    private Tag persistTag(String name, String sourceRef, UUID parentId) {
        return tagRepository.save(Tag.builder().name(name).sourceRef(sourceRef).parentId(parentId).build());
    }

    private Document persistDocTaggedWith(Tag tag) {
        return documentRepository.save(Document.builder()
                .originalFilename("C-7301")
                .title("Weihnachtsbrief")
                .documentDate(LocalDate.of(1928, 1, 1))
                .metaDatePrecision(DatePrecision.YEAR)
                .status(DocumentStatus.UPLOADED)
                .tags(new HashSet<>(Set.of(tag)))
                .build());
    }

    @Test
    void updateDocument_succeedsAndKeepsExactChildTag_whenTaggedWithCaseCollidingChild() throws Exception {
        Tag parent = persistTag("Weihnachten", "Weihnachten", null);
        Tag child = persistTag("weihnachten", "Weihnachten/weihnachten", parent.getId());
        Document doc = persistDocTaggedWith(child);

        DocumentUpdateDTO dto = new DocumentUpdateDTO();
        dto.setTitle("Weihnachtsbrief");
        dto.setDocumentDate(LocalDate.of(1930, 1, 1)); // change the date — the field that 500'd on staging
        dto.setMetaDatePrecision(DatePrecision.YEAR);
        dto.setTags("weihnachten"); // the edit form round-trips the stored child name

        assertThatCode(() -> documentService.updateDocument(doc.getId(), dto, null, null))
                .doesNotThrowAnyException();

        Set<Tag> tags = documentRepository.findById(doc.getId()).orElseThrow().getTags();
        assertThat(tags).hasSize(1);
        assertThat(tags.iterator().next().getId()).isEqualTo(child.getId()); // child kept, not the parent
    }

    @Test
    void findOrCreate_resolvesUmlautCollisionDeterministically_withoutThrow() {
        // The regression catcher: a plain-ASCII pair would stay green even if Postgres folded ü wrongly.
        Tag parent = persistTag("Glückwünsche", "Glückwünsche", null);
        Tag child = persistTag("glückwünsche", "Glückwünsche/glückwünsche", parent.getId());

        // Proof that real Postgres LOWER() folds the umlaut so both rows match case-insensitively.
        // Query with the UPPERCASE form findOrCreate actually passes — folding LOWER('GLÜCKWÜNSCHE')
        // against LOWER(name) is the exact step under test; a lowercase probe wouldn't exercise it.
        assertThat(tagRepository.findAllByNameIgnoreCase("GLÜCKWÜNSCHE")).hasSize(2);

        // No exact-case "GLÜCKWÜNSCHE" row exists → resolution falls through to the case-insensitive
        // branch with two candidates and must pick the lowest id deterministically, never throwing.
        UUID expected = List.of(parent, child).stream().min(Comparator.comparing(Tag::getId)).orElseThrow().getId();
        Tag first = tagService.findOrCreate("GLÜCKWÜNSCHE");
        Tag second = tagService.findOrCreate("GLÜCKWÜNSCHE");

        assertThat(first.getId()).isEqualTo(expected);
        assertThat(second.getId()).isEqualTo(first.getId());
    }

    @Test
    void bulkEdit_resolvesCaseCollidingTagThroughFindOrCreate_withoutThrow() {
        // Bulk-edit shares resolveTags → findOrCreate; this guards a future refactor that bypasses it.
        Tag parent = persistTag("Weihnachten", "Weihnachten", null);
        Tag child = persistTag("weihnachten", "Weihnachten/weihnachten", parent.getId());
        Document doc = documentRepository.save(Document.builder()
                .originalFilename("C-7302")
                .title("Brief")
                .status(DocumentStatus.UPLOADED)
                .build());

        DocumentBulkEditDTO dto = new DocumentBulkEditDTO();
        dto.setTagNames(List.of("weihnachten"));

        assertThatCode(() -> documentService.applyBulkEditToDocument(doc.getId(), dto, null))
                .doesNotThrowAnyException();

        Set<Tag> tags = documentRepository.findById(doc.getId()).orElseThrow().getTags();
        assertThat(tags).hasSize(1);
        assertThat(tags.iterator().next().getId()).isEqualTo(child.getId());
    }
}
