package org.raddatz.familienarchiv.tag;

import jakarta.persistence.EntityManager;
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.DocumentRepository;
import org.raddatz.familienarchiv.document.DocumentSpecifications;
import org.raddatz.familienarchiv.document.DocumentStatus;
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 java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;

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

/**
 * Real-Postgres validation of the subtree document-count rollup ({@link TagRepository
 * #findSubtreeDocumentCountsPerTag}). The recursive CTE + COUNT(DISTINCT) cannot be exercised on
 * H2, so these run against {@code postgres:16-alpine} via Testcontainers. Covers issue #698
 * AC#1–#4, #6 (REQ-THEMEN-06 cycle guard) and #7 (count↔destination parity).
 */
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import({PostgresContainerConfig.class, FlywayConfig.class})
class TagRollupRepositoryIntegrationTest {

    @Autowired private TagRepository tagRepository;
    @Autowired private DocumentRepository documentRepository;
    @Autowired private EntityManager entityManager;

    // ─── helpers ──────────────────────────────────────────────────────────────

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

    private Document docWithTags(String title, Tag... tags) {
        return documentRepository.save(Document.builder()
                .title(title)
                .originalFilename(title + ".pdf")
                .status(DocumentStatus.UPLOADED)
                .tags(new HashSet<>(Set.of(tags)))
                .build());
    }

    private Map<UUID, Long> rollup() {
        entityManager.flush();
        entityManager.clear();
        return tagRepository.findSubtreeDocumentCountsPerTag().stream()
                .collect(Collectors.toMap(TagRepository.TagCount::getTagId, TagRepository.TagCount::getCount));
    }

    // ─── AC#4 — rollup of a leaf equals its direct count ────────────────────────

    @Test
    void leafTag_subtreeCount_equalsItsDirectCount() {
        Tag leaf = tag("Tagebuch", null);
        docWithTags("a", leaf);
        docWithTags("b", leaf);
        docWithTags("c", leaf);

        assertThat(rollup().get(leaf.getId())).isEqualTo(3L);
    }

    // ─── AC#1 + AC#2 — parent rolls up children, distinct (shared doc counted once) ──

    @Test
    void parentTag_rollsUpChildDocuments_countingSharedDocumentOnce() {
        Tag reisen = tag("Reisen", null);
        Tag italien = tag("Italien", reisen.getId());

        Document shared = docWithTags("shared", reisen, italien); // tagged with both
        docWithTags("reisenOnly", reisen);
        docWithTags("it1", italien);
        docWithTags("it2", italien);
        docWithTags("it3", italien);
        docWithTags("it4", italien);

        Map<UUID, Long> rollup = rollup();

        // Reisen direct {shared, reisenOnly} = 2; Italien {shared, it1..it4} = 5; union distinct = 6
        assertThat(rollup.get(reisen.getId())).isEqualTo(6L);
        assertThat(rollup.get(italien.getId())).isEqualTo(5L);
        assertThat(shared.getId()).isNotNull();
    }

    // ─── AC#3 — full descendant depth (grandchildren included) ──────────────────

    @Test
    void rollup_includesGrandchildDocuments_atFullDepth() {
        Tag reisen = tag("Reisen", null);
        Tag italien = tag("Italien", reisen.getId());
        Tag rom = tag("Rom", italien.getId());

        docWithTags("r1", reisen);
        docWithTags("i1", italien);
        docWithTags("rom1", rom);
        docWithTags("rom2", rom);
        docWithTags("rom3", rom);

        Map<UUID, Long> rollup = rollup();

        assertThat(rollup.get(reisen.getId())).isEqualTo(5L); // 1 + 1 + 3, all distinct
        assertThat(rollup.get(italien.getId())).isEqualTo(4L); // 1 + 3
        assertThat(rollup.get(rom.getId())).isEqualTo(3L);
    }

    // ─── REQ-THEMEN-05 — a tag whose whole subtree is empty is absent (→ 0) ─────

    @Test
    void tagWithEmptySubtree_isAbsentFromRollup() {
        Tag empty = tag("Leer", null);
        Tag emptyChild = tag("LeerKind", empty.getId());

        Map<UUID, Long> rollup = rollup();

        assertThat(rollup).doesNotContainKey(empty.getId());
        assertThat(rollup).doesNotContainKey(emptyChild.getId());
    }

    // ─── REQ-THEMEN-06 — a hierarchy cycle terminates safely via the depth guard ──

    @Test
    void rollup_terminatesSafely_whenHierarchyContainsCycle() {
        Tag a = tag("CycleA", null);
        Tag b = tag("CycleB", a.getId());
        // Close the loop: A.parent = B (DB only forbids parent_id == id, so a 2-node cycle is insertable)
        a.setParentId(b.getId());
        tagRepository.save(a);

        docWithTags("ca", a);
        docWithTags("cb", b);

        assertThatCode(this::rollup).doesNotThrowAnyException(); // depth guard prevents a runaway recursion
        Map<UUID, Long> rollup = rollup();

        // COUNT(DISTINCT document_id) dedupes documents reached via repeated cycle paths
        assertThat(rollup.get(a.getId())).isEqualTo(2L);
        assertThat(rollup.get(b.getId())).isEqualTo(2L);
    }

    // ─── AC#7 — count↔destination parity with the real search expansion ─────────

    @Test
    void subtreeCount_equalsDistinctDocumentsFoundByTagSearch_parity() {
        // Uniquely-named root so name-based search expansion lines up with the per-id rollup.
        Tag root = tag("ZzzParitaetReise", null);
        Tag child = tag("ZzzParitaetItalien", root.getId());
        Tag grandchild = tag("ZzzParitaetRom", child.getId());

        docWithTags("p_shared", root, child); // overlap inside the subtree
        docWithTags("p_root", root);
        docWithTags("p_child", child);
        docWithTags("p_gc1", grandchild);
        docWithTags("p_gc2", grandchild);

        entityManager.flush();
        entityManager.clear();

        long rollupCount = rollup().get(root.getId());

        List<UUID> searchExpansionIds = tagRepository.findDescendantIdsByName("ZzzParitaetReise");
        var spec = DocumentSpecifications.hasTags(List.of(new HashSet<>(searchExpansionIds)), true);
        long distinctSearchResults = documentRepository.findAll(spec).stream()
                .map(Document::getId).distinct().count();

        assertThat(rollupCount).isEqualTo(distinctSearchResults);
    }
}
