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.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

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

import de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingFailure;
import de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingResult;
import de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingSuccess;
import de.gecheckt.pdf.umbenenner.application.port.out.PromptSaveResult;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion;
import de.gecheckt.pdf.umbenenner.domain.model.PromptIdentifier;
import javafx.application.Platform;
import javafx.scene.control.Tab;

/**
 * Monocle-basierte Headless-Smoke-Tests für {@link GuiPromptEditorTab}.
 * <p>
 * Geprüfte Szenarien:
 * <ul>
 *   <li>Tab wird korrekt mit Titel „Prompt" erstellt.</li>
 *   <li>Dirty-State ist nach Konstruktion {@code false}.</li>
 *   <li>Nach synchronem Laden mit Erfolg: Dirty-State bleibt {@code false},
 *       Tab-Titel enthält keinen Asterisk.</li>
 *   <li>Nach synchronem Laden mit FILE_NOT_FOUND: Dirty-State bleibt {@code false}.</li>
 *   <li>Nach synchronem Speichern mit Erfolg: Dirty-State zurückgesetzt.</li>
 *   <li>Nach {@code resetToDefault}: Textfeld enthält Default-Inhalt (nicht leer),
 *       Dirty-State ist {@code true} (Abweichung von geladener Baseline).</li>
 * </ul>
 */
