package org.raddatz.familienarchiv.importing;

import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.raddatz.familienarchiv.PostgresContainerConfig;
import org.raddatz.familienarchiv.document.Document;
import org.raddatz.familienarchiv.document.DocumentRepository;
import org.raddatz.familienarchiv.document.DocumentStatus;
import org.raddatz.familienarchiv.person.Person;
import org.raddatz.familienarchiv.person.PersonRepository;
import org.raddatz.familienarchiv.tag.TagRepository;
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.test.util.ReflectionTestUtils;
import software.amazon.awssdk.services.s3.S3Client;

import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;

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

/**
 * Real Postgres (Testcontainers) integration test for the canonical importer. The
 * {@code UNIQUE(source_ref)} constraint and the upsert-on-conflict behaviour only exist
 * in real Postgres (never H2), so idempotency is verified here. S3 is mocked — the
 * synthetic document rows carry no on-disk files, so every document is a PLACEHOLDER and
 * no upload is attempted.
 */
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@ActiveProfiles("test")
@Import(PostgresContainerConfig.class)
class CanonicalImportIntegrationTest {

    @MockitoBean S3Client s3Client;

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

    Path artifactDir;

    @BeforeEach
    void setUp() throws Exception {
        documentRepository.deleteAll();
        personRepository.deleteAll();
        tagRepository.deleteAll();
        artifactDir = Files.createTempDirectory("canonical-import-it");
        writeArtifacts(artifactDir);
        ReflectionTestUtils.setField(orchestrator, "canonicalDir", artifactDir.toString());
    }

    /**
     * The import commits through its own transactions (the orchestrator is not transactional),
     * so this test cannot rely on {@code @Transactional} rollback for isolation. Delete the
     * committed rows after each test — otherwise the last test's documents (dated 1888-02) and
     * persons/tags leak into the shared Testcontainers Postgres and pollute other integration
     * tests that assume a known seed (e.g. DocumentDensityIntegrationTest,
     * DocumentSearchPagedIntegrationTest). Mirrors the @AfterEach deleteAll convention used by
     * DocumentListItemIntegrationTest.
     */
    @AfterEach
    void cleanup() {
        documentRepository.deleteAll();
        personRepository.deleteAll();
        tagRepository.deleteAll();
    }

    @Test
    void reimport_isIdempotent_noDuplicatePersonsTagsOrDocuments() {
        orchestrator.runImport();
        long personsAfterFirst = personRepository.count();
        long tagsAfterFirst = tagRepository.count();
        long documentsAfterFirst = documentRepository.count();
        assertThat(orchestrator.getStatus().state()).isEqualTo(ImportStatus.State.DONE);
        assertThat(personsAfterFirst).isPositive();
        assertThat(tagsAfterFirst).isPositive();
        assertThat(documentsAfterFirst).isPositive();

        orchestrator.runImport();

        assertThat(personRepository.count()).isEqualTo(personsAfterFirst);
        assertThat(tagRepository.count()).isEqualTo(tagsAfterFirst);
        assertThat(documentRepository.count()).isEqualTo(documentsAfterFirst);
    }

    @Test
    void reimport_preservesHumanEditedPersonField() {
        orchestrator.runImport();
        Person walter = personRepository.findBySourceRef("de-gruyter-walter").orElseThrow();
        walter.setNotes("Verified by archivist");
        walter.setFirstName("Walther");
        personRepository.save(walter);

        orchestrator.runImport();

        Person reimported = personRepository.findBySourceRef("de-gruyter-walter").orElseThrow();
        assertThat(reimported.getNotes()).isEqualTo("Verified by archivist");
        assertThat(reimported.getFirstName()).isEqualTo("Walther");
    }

    @Test
    void import_linksDocumentSenderToRegisterPerson_andRetainsRawText() {
        orchestrator.runImport();

        Person walter = personRepository.findBySourceRef("de-gruyter-walter").orElseThrow();
        Document doc = documentRepository.findByOriginalFilename("W-0001").orElseThrow();
        assertThat(doc.getSender()).isNotNull();
        assertThat(doc.getSender().getId()).isEqualTo(walter.getId());
        assertThat(doc.getSenderText()).isEqualTo("Walter de Gruyter");
        assertThat(doc.getStatus()).isEqualTo(DocumentStatus.PLACEHOLDER);
    }

    @Test
    void import_provisionalFlag_trueForImporterCreated_falseForRegister() {
        orchestrator.runImport();

        Optional<Person> register = personRepository.findBySourceRef("de-gruyter-walter");
        assertThat(register).get().extracting(Person::isProvisional).isEqualTo(false);
    }

