package de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun;

import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.IOException;
import java.nio.file.Path;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import javafx.application.Platform;

/**
 * Headless (Monocle) Tests, die echte PDF-Dateien rendern, damit die
 * Worker-Thread-Pfade {@code loadAndRenderFirstPageOnWorker} und
 * {@code renderPageOnWorker} tatsächlich ausgeführt werden.
 */
class PdfPreviewPaneRenderingTest {

    private static final long FX_TIMEOUT_SECONDS = 10;
    private static final long WORKER_TIMEOUT_SECONDS = 15;
    private static final AtomicBoolean PLATFORM_STARTED = new AtomicBoolean(false);

    @BeforeAll
    static void startPlatform() throws InterruptedException {
        Platform.setImplicitExit(false);
        if (PLATFORM_STARTED.compareAndSet(false, true)) {
            CountDownLatch latch = new CountDownLatch(1);
            try {
                Platform.startup(latch::countDown);
            } catch (IllegalStateException alreadyStarted) {
                latch.countDown();
            }
            assertTrue(latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS));
        }
    }

    @Test
    void loadSource_realSinglePagePdf_pageLabelShowsRenderedPage(@TempDir Path tempDir) throws Exception {
        Path pdfFile = tempDir.resolve("single-page.pdf");
        createPdfWithPages(pdfFile, 1);

        AtomicReference<PdfPreviewPane> paneRef = new AtomicReference<>();
        CountDownLatch firstPageRendered = new CountDownLatch(1);

        runOnFx(() -> {
            PdfPreviewPane pane = new PdfPreviewPane();
            paneRef.set(pane);
            pane.pageLabel().textProperty().addListener((obs, old, newText) -> {
                if (newText != null && newText.contains("Seite 1 / 1")) {
                    firstPageRendered.countDown();
                }
            });
            pane.loadSource(pdfFile);
        });

        assertTrue(firstPageRendered.await(WORKER_TIMEOUT_SECONDS, TimeUnit.SECONDS),
                "Erste Seite eines einseitigen PDFs muss innerhalb der Worker-Timeout-Frist gerendert werden");

        runOnFx(() -> paneRef.get().shutdown());
    }

    @Test
    void navigateToNextPage_multiPagePdf_rendersSecondPage(@TempDir Path tempDir) throws Exception {
        Path pdfFile = tempDir.resolve("multi-page.pdf");
        createPdfWithPages(pdfFile, 3);

        AtomicReference<PdfPreviewPane> paneRef = new AtomicReference<>();
        CountDownLatch firstPageRendered = new CountDownLatch(1);
        CountDownLatch secondPageRendered = new CountDownLatch(1);
        AtomicBoolean firstSeen = new AtomicBoolean(false);

        runOnFx(() -> {
            PdfPreviewPane pane = new PdfPreviewPane();
            paneRef.set(pane);
            pane.pageLabel().textProperty().addListener((obs, old, newText) -> {
                if (newText == null) {
                    return;
                }
                if (newText.contains("Seite 1 / 3") && firstSeen.compareAndSet(false, true)) {
                    firstPageRendered.countDown();
                } else if (newText.contains("Seite 2 / 3")) {
                    secondPageRendered.countDown();
                }
            });
            pane.loadSource(pdfFile);
        });

        assertTrue(firstPageRendered.await(WORKER_TIMEOUT_SECONDS, TimeUnit.SECONDS),
                "Erste Seite muss innerhalb der Worker-Timeout-Frist gerendert werden");

        // Auf zweite Seite navigieren – triggert renderPageOnWorker
        runOnFx(() -> paneRef.get().nextButton().fire());

        assertTrue(secondPageRendered.await(WORKER_TIMEOUT_SECONDS, TimeUnit.SECONDS),
                "Zweite Seite muss nach Klick auf Weiter gerendert werden");

        runOnFx(() -> paneRef.get().shutdown());
    }

    // -------------------------------------------------------------------------
    // Hilfsmethoden
    // -------------------------------------------------------------------------

    private static void createPdfWithPages(Path outputPath, int pages) throws IOException {
        try (PDDocument doc = new PDDocument()) {
            for (int i = 1; i <= pages; i++) {
                PDPage page = new PDPage();
                doc.addPage(page);
                try (PDPageContentStream stream = new PDPageContentStream(doc, page)) {
                    stream.beginText();
                    stream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
                    stream.newLineAtOffset(50, 700);
                    stream.showText("Testseite " + i);
                    stream.endText();
                }
            }
            doc.save(outputPath.toFile());
        }
    }

    private void runOnFx(Runnable action) throws InterruptedException {
        CountDownLatch done = new CountDownLatch(1);
        AtomicReference<Throwable> error = new AtomicReference<>();
        Platform.runLater(() -> {
            try {
                action.run();
            } catch (Throwable t) {
                error.set(t);
            } finally {
                done.countDown();
            }
        });
        assertTrue(done.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS), "FX-Thread Timeout");
        if (error.get() != null) {
            throw new AssertionError(error.get());
        }
    }
}
