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

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

import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiMessageEntry;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CheckpointId;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CheckpointResult;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CheckpointSeverity;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.TechnicalTestReport;
import javafx.application.Platform;

/**
 * Monocle-basierte headless Smoke-Tests für {@link GuiCorrectionDialogCoordinator}.
 * <p>
 * Prüft folgende Szenarien:
 * <ul>
 *   <li>Bericht mit korrigierbaren Befunden → Dialog wird angefragt, bei Bestätigung
 *       werden Korrekturen ausgeführt, Meldungen erscheinen.</li>
 *   <li>Bericht ohne korrigierbare Befunde → kein Dialog, keine Korrekturen.</li>
 *   <li>Bei Dialog-Abbruch → keine Korrekturen, keine Meldungen mit Source „Korrekturen".</li>
 * </ul>
 * <p>
 * Der {@code correctionThreadFactory} wird auf synchrone Ausführung umgestellt und
 * {@code resultDelivery} auf direkten Aufruf, damit Ergebnisse sofort nach
 * {@code offerCorrections()} verfügbar sind.
 */
class GuiCorrectionDialogCoordinatorSmokeTest {

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

    @BeforeAll
    static void setUpJavaFxPlatform() throws InterruptedException {
        Platform.setImplicitExit(false);
        CountDownLatch latch = new CountDownLatch(1);
        try {
            Platform.startup(() -> {
                PLATFORM_STARTED.set(true);
                latch.countDown();
            });
            assertTrue(latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS),
                    "JavaFX Platform must start within timeout");
        } catch (IllegalStateException alreadyStarted) {
            CountDownLatch verifyLatch = new CountDownLatch(1);
            Platform.runLater(() -> {
                PLATFORM_STARTED.set(true);
                verifyLatch.countDown();
            });
            assertTrue(verifyLatch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS),
                    "Existing JavaFX Platform must be reachable within timeout");
        }
    }

    @AfterAll
    static void tearDownJavaFxPlatform() {
        // Shared platform – do not call Platform.exit().
    }

    // =========================================================================
    // Scenario: report with correctable findings → dialog shown, corrections applied
    // =========================================================================

    /**
     * Smoke-Test: Bei korrigierbaren Befunden und Dialog-Bestätigung werden Korrekturen
     * ausgeführt und als Meldungen eingehängt.
     */
    @Test
    void offerCorrections_withCorrectableFindings_dialogConfirmed_appliesCorrectionAndAddsMessages()
            throws Exception {
        runOnFx(() -> {
            List<GuiMessageEntry> messages = new ArrayList<>();
            AtomicBoolean correctionExecuted = new AtomicBoolean(false);

            CorrectionExecutionService service = buildServiceThatTracksExecution(correctionExecuted);
            GuiCorrectionDialogCoordinator coordinator = buildSyncCoordinator(
                    service, messages, true /* confirm */);

            TechnicalTestReport report = buildReportWithCorrectableFinding();
            coordinator.offerCorrections(report);

            assertTrue(correctionExecuted.get(),
                    "Korrektur muss nach Bestätigung ausgeführt worden sein");

            long correctionEntries = messages.stream()
                    .filter(m -> m.source().isPresent()
                            && GuiCorrectionDialogCoordinator.SOURCE_TAG.equals(m.source().get()))
                    .count();
            assertTrue(correctionEntries > 0,
                    "Nach Ausführung müssen Meldungen mit Source '"
                            + GuiCorrectionDialogCoordinator.SOURCE_TAG + "' vorhanden sein");
        });
    }

    // =========================================================================
    // Scenario: report without correctable findings → no dialog, no corrections
    // =========================================================================

    /**
     * Smoke-Test: Bericht ohne korrigierbare Befunde → kein Dialog, keine Korrekturen.
     */
    @Test
    void offerCorrections_withoutCorrectableFindings_noDialogNoCorrections() throws Exception {
        runOnFx(() -> {
            List<GuiMessageEntry> messages = new ArrayList<>();
            AtomicBoolean dialogShown = new AtomicBoolean(false);
            AtomicBoolean correctionExecuted = new AtomicBoolean(false);

            CorrectionExecutionService service = buildServiceThatTracksExecution(correctionExecuted);
            GuiCorrectionDialogCoordinator coordinator = buildSyncCoordinator(
                    service, messages, true /* confirm */);
            coordinator.dialogSupplier = content -> {
                dialogShown.set(true);
                return true;
            };

            // Report with NO correctable findings (all succeed)
            TechnicalTestReport report = buildReportWithNoCorrectableFindings();
            coordinator.offerCorrections(report);

            assertFalse(dialogShown.get(), "Kein Dialog darf angezeigt werden wenn keine Korrekturen möglich");
            assertFalse(correctionExecuted.get(), "Keine Korrektur darf ausgeführt werden");
            assertTrue(messages.isEmpty(), "Keine Meldungen dürfen hinzugefügt werden");
        });
    }

    // =========================================================================
    // Scenario: dialog cancelled → no corrections, no messages with SOURCE_TAG
    // =========================================================================

    /**
     * Smoke-Test: Dialog-Abbruch → keine Korrekturen, keine Meldungen mit Source-Tag.
     */
    @Test
    void offerCorrections_dialogCancelled_noCorrectionsNoMessages() throws Exception {
        runOnFx(() -> {
            List<GuiMessageEntry> messages = new ArrayList<>();
            AtomicBoolean correctionExecuted = new AtomicBoolean(false);

            CorrectionExecutionService service = buildServiceThatTracksExecution(correctionExecuted);
            GuiCorrectionDialogCoordinator coordinator = buildSyncCoordinator(
                    service, messages, false /* cancel */);

            TechnicalTestReport report = buildReportWithCorrectableFinding();
            coordinator.offerCorrections(report);

            assertFalse(correctionExecuted.get(),
                    "Bei Dialog-Abbruch dürfen keine Korrekturen ausgeführt werden");

            long correctionEntries = messages.stream()
                    .filter(m -> m.source().isPresent()
                            && GuiCorrectionDialogCoordinator.SOURCE_TAG.equals(m.source().get()))
                    .count();
            assertEquals(0, correctionEntries,
                    "Bei Dialog-Abbruch dürfen keine Meldungen mit Source '"
                            + GuiCorrectionDialogCoordinator.SOURCE_TAG + "' hinzugefügt werden");
        });
    }

    // =========================================================================
    // Scenario: replace semantics – second run replaces previous SOURCE_TAG entries
    // =========================================================================

    /**
     * Smoke-Test: Beim zweiten Aufruf werden vorherige SOURCE_TAG-Einträge ersetzt.
     */
    @Test
    void offerCorrections_calledTwice_replacesPreviousMessages() throws Exception {
        runOnFx(() -> {
            List<GuiMessageEntry> messages = new ArrayList<>();
            AtomicBoolean ignored = new AtomicBoolean(false);

            CorrectionExecutionService service = buildServiceThatTracksExecution(ignored);
            GuiCorrectionDialogCoordinator coordinator = buildSyncCoordinator(
                    service, messages, true);

            TechnicalTestReport report = buildReportWithCorrectableFinding();
            coordinator.offerCorrections(report);
            long countAfterFirst = messages.stream()
                    .filter(m -> m.source().isPresent()
                            && GuiCorrectionDialogCoordinator.SOURCE_TAG.equals(m.source().get()))
                    .count();

            coordinator.offerCorrections(report);
            long countAfterSecond = messages.stream()
                    .filter(m -> m.source().isPresent()
                            && GuiCorrectionDialogCoordinator.SOURCE_TAG.equals(m.source().get()))
                    .count();

            assertEquals(countAfterFirst, countAfterSecond,
                    "Zweiter Aufruf muss vorherige Einträge ersetzen (Replace-Semantik)");
        });
    }

    // =========================================================================
    // Helpers
    // =========================================================================

    /**
     * Baut einen synchron laufenden {@link GuiCorrectionDialogCoordinator}.
     * <p>
     * Thread-Factory läuft inline; resultDelivery ist direkter Aufruf; dialogSupplier
     * gibt den festen {@code confirm}-Wert zurück.
     */
    private static GuiCorrectionDialogCoordinator buildSyncCoordinator(
            CorrectionExecutionService service,
            List<GuiMessageEntry> messages,
            boolean confirm) {

        GuiCorrectionDialogCoordinator coordinator = new GuiCorrectionDialogCoordinator(
                service,
                messages,
                ignored -> { /* no-op refresh */ });

        coordinator.dialogSupplier = content -> confirm;
        coordinator.correctionThreadFactory = task -> new Thread(task, "sync-correction-thread") {
            @Override
            public void start() {
                run(); // inline, synchronous
            }
        };
        coordinator.resultDelivery = Runnable::run; // direct call, no FX queue

        return coordinator;
    }

    /**
     * Baut einen {@link CorrectionExecutionService}, der {@code correctionExecuted} auf {@code true}
     * setzt, wenn eine Korrektur aufgerufen wird.
     */
    private static CorrectionExecutionService buildServiceThatTracksExecution(
            AtomicBoolean correctionExecuted) {
        ResourceCreationPort trackingPort = new ResourceCreationPort() {
            @Override
            public CorrectionOutcome createDirectory(CorrectionSuggestion.CreateDirectory s) {
                correctionExecuted.set(true);
                return new CorrectionOutcome.Applied(s, "Angelegt");
            }
            @Override
            public CorrectionOutcome createPromptFile(CorrectionSuggestion.CreatePromptFile s) {
                correctionExecuted.set(true);
                return new CorrectionOutcome.Applied(s, "Erzeugt");
            }
            @Override
            public CorrectionOutcome prepareSqlitePath(CorrectionSuggestion.PrepareSqlitePath s) {
                correctionExecuted.set(true);
                return new CorrectionOutcome.Applied(s, "Vorbereitet");
            }
        };
        return new CorrectionExecutionService(trackingPort);
    }

    /** Baut einen Bericht mit einem korrigierbaren Fehler-Befund. */
    private static TechnicalTestReport buildReportWithCorrectableFinding() {
        CorrectionSuggestion.CreateDirectory suggestion =
                new CorrectionSuggestion.CreateDirectory("C:/test/target", "Zielordner anlegen: C:/test/target");
        CheckpointResult.Failure failure = new CheckpointResult.Failure(
                CheckpointId.TARGET_FOLDER_USABLE,
                CheckpointSeverity.ERROR,
                "Zielordner nicht vorhanden",
                Optional.of(suggestion));
        return new TechnicalTestReport(List.of(failure), Instant.now());
    }

    /** Baut einen Bericht ohne korrigierbare Befunde (alles erfolgreich). */
    private static TechnicalTestReport buildReportWithNoCorrectableFindings() {
        CheckpointResult.Success success = new CheckpointResult.Success(
                CheckpointId.TARGET_FOLDER_USABLE,
                "Zielordner vorhanden");
        return new TechnicalTestReport(List.of(success), Instant.now());
    }

    private static void runOnFx(ThrowingRunnable task) throws Exception {
        java.util.concurrent.atomic.AtomicReference<Throwable> error =
                new java.util.concurrent.atomic.AtomicReference<>();
        CountDownLatch latch = new CountDownLatch(1);
        Platform.runLater(() -> {
            try {
                task.run();
            } catch (Throwable t) {
                error.set(t);
            } finally {
                latch.countDown();
            }
        });
        assertTrue(latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS),
                "FX task must complete within timeout");
        Throwable t = error.get();
        if (t != null) {
            if (t instanceof Exception ex) throw ex;
            throw new AssertionError("Unexpected error", t);
        }
    }

    @FunctionalInterface
    private interface ThrowingRunnable {
        void run() throws Exception;
    }
}