    @Test
    void reimport_prunesRemovedReceiverAndTag_whenCanonicalRowShrinks() throws Exception {
        orchestrator.runImport();
        // findById uses the Document.full entity graph so receivers/tags initialise eagerly.
        Document before = documentRepository.findById(
                documentRepository.findByOriginalFilename("W-0001").orElseThrow().getId()).orElseThrow();
        assertThat(before.getReceivers()).isNotEmpty();
        assertThat(before.getTags()).isNotEmpty();

        // Re-stage the document sheet with W-0001's receiver and tag removed.
        writeSheet(artifactDir.resolve("canonical-documents.xlsx"),
                List.of("index", "sender_person_id", "sender_name", "receiver_person_ids",
                        "receiver_names", "date_iso", "date_raw", "date_precision", "date_end", "location", "tags", "summary"),
                List.of(
                        List.of("W-0001", "de-gruyter-walter", "Walter de Gruyter",
                                "", "", "1888-02-15", "15.2.1888", "DAY", "", "Rotterdam", "", "Geschäftsreise"),
                        List.of("W-0002", "de-gruyter-eugenie", "Eugenie de Gruyter",
                                "de-gruyter-walter", "Walter de Gruyter", "1888-02-16", "16.2.1888", "DAY", "",
                                "Middelburg", "Themen/Brautbriefe", "Reisepläne")));

        orchestrator.runImport();

        Document after = documentRepository.findById(before.getId()).orElseThrow();
        assertThat(after.getReceivers()).isEmpty();
        assertThat(after.getTags()).isEmpty();
    }

    @Test
    void import_neverFlipsRegisterPersonToProvisional_whenReferencedByDocumentRow() {
        // de-gruyter-walter is a register person (provisional=false) AND the sender of W-0001.
        // The orchestrator loads the register before documents, so the document loader's
        // register-first match links the existing person and never mints a provisional one.
        // A second run (documents reference the same person again) must not flip it true.
        orchestrator.runImport();
        orchestrator.runImport();

        Person walter = personRepository.findBySourceRef("de-gruyter-walter").orElseThrow();
        assertThat(walter.isProvisional()).isFalse();
        Person eugenie = personRepository.findBySourceRef("de-gruyter-eugenie").orElseThrow();
        assertThat(eugenie.isProvisional()).isFalse();
    }

    // ─── synthetic-but-real artifact set ─────────────────────────────────────────────

    private void writeArtifacts(Path dir) throws Exception {
        writeSheet(dir.resolve("canonical-tag-tree.xlsx"),
                List.of("tag_path", "parent_name", "tag_name"),
                List.of(
                        List.of("Themen", "", "Themen"),
                        List.of("Themen/Brautbriefe", "Themen", "Brautbriefe")));

        writeSheet(dir.resolve("canonical-persons.xlsx"),
                List.of("person_id", "last_name", "first_name", "maiden_name", "notes", "birth_date", "death_date", "provisional"),
                List.of(
                        List.of("de-gruyter-walter", "de Gruyter", "Walter", "", "", "1865-01-01", "", "False"),
                        List.of("de-gruyter-eugenie", "de Gruyter", "Eugenie", "Wöhler", "", "", "", "False")));

        Files.writeString(dir.resolve("canonical-persons-tree.json"), """
                {"persons":[
                  {"rowId":"row_1","firstName":"Walter","lastName":"de Gruyter","familyMember":true,"personId":"de-gruyter-walter"},
                  {"rowId":"row_2","firstName":"Eugenie","lastName":"de Gruyter","maidenName":"Wöhler","familyMember":true,"personId":"de-gruyter-eugenie"}
                ],"relationships":[
                  {"personId":"row_1","relatedPersonId":"row_2","type":"SPOUSE_OF","source":"verheiratet_mit"}
                ]}
                """);

        writeSheet(dir.resolve("canonical-documents.xlsx"),
                List.of("index", "sender_person_id", "sender_name", "receiver_person_ids",
                        "receiver_names", "date_iso", "date_raw", "date_precision", "date_end", "location", "tags", "summary"),
                List.of(
                        List.of("W-0001", "de-gruyter-walter", "Walter de Gruyter",
                                "de-gruyter-eugenie", "Eugenie de Gruyter", "1888-02-15", "15.2.1888", "DAY", "",
                                "Rotterdam", "Themen/Brautbriefe", "Geschäftsreise"),
                        List.of("W-0002", "de-gruyter-eugenie", "Eugenie de Gruyter",
                                "de-gruyter-walter", "Walter de Gruyter", "1888-02-16", "16.2.1888", "DAY", "",
                                "Middelburg", "Themen/Brautbriefe", "Reisepläne")));
    }

    private void writeSheet(Path file, List<String> headers, List<List<String>> rows) throws Exception {
        try (XSSFWorkbook wb = new XSSFWorkbook()) {
            Sheet sheet = wb.createSheet("Sheet1");
            Row header = sheet.createRow(0);
            for (int i = 0; i < headers.size(); i++) {
                header.createCell(i).setCellValue(headers.get(i));
            }
            for (int r = 0; r < rows.size(); r++) {
                Row row = sheet.createRow(r + 1);
                List<String> values = rows.get(r);
                for (int c = 0; c < values.size(); c++) {
                    row.createCell(c).setCellValue(values.get(c));
                }
            }
            try (OutputStream out = Files.newOutputStream(file)) {
                wb.write(out);
            }
        }
    }
}
