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

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

import java.nio.file.Path;
import java.util.Optional;
import java.util.Properties;
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.adapter.in.gui.editor.GuiConfigurationEditorState;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationFileSnapshot;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationTemplateFactory;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationValues;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiProviderConfigurationState;
import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
import javafx.application.Platform;

/**
 * Tests für die Statuszeilen-Komponente {@link GuiStatusBar}.
 * <p>
 * Überprüft die Versionsanzeige, den Provider-Text und den Konfigurationspfad-Text
 * in den verschiedenen Zuständen (ohne und mit geladener Konfiguration).
 * <p>
 * Die Tests laufen unter Monocle (Headless-JavaFX), da {@link GuiStatusBar} JavaFX-Controls erzeugt.
 */
class GuiStatusBarTest {

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

    /**
     * Initialisiert die JavaFX-Plattform einmalig für alle Tests dieser Klasse.
     *
     * @throws InterruptedException falls der Thread beim Warten unterbrochen wird
     */
    @BeforeAll
    static void setUpJavaFxPlatform() throws InterruptedException {
        Platform.setImplicitExit(false);
        CountDownLatch startLatch = new CountDownLatch(1);
        try {
            Platform.startup(() -> {
                PLATFORM_STARTED.set(true);
                startLatch.countDown();
            });
        } catch (IllegalStateException alreadyInitialized) {
            // JavaFX wurde bereits durch einen anderen Test gestartet
            PLATFORM_STARTED.set(true);
            startLatch.countDown();
        }
        assertTrue(
                startLatch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS),
                "JavaFX-Plattform muss innerhalb des Timeouts starten");
    }

    /** Plattform bleibt für nachfolgende Tests am Leben. */
    @AfterAll
    static void tearDownJavaFxPlatform() {
        // Absichtlich kein Platform.exit() – damit andere Smoke-Tests weiterhin die Plattform nutzen können.
    }

    // =========================================================================
    // Versionsanzeige
    // =========================================================================

    /**
     * Prüft, dass die Versionsanzeige das korrekte Präfix und die übergebene Version enthält.
     *
     * @throws Exception falls der FX-Thread-Task fehlschlägt oder das Timeout überschritten wird
     */
    @Test
    void versionLabel_zeigtVersionMitPraefix() throws Exception {
        AtomicReference<String> versionText = new AtomicReference<>();
        runOnFxThread(() -> {
            GuiStatusBar bar = new GuiStatusBar("3.0.42");
            versionText.set(bar.versionText());
        });
        assertEquals("V3.0.42", versionText.get(),
                "Die Versionsanzeige muss das Präfix 'V' gefolgt von der Versionsnummer enthalten");
    }

    /**
     * Prüft, dass ein {@code null}-Wert für die Version als {@code "dev"} angezeigt wird.
     *
     * @throws Exception falls der FX-Thread-Task fehlschlägt oder das Timeout überschritten wird
     */
    @Test
    void versionLabel_mitNullFaellzurueckAufDev() throws Exception {
        AtomicReference<String> versionText = new AtomicReference<>();
        runOnFxThread(() -> {
            GuiStatusBar bar = new GuiStatusBar(null);
            versionText.set(bar.versionText());
        });
        assertEquals("Vdev", versionText.get(),
                "Ein null-Wert muss als Fallback 'dev' angezeigt werden");
    }

    /**
     * Prüft, dass ein leerer String für die Version als {@code "dev"} angezeigt wird.
     *
     * @throws Exception falls der FX-Thread-Task fehlschlägt oder das Timeout überschritten wird
     */
    @Test
    void versionLabel_mitLeeremStringFaellzurueckAufDev() throws Exception {
        AtomicReference<String> versionText = new AtomicReference<>();
        runOnFxThread(() -> {
            GuiStatusBar bar = new GuiStatusBar("   ");
            versionText.set(bar.versionText());
        });
        assertEquals("Vdev", versionText.get(),
                "Ein leerer String muss als Fallback 'dev' angezeigt werden");
    }

    // =========================================================================
    // Standardzustand ohne geladene Konfiguration
    // =========================================================================

    /**
     * Prüft, dass Mitte und Rechts den Text „Kein Profil geladen" zeigen, wenn keine
     * Konfiguration geladen ist.
     *
     * @throws Exception falls der FX-Thread-Task fehlschlägt oder das Timeout überschritten wird
     */
    @Test
    void ohneKonfiguration_zeigtKeinProfilGeladen() throws Exception {
        AtomicReference<String> providerText = new AtomicReference<>();
        AtomicReference<String> configPathText = new AtomicReference<>();
        runOnFxThread(() -> {
            GuiStatusBar bar = new GuiStatusBar("1.0.0");
            providerText.set(bar.providerText());
            configPathText.set(bar.configPathText());
        });
        assertEquals(GuiStatusBar.KEIN_PROFIL_TEXT, providerText.get(),
                "Ohne geladene Konfiguration muss 'Kein Profil geladen' als Provider-Text erscheinen");
        assertEquals(GuiStatusBar.KEIN_PROFIL_TEXT, configPathText.get(),
                "Ohne geladene Konfiguration muss 'Kein Profil geladen' als Konfigurationspfad erscheinen");
    }

    /**
     * Prüft, dass {@link GuiStatusBar#clearConfiguration()} Mitte und Rechts zurücksetzt.
     *
     * @throws Exception falls der FX-Thread-Task fehlschlägt oder das Timeout überschritten wird
     */
    @Test
    void clearConfiguration_setztMitteUndRechtsZurueck() throws Exception {
        AtomicReference<String> providerText = new AtomicReference<>();
        AtomicReference<String> configPathText = new AtomicReference<>();
        runOnFxThread(() -> {
            GuiStatusBar bar = new GuiStatusBar("1.0.0");
            // Zustand mit Konfiguration setzen, dann löschen
            GuiConfigurationEditorState state = buildStateWithConfiguration(
                    "config/application.properties", AiProviderFamily.CLAUDE, "claude-opus-4-7");
            bar.applyEditorState(state);
            bar.clearConfiguration();
            providerText.set(bar.providerText());
            configPathText.set(bar.configPathText());
        });
        assertEquals(GuiStatusBar.KEIN_PROFIL_TEXT, providerText.get(),
                "Nach clearConfiguration() muss 'Kein Profil geladen' als Provider-Text erscheinen");
        assertEquals(GuiStatusBar.KEIN_PROFIL_TEXT, configPathText.get(),
                "Nach clearConfiguration() muss 'Kein Profil geladen' als Konfigurationspfad erscheinen");
    }

    // =========================================================================
    // Zustand nach Laden einer Konfiguration
    // =========================================================================

    /**
     * Prüft, dass nach {@link GuiStatusBar#applyEditorState} der korrekte Provider-Text
     * mit Modell angezeigt wird.
     *
     * @throws Exception falls der FX-Thread-Task fehlschlägt oder das Timeout überschritten wird
     */
    @Test
    void applyEditorState_mitClaudeUndModell_zeigtKorrektesFormat() throws Exception {
        AtomicReference<String> providerText = new AtomicReference<>();
        runOnFxThread(() -> {
            GuiStatusBar bar = new GuiStatusBar("1.0.0");
            GuiConfigurationEditorState state = buildStateWithConfiguration(
                    "config/application.properties", AiProviderFamily.CLAUDE, "claude-opus-4-7");
            bar.applyEditorState(state);
            providerText.set(bar.providerText());
        });
        assertEquals("Provider: Claude · claude-opus-4-7", providerText.get(),
                "Der Provider-Text muss das Format 'Provider: <Name> · <Modell>' haben");
    }

    /**
     * Prüft, dass nach {@link GuiStatusBar#applyEditorState} der korrekte Konfigurationspfad
     * angezeigt wird.
     *
     * @throws Exception falls der FX-Thread-Task fehlschlägt oder das Timeout überschritten wird
     */
    @Test
    void applyEditorState_mitKonfigurationspfad_zeigtKonfiguationspfad() throws Exception {
        AtomicReference<String> configPathText = new AtomicReference<>();
        runOnFxThread(() -> {
            GuiStatusBar bar = new GuiStatusBar("1.0.0");
            GuiConfigurationEditorState state = buildStateWithConfiguration(
                    "config/application.properties", AiProviderFamily.CLAUDE, "claude-opus-4-7");
            bar.applyEditorState(state);
            configPathText.set(bar.configPathText());
        });
        assertTrue(configPathText.get().contains("application.properties"),
                "Der Konfigurationspfad muss den Dateinamen enthalten");
    }

    /**
     * Prüft, dass ein OpenAI-kompatibler Provider korrekt angezeigt wird.
     *
     * @throws Exception falls der FX-Thread-Task fehlschlägt oder das Timeout überschritten wird
     */
    @Test
    void applyEditorState_mitOpenAiUndModell_zeigtKorrektesFormat() throws Exception {
        AtomicReference<String> providerText = new AtomicReference<>();
        runOnFxThread(() -> {
            GuiStatusBar bar = new GuiStatusBar("1.0.0");
            GuiConfigurationEditorState state = buildStateWithConfiguration(
                    "config/application.properties", AiProviderFamily.OPENAI_COMPATIBLE, "gpt-4o");
            bar.applyEditorState(state);
            providerText.set(bar.providerText());
        });
        assertEquals("Provider: OpenAI-kompatibel · gpt-4o", providerText.get(),
                "Der Provider-Text muss für OpenAI-kompatibel den deutschen Anzeigenamen verwenden");
    }

    /**
     * Prüft, dass beim Übergeben eines {@code null}-Zustands kein Absturz erfolgt und der
     * Text „Kein Profil geladen" erscheint.
     *
     * @throws Exception falls der FX-Thread-Task fehlschlägt oder das Timeout überschritten wird
     */
    @Test
    void applyEditorState_mitNull_keinAbsturz() throws Exception {
        AtomicReference<String> providerText = new AtomicReference<>();
        runOnFxThread(() -> {
            GuiStatusBar bar = new GuiStatusBar("1.0.0");
            bar.applyEditorState(null);
            providerText.set(bar.providerText());
        });
        assertEquals(GuiStatusBar.KEIN_PROFIL_TEXT, providerText.get(),
                "Ein null-Zustand darf keinen Absturz verursachen");
    }

    /**
     * Prüft, dass ohne geladenen Dateisnapshot „Kein Profil geladen" angezeigt wird,
     * auch wenn Konfigurationswerte vorhanden sind.
     *
     * @throws Exception falls der FX-Thread-Task fehlschlägt oder das Timeout überschritten wird
     */
    @Test
    void applyEditorState_ohneSnapshot_zeigtKeinProfilGeladen() throws Exception {
        AtomicReference<String> providerText = new AtomicReference<>();
        runOnFxThread(() -> {
            GuiStatusBar bar = new GuiStatusBar("1.0.0");
            // Standard-Template hat keinen Snapshot
            GuiConfigurationEditorState state = GuiConfigurationTemplateFactory.createStandardTemplate();
            bar.applyEditorState(state);
            providerText.set(bar.providerText());
        });
        assertEquals(GuiStatusBar.KEIN_PROFIL_TEXT, providerText.get(),
                "Ohne geladenen Dateisnapshot muss 'Kein Profil geladen' erscheinen");
    }

    /**
     * Prüft, dass der Wurzelknoten der Statuszeile nicht null ist.
     *
     * @throws Exception falls der FX-Thread-Task fehlschlägt oder das Timeout überschritten wird
     */
    @Test
    void root_istNichtNull() throws Exception {
        AtomicBoolean rootNotNull = new AtomicBoolean(false);
        runOnFxThread(() -> {
            GuiStatusBar bar = new GuiStatusBar("1.0.0");
            rootNotNull.set(bar.root() != null);
        });
        assertTrue(rootNotNull.get(), "Der Wurzelknoten der Statuszeile darf nicht null sein");
    }

    // =========================================================================
    // Hilfsmethoden
    // =========================================================================

    /**
     * Führt eine Aktion synchron auf dem JavaFX Application Thread aus und wartet auf Abschluss.
     *
     * @param action die auszuführende Aktion
     * @throws Exception falls die Aktion einen Fehler wirft oder das Timeout überschritten wird
     */
    private static void runOnFxThread(Runnable action) throws Exception {
        CountDownLatch latch = new CountDownLatch(1);
        AtomicReference<Throwable> error = new AtomicReference<>();
        Platform.runLater(() -> {
            try {
                action.run();
            } catch (Throwable t) {
                error.set(t);
            } finally {
                latch.countDown();
            }
        });
        assertTrue(latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS),
                "FX-Thread-Task muss innerhalb des Timeouts abgeschlossen werden");
        if (error.get() != null) {
            throw new AssertionError("FX-Thread hat eine Ausnahme geworfen", error.get());
        }
    }

    /**
     * Erstellt einen Editor-Zustand mit geladenem Dateisnapshot für den angegebenen
     * Konfigurationspfad, Provider und Modell.
     *
     * @param configPath relativer Konfigurationsdateipfad
     * @param family     aktive Provider-Familie
     * @param model      Modellbezeichner
     * @return ein Editor-Zustand mit Snapshot
     */
    private static GuiConfigurationEditorState buildStateWithConfiguration(
            String configPath, AiProviderFamily family, String model) {
        GuiConfigurationEditorState template = GuiConfigurationTemplateFactory.createStandardTemplate();
        // Provider und Modell setzen
        GuiProviderConfigurationState providerState = new GuiProviderConfigurationState(
                "https://api.example.com", model, "30",
                de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiProviderApiKeyState.unresolved());
        GuiConfigurationValues values = template.values()
                .withActiveProviderFamily(family.getIdentifier())
                .withProviderConfiguration(family, providerState);
        // Snapshot anlegen
        GuiConfigurationFileSnapshot snapshot = new GuiConfigurationFileSnapshot(
                Path.of(configPath), new Properties());
        return new GuiConfigurationEditorState(
                Optional.of(snapshot), values, values, Optional.empty());
    }
}
