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

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.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.time.LocalDate;
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.BeforeAll;
import org.junit.jupiter.api.Test;

import de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionEvent;
import de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionStatus;
import de.gecheckt.pdf.umbenenner.application.port.in.RunSummary;
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
import de.gecheckt.pdf.umbenenner.domain.model.RunId;
import javafx.application.Platform;

/**
 * Headless (Monocle) smoke tests for {@link GuiBatchRunTab}. These tests drive the tab
 * end-to-end via a stubbed launcher, asserting the observable UI state transitions on
 * the JavaFX Application Thread.
 */
class GuiBatchRunTabSmokeTest {

    private static final long FX_TIMEOUT_SECONDS = 10;
    private static final DocumentFingerprint DUMMY_FP = new DocumentFingerprint("a".repeat(64));
    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) {
                // JavaFX is already running; reuse it.
                latch.countDown();
            }
            assertTrue(latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS));
        }
    }

    @Test
    void start_withoutSavedConfiguration_showsHint() throws Exception {
        onFxAndWait(tab -> {
            tab.startButton().fire();
            assertEquals(GuiBatchRunTab.NO_SAVED_CONFIGURATION_HINT,
                    tab.messageArea().getText());
            assertFalse(tab.coordinator().isRunning());
        }, /*savedReady*/ false, /*configPath*/ null, /*launcher*/ null);
    }

    @Test
    void start_withEmptyFolder_showsEmptyFolderHintAndSummary() throws Exception {
        AtomicReference<Throwable> error = new AtomicReference<>();
        CountDownLatch launcherInvoked = new CountDownLatch(1);
        GuiBatchRunLauncher launcher = (configPath, observer, token) -> {
            observer.onRunStarted(new RunId("empty"), 0);
            observer.onRunEnded(new RunSummary(0, 0, 0));
            launcherInvoked.countDown();
            return GuiBatchRunLaunchOutcome.completed();
        };

        CountDownLatch latch = runOnFxAndWaitUntilDone(tab -> {
            try {
                tab.startButton().fire();
            } catch (Throwable t) {
                error.set(t);
            }
        }, /*savedReady*/ true, Paths.get("ignored.properties"), launcher,
                () -> !tab().coordinator().isRunning() && !tab().messageArea().getText().isBlank());

        assertTrue(latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS), "tab must quiesce");
        if (error.get() != null) throw new AssertionError(error.get());
        assertTrue(launcherInvoked.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS));

        runOnFx(() -> {
            assertTrue(tab().messageArea().getText().contains(GuiBatchRunTab.EMPTY_SOURCE_FOLDER_HINT),
                    () -> "Missing empty-folder hint in: " + tab().messageArea().getText());
            assertTrue(tab().messageArea().getText().contains("0 erfolgreich"),
                    () -> "Missing summary counters in: " + tab().messageArea().getText());
            assertEquals("0 / 0 Dateien", tab().counterLabel().getText());
        });
    }

    @Test
    void completedRun_populatesListProgressBarAndSummary() throws Exception {
        GuiBatchRunLauncher launcher = (configPath, observer, token) -> {
            observer.onRunStarted(new RunId("run"), 3);
            observer.onDocumentCompleted(new DocumentCompletionEvent(
                    "a.pdf", DUMMY_FP, DocumentCompletionStatus.SUCCESS,
                    "2026-03-01 - Titel.pdf",
                    LocalDate.of(2026, 3, 1),
                    "gut begründet",
                    null,
                    Duration.ofMillis(42)));
            observer.onDocumentCompleted(new DocumentCompletionEvent(
                    "b.pdf", DUMMY_FP, DocumentCompletionStatus.FAILED_RETRYABLE,
                    null, null, null, null, Duration.ofMillis(10)));
            observer.onDocumentCompleted(new DocumentCompletionEvent(
                    "c.pdf", DUMMY_FP, DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED,
                    null, null, null, null, Duration.ofMillis(5)));
            observer.onRunEnded(new RunSummary(1, 1, 1));
            return GuiBatchRunLaunchOutcome.completed();
        };

        CountDownLatch latch = runOnFxAndWaitUntilDone(
                tab -> tab.startButton().fire(),
                true, Paths.get("ok.properties"), launcher,
                () -> !tab().coordinator().isRunning() && tab().resultTable().getItems().size() == 3);
        assertTrue(latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS),
                "tab must quiesce with 3 rows");

        runOnFx(() -> {
            assertEquals(3, tab().resultTable().getItems().size());
            assertEquals("3 / 3 Dateien", tab().counterLabel().getText());
            assertEquals(1.0, tab().progressBar().getProgress(), 0.001);
            String messageText = tab().messageArea().getText();
            assertTrue(messageText.contains("1 erfolgreich"), messageText);
            assertTrue(messageText.contains("1 fehlgeschlagen"), messageText);
            assertTrue(messageText.contains("1 übersprungen"), messageText);

            // Clicking the first row populates the detail pane with the AI reasoning.
            tab().resultTable().getSelectionModel().select(0);
            String detail = tab().detailArea().getText();
            assertTrue(detail.contains("a.pdf"), detail);
            assertTrue(detail.contains("2026-03-01 - Titel.pdf"), detail);
            assertTrue(detail.contains("gut begründet"), detail);

            // Clicking a row without reasoning shows the no-reasoning placeholder.
            tab().resultTable().getSelectionModel().select(1);
            assertTrue(tab().detailArea().getText().contains(GuiBatchRunTab.NO_REASONING_TEXT));

            // SKIPPED_ALREADY_PROCESSED trägt das Identisch-Icon ≡, nicht ⏭.
            GuiBatchRunResultRow skippedRow = tab().resultTable().getItems().get(2);
            assertEquals(DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED, skippedRow.status());
            assertEquals("≡", skippedRow.statusIcon());
        });
    }

    @Test
    void detailPane_initiallyShowsPlaceholder() throws Exception {
        onFxAndWait(tab -> {
            assertEquals(GuiBatchRunTab.DETAIL_PLACEHOLDER, tab.detailArea().getText());
        }, /*savedReady*/ false, /*configPath*/ null, /*launcher*/ null);
    }

    @Test
    void runLauncherFailure_showsFailureMessageInTerminalState() throws Exception {
        GuiBatchRunLauncher launcher = (configPath, observer, token) -> {
            observer.onRunStarted(new RunId("x"), 0);
            observer.onRunEnded(new RunSummary(0, 0, 0));
            return GuiBatchRunLaunchOutcome.rejected("SQLite unvailable (Testnachricht)");
        };

        CountDownLatch latch = runOnFxAndWaitUntilDone(
                tab -> tab.startButton().fire(),
                true, Paths.get("ignore.properties"), launcher,
                () -> !tab().coordinator().isRunning() && !tab().messageArea().getText().isBlank());
        assertTrue(latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS));

        runOnFx(() -> {
            String message = tab().messageArea().getText();
            assertTrue(message.contains("SQLite unvailable (Testnachricht)"),
                    () -> "Expected failure message in: " + message);
            // Start must be enabled again, cancel disabled.
            assertFalse(tab().startButton().isDisabled());
            assertTrue(tab().cancelButton().isDisabled());
        });
    }

    // -------------------------------------------------------------------------
    // FX helpers
    // -------------------------------------------------------------------------

    private static final ThreadLocal<GuiBatchRunTab> CURRENT_TAB = new ThreadLocal<>();

    private GuiBatchRunTab tab() {
        return CURRENT_TAB.get();
    }

    private void onFxAndWait(java.util.function.Consumer<GuiBatchRunTab> action,
                             boolean savedReady,
                             Path configPath,
                             GuiBatchRunLauncher launcher) throws InterruptedException {
        CountDownLatch done = new CountDownLatch(1);
        AtomicReference<Throwable> error = new AtomicReference<>();
        Platform.runLater(() -> {
            try {
                GuiBatchRunTab tab = makeTab(savedReady, configPath, launcher);
                CURRENT_TAB.set(tab);
                action.accept(tab);
            } catch (Throwable t) {
                error.set(t);
            } finally {
                done.countDown();
            }
        });
        assertTrue(done.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS));
        if (error.get() != null) throw new AssertionError(error.get());
    }

    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));
        if (error.get() != null) throw new AssertionError(error.get());
    }

    /**
     * Creates a tab on the FX thread, fires the supplied action, and returns a latch that
     * is counted down as soon as the supplied predicate becomes true on a subsequent
     * FX-thread tick.
     */
    private CountDownLatch runOnFxAndWaitUntilDone(
            java.util.function.Consumer<GuiBatchRunTab> action,
            boolean savedReady,
            Path configPath,
            GuiBatchRunLauncher launcher,
            java.util.function.BooleanSupplier quiesced) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(1);
        AtomicReference<Throwable> error = new AtomicReference<>();

        Platform.runLater(() -> {
            try {
                GuiBatchRunTab tab = makeTab(savedReady, configPath, launcher);
                CURRENT_TAB.set(tab);
                action.accept(tab);
                schedulePoll(latch, quiesced);
            } catch (Throwable t) {
                error.set(t);
                latch.countDown();
            }
        });
        if (error.get() != null) throw new AssertionError(error.get());
        return latch;
    }

    private static void schedulePoll(CountDownLatch latch, java.util.function.BooleanSupplier predicate) {
        Platform.runLater(() -> {
            if (predicate.getAsBoolean()) {
                latch.countDown();
            } else {
                schedulePoll(latch, predicate);
            }
        });
    }

    private static GuiBatchRunTab makeTab(boolean savedReady, Path configPath, GuiBatchRunLauncher launcher) {
        GuiBatchRunLauncher effective = launcher == null
                ? (configPath1, observer, token) -> GuiBatchRunLaunchOutcome.rejected(
                        "Test-Stub sollte nicht aufgerufen werden.")
                : launcher;
        return new GuiBatchRunTab(
                () -> effective,
                () -> configPath,
                () -> savedReady,
                () -> { /* state-change hook not needed for these tests */ });
    }
}