class GuiPromptEditorTabSmokeTest {

    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 muss innerhalb des Timeouts starten");
        } catch (IllegalStateException alreadyStarted) {
            CountDownLatch verifyLatch = new CountDownLatch(1);
            Platform.runLater(() -> {
                PLATFORM_STARTED.set(true);
                verifyLatch.countDown();
            });
            assertTrue(verifyLatch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS),
                    "Vorhandene JavaFX-Platform muss innerhalb des Timeouts erreichbar sein");
        }
    }

    @AfterAll
    static void tearDownJavaFxPlatform() {
        // Gemeinsame Platform – kein Platform.exit().
    }

    // =========================================================================
    // Hilfsklassen
    // =========================================================================

    /** Synchroner Stub-Port: gibt vorbereitete Ergebnisse sofort zurück. */
    private static class SyncPromptEditorPort implements GuiPromptEditorPort {
        PromptLoadingResult loadResult = new PromptLoadingSuccess(
                new PromptIdentifier("test-prompt.txt"), "Stub-Prompt-Inhalt");
        PromptSaveResult saveResult = new PromptSaveResult.Saved("/stub/test-prompt.txt");

        @Override
        public PromptLoadingResult loadCurrentPrompt() {
            return loadResult;
        }

        @Override
        public PromptSaveResult save(String content) {
            return saveResult;
        }

        @Override
        public CorrectionOutcome createDefaultPromptIfMissing(
                CorrectionSuggestion.CreatePromptFile suggestion) {
            return new CorrectionOutcome.Applied(suggestion, "Stub-Prompt-Datei angelegt.");
        }
    }

    /**
     * Erstellt einen {@link GuiPromptEditorTab} mit synchronen Stubs:
     * threadFactory führt den Runnable inline aus (vor worker.start()),
     * fxDispatcher gibt den UI-Update-Runnable direkt weiter (kein Platform.runLater).
     * Damit sind alle Operationen aus Testsicht vollständig synchron.
     */
    private static GuiPromptEditorTab buildSyncTab(SyncPromptEditorPort port) {
        GuiPromptEditorTab tab = new GuiPromptEditorTab(port, "/stub/test-prompt.txt", 60);
        // Runnable wird inline ausgeführt; der zurückgegebene Thread startet leer (kein-op).
        tab.threadFactory = runnable -> {
            runnable.run(); // Synchron ausführen, inkl. fxDispatcher-Aufruf
            return new Thread(); // Dummy-Thread; worker.start() beendet sofort
        };
        // UI-Updates synchron im selben Thread
        tab.fxDispatcher = Runnable::run;
        return tab;
    }

    // =========================================================================
    // Tests
    // =========================================================================

    @Test
    void tab_shouldBeCreatedWithTitlePrompt() throws Exception {
        CountDownLatch latch = new CountDownLatch(1);
        AtomicReference<Throwable> fxError = new AtomicReference<>();
        AtomicReference<Tab> tabRef = new AtomicReference<>();

        Platform.runLater(() -> {
            try {
                SyncPromptEditorPort port = new SyncPromptEditorPort();
                GuiPromptEditorTab editorTab = buildSyncTab(port);
                tabRef.set(editorTab.tab());
            } catch (Throwable t) {
                fxError.set(t);
            } finally {
                latch.countDown();
            }
        });

        assertTrue(latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS));
        if (fxError.get() != null) {
            throw new AssertionError("FX-Thread hat eine Ausnahme geworfen", fxError.get());
        }
        assertNotNull(tabRef.get(), "Tab darf nicht null sein");
        assertEquals("Prompt", tabRef.get().getText(), "Tab-Titel muss 'Prompt' sein");
    }

    @Test
    void dirtyState_shouldBeFalse_afterConstruction() throws Exception {
        CountDownLatch latch = new CountDownLatch(1);
        AtomicReference<Throwable> fxError = new AtomicReference<>();
        AtomicBoolean dirtyRef = new AtomicBoolean(true);

        Platform.runLater(() -> {
            try {
                SyncPromptEditorPort port = new SyncPromptEditorPort();
                GuiPromptEditorTab editorTab = buildSyncTab(port);
                dirtyRef.set(editorTab.hasDirtyContent());
            } catch (Throwable t) {
                fxError.set(t);
            } finally {
                latch.countDown();
            }
        });

        assertTrue(latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS));
        if (fxError.get() != null) {
            throw new AssertionError("FX-Thread hat eine Ausnahme geworfen", fxError.get());
        }
        assertFalse(dirtyRef.get(), "Dirty-State muss nach Konstruktion false sein");
    }

    @Test
    void dirtyState_shouldBeFalse_afterSuccessfulLoad() throws Exception {
        CountDownLatch latch = new CountDownLatch(1);
        AtomicReference<Throwable> fxError = new AtomicReference<>();
        AtomicBoolean dirtyRef = new AtomicBoolean(true);

        Platform.runLater(() -> {
            try {
                SyncPromptEditorPort port = new SyncPromptEditorPort();
                GuiPromptEditorTab editorTab = buildSyncTab(port);
                // Laden synchron auslösen (fxDispatcher = Runnable::run)
                editorTab.loadPromptAsync();
                dirtyRef.set(editorTab.hasDirtyContent());
            } catch (Throwable t) {
                fxError.set(t);
            } finally {
                latch.countDown();
            }
        });

        assertTrue(latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS));
        if (fxError.get() != null) {
            throw new AssertionError("FX-Thread hat eine Ausnahme geworfen", fxError.get());
        }
        assertFalse(dirtyRef.get(), "Dirty-State muss nach erfolgreichem Laden false sein");
    }

    @Test
    void tabTitle_shouldNotContainAsterisk_afterSuccessfulLoad() throws Exception {
        CountDownLatch latch = new CountDownLatch(1);
        AtomicReference<Throwable> fxError = new AtomicReference<>();
        AtomicReference<String> titleRef = new AtomicReference<>();

        Platform.runLater(() -> {
            try {
                SyncPromptEditorPort port = new SyncPromptEditorPort();
                GuiPromptEditorTab editorTab = buildSyncTab(port);
                editorTab.loadPromptAsync();
                titleRef.set(editorTab.tab().getText());
            } catch (Throwable t) {
                fxError.set(t);
            } finally {
                latch.countDown();
            }
        });

        assertTrue(latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS));
        if (fxError.get() != null) {
            throw new AssertionError("FX-Thread hat eine Ausnahme geworfen", fxError.get());
        }
        assertFalse(titleRef.get().contains("*"),
                "Tab-Titel darf nach erfolgreichem Laden keinen Asterisk enthalten");
    }

    @Test
    void dirtyState_shouldBeFalse_whenLoadReturnsFileNotFound() throws Exception {
        CountDownLatch latch = new CountDownLatch(1);
        AtomicReference<Throwable> fxError = new AtomicReference<>();
        AtomicBoolean dirtyRef = new AtomicBoolean(true);

        Platform.runLater(() -> {
            try {
                SyncPromptEditorPort port = new SyncPromptEditorPort();
                port.loadResult = new PromptLoadingFailure("FILE_NOT_FOUND", "Datei nicht gefunden");
                GuiPromptEditorTab editorTab = buildSyncTab(port);
                editorTab.loadPromptAsync();
                dirtyRef.set(editorTab.hasDirtyContent());
            } catch (Throwable t) {
                fxError.set(t);
            } finally {
                latch.countDown();
            }
        });

        assertTrue(latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS));
        if (fxError.get() != null) {
            throw new AssertionError("FX-Thread hat eine Ausnahme geworfen", fxError.get());
        }
        assertFalse(dirtyRef.get(), "Dirty-State muss false sein wenn Datei nicht gefunden wurde");
    }

    @Test
    void notifyConfigurationChanged_shouldResetDirtyStateAndTitle() throws Exception {
        CountDownLatch latch = new CountDownLatch(1);
        AtomicReference<Throwable> fxError = new AtomicReference<>();
        AtomicBoolean dirtyRef = new AtomicBoolean(true);
        AtomicReference<String> titleRef = new AtomicReference<>();

        Platform.runLater(() -> {
            try {
                SyncPromptEditorPort port = new SyncPromptEditorPort();
                GuiPromptEditorTab editorTab = buildSyncTab(port);
                // Laden und anschliessend Inhalt aendern, um Dirty-State zu erzeugen
                editorTab.loadPromptAsync();
                editorTab.resetToDefault();
                // Vorbedingung: Dirty-State muss aktiv sein
                assertTrue(editorTab.hasDirtyContent(),
                        "Vorbedingung: Dirty-State muss nach resetToDefault aktiv sein");

                // Konfiguration wechseln – Dirty-State und Titel sollen zurueckgesetzt werden
                editorTab.notifyConfigurationChanged(new SyncPromptEditorPort(), "/new/prompt.txt", 80);

                dirtyRef.set(editorTab.hasDirtyContent());
                titleRef.set(editorTab.tab().getText());
            } catch (Throwable t) {
                fxError.set(t);
            } finally {
                latch.countDown();
            }
        });

        assertTrue(latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS));
        if (fxError.get() != null) {
            throw new AssertionError("FX-Thread hat eine Ausnahme geworfen", fxError.get());
        }
        assertFalse(dirtyRef.get(),
                "Dirty-State muss nach notifyConfigurationChanged false sein");
        assertFalse(titleRef.get().contains("*"),
                "Tab-Titel darf nach notifyConfigurationChanged keinen Asterisk enthalten; Titel war: "
                        + titleRef.get());
    }

    @Test
    void tabTitle_shouldContainAsterisk_afterEditWithLoadedBaseline() throws Exception {
        CountDownLatch latch = new CountDownLatch(1);
        AtomicReference<Throwable> fxError = new AtomicReference<>();
        AtomicReference<String> titleRef = new AtomicReference<>();

        Platform.runLater(() -> {
            try {
                SyncPromptEditorPort port = new SyncPromptEditorPort();
                GuiPromptEditorTab editorTab = buildSyncTab(port);
                editorTab.loadPromptAsync();
                // Direkte TextArea-Manipulation simuliert Benutzer-Eingabe
                // Über Reflection auf das private textArea-Feld zugreifen ist unerwünscht.
                // Stattdessen: resetToDefault() setzt einen anderen Inhalt als den geladenen,
                // was den Dirty-State auslöst.
                editorTab.resetToDefault();
                titleRef.set(editorTab.tab().getText());
            } catch (Throwable t) {
                fxError.set(t);
            } finally {
                latch.countDown();
            }
        });

        assertTrue(latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS));
        if (fxError.get() != null) {
            throw new AssertionError("FX-Thread hat eine Ausnahme geworfen", fxError.get());
        }
        // Nach resetToDefault() wird der Default-Inhalt gesetzt.
        // Falls dieser vom geladenen Inhalt abweicht, entsteht ein Dirty-State.
        // Da Stub-Inhalt != Default-Template, muss Asterisk vorhanden sein.
        assertTrue(titleRef.get().contains("*"),
                "Tab-Titel muss nach Bearbeitung (resetToDefault) einen Asterisk enthalten; Titel war: "
                        + titleRef.get());
    }

    @Test
    void discardChanges_shouldResetDirtyStateAndTitle() throws Exception {
        CountDownLatch latch = new CountDownLatch(1);
        AtomicReference<Throwable> fxError = new AtomicReference<>();
        AtomicBoolean dirtyRef = new AtomicBoolean(true);
        AtomicReference<String> titleRef = new AtomicReference<>();

        Platform.runLater(() -> {
            try {
                SyncPromptEditorPort port = new SyncPromptEditorPort();
                GuiPromptEditorTab editorTab = buildSyncTab(port);
                editorTab.loadPromptAsync();
                editorTab.resetToDefault();
                // Vorbedingung: Dirty-State muss aktiv sein
                assertTrue(editorTab.hasDirtyContent(),
                        "Vorbedingung: Dirty-State muss nach resetToDefault aktiv sein");

                // Verwerfen simulieren
                editorTab.discardChanges();

                dirtyRef.set(editorTab.hasDirtyContent());
                titleRef.set(editorTab.tab().getText());
            } catch (Throwable t) {
                fxError.set(t);
            } finally {
                latch.countDown();
            }
        });

        assertTrue(latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS));
        if (fxError.get() != null) {
            throw new AssertionError("FX-Thread hat eine Ausnahme geworfen", fxError.get());
        }
        assertFalse(dirtyRef.get(),
                "Dirty-State muss nach discardChanges false sein");
        assertFalse(titleRef.get().contains("*"),
                "Tab-Titel darf nach discardChanges keinen Asterisk enthalten; Titel war: "
                        + titleRef.get());
    }
}
