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.Paths;
import java.time.Duration;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

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

import de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionStatus;
import de.gecheckt.pdf.umbenenner.application.port.in.ResetDocumentStatusResult;
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 selection, upsert, and button-state behaviour added
 * to {@link GuiBatchRunTab}.
 */
class GuiBatchRunTabSelectionSmokeTest {

    private static final long FX_TIMEOUT_SECONDS = 10;
    private static final DocumentFingerprint FP1 = new DocumentFingerprint("a".repeat(64));
    private static final DocumentFingerprint FP2 = new DocumentFingerprint("b".repeat(64));
    private static final DocumentFingerprint FP3 = new DocumentFingerprint("c".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) {
                latch.countDown();
            }
            assertTrue(latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS));
        }
    }

    // -------------------------------------------------------------------------
    // upsertResultRowByFingerprint
    // -------------------------------------------------------------------------

    @Test
    void upsert_appendsNewFingerprint() throws Exception {
        runOnFx(() -> {
            GuiBatchRunTab tab = makeTab();
            GuiBatchRunResultRow row = row("a.pdf", FP1, DocumentCompletionStatus.SUCCESS);
            tab.upsertResultRowByFingerprint(row);
            assertEquals(1, tab.resultTable().getItems().size());
            assertEquals("a.pdf", tab.resultTable().getItems().get(0).originalFileName());
        });
    }

    @Test
    void upsert_replacesExistingFingerprint() throws Exception {
        runOnFx(() -> {
            GuiBatchRunTab tab = makeTab();
            GuiBatchRunResultRow original = row("a.pdf", FP1, DocumentCompletionStatus.FAILED_PERMANENT);
            GuiBatchRunResultRow replacement = row("a.pdf", FP1, DocumentCompletionStatus.SUCCESS);
            tab.upsertResultRowByFingerprint(original);
            tab.upsertResultRowByFingerprint(replacement);
            assertEquals(1, tab.resultTable().getItems().size());
            assertEquals(DocumentCompletionStatus.SUCCESS,
                    tab.resultTable().getItems().get(0).status());
        });
    }

    @Test
    void upsert_differentFingerprintsAppendSeparately() throws Exception {
        runOnFx(() -> {
            GuiBatchRunTab tab = makeTab();
            tab.upsertResultRowByFingerprint(row("a.pdf", FP1, DocumentCompletionStatus.SUCCESS));
            tab.upsertResultRowByFingerprint(row("b.pdf", FP2, DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED));
            tab.upsertResultRowByFingerprint(row("c.pdf", FP3, DocumentCompletionStatus.FAILED_PERMANENT));
            assertEquals(3, tab.resultTable().getItems().size());
        });
    }

    // -------------------------------------------------------------------------
    // Button enable state
    // -------------------------------------------------------------------------

    @Test
    void reprocessAndResetButtons_initiallyDisabled() throws Exception {
        runOnFx(() -> {
            GuiBatchRunTab tab = makeTab();
            assertTrue(tab.reprocessButton().isDisabled(),
                    "reprocess button must be disabled when no rows are selected");
            assertTrue(tab.resetStatusButton().isDisabled(),
                    "reset button must be disabled when no rows are selected");
        });
    }

    // -------------------------------------------------------------------------
    // Reset-pending visual state after onResetCompleted
    // -------------------------------------------------------------------------

    @Test
    void resetCompleted_marksSuccessfulFingerprintsAsResetPending() throws Exception {
        CountDownLatch done = new CountDownLatch(1);
        AtomicBoolean[] results = { new AtomicBoolean(false), new AtomicBoolean(false) };

        GuiResetDocumentStatusPort resetPort = (configPath, fps) ->
                new ResetDocumentStatusResult(2, Set.of(FP1, FP2), Map.of());

        GuiBatchRunLauncher noOpLauncher = (p, o, t) ->
                GuiBatchRunLaunchOutcome.rejected("not used");
        GuiMiniRunLauncher noOpMiniLauncher = (p, f, o, t) ->
                GuiBatchRunLaunchOutcome.rejected("not used");

        Platform.runLater(() -> {
            try {
                GuiBatchRunTab tab = new GuiBatchRunTab(
                        () -> noOpLauncher,
                        () -> noOpMiniLauncher,
                        () -> resetPort,
                        () -> Paths.get("test.properties"),
                        () -> true,
                        () -> { });

                // Pre-populate result list.
                GuiBatchRunResultRow r1 = row("a.pdf", FP1, DocumentCompletionStatus.SUCCESS);
                GuiBatchRunResultRow r2 = row("b.pdf", FP2, DocumentCompletionStatus.FAILED_PERMANENT);
                tab.upsertResultRowByFingerprint(r1);
                tab.upsertResultRowByFingerprint(r2);

                // Simulate the coordinator's reset result callback:
                // Since we can't drive it through the full coordinator (which would need
                // a real config file for the reset port), we manually trigger the relevant
                // visual updates that onResetCompleted in CoordinatorListener performs.
                ResetDocumentStatusResult result = new ResetDocumentStatusResult(
                        2, Set.of(FP1, FP2), Map.of());
                for (DocumentFingerprint fp : result.successfullyReset()) {
                    for (int i = 0; i < tab.resultTable().getItems().size(); i++) {
                        if (tab.resultTable().getItems().get(i).fingerprint().equals(fp)) {
                            tab.upsertResultRowByFingerprint(
                                    GuiBatchRunResultRow.resetMarker(
                                            tab.resultTable().getItems().get(i)));
                            break;
                        }
                    }
                }

                results[0].set(tab.resultTable().getItems().get(0).resetPending());
                results[1].set(tab.resultTable().getItems().get(1).resetPending());
            } finally {
                done.countDown();
            }
        });

        assertTrue(done.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS));
        assertTrue(results[0].get(), "Row 0 should be reset-pending");
        assertTrue(results[1].get(), "Row 1 should be reset-pending");
    }

    // -------------------------------------------------------------------------
    // Missing-source-file handling in mini-runs (Spec: "Quelldatei nicht gefunden")
    // -------------------------------------------------------------------------

    @Test
    void miniRun_missingSourceFile_becomesFailedPermanentWithGermanMessage()
            throws Exception {
        // FP1 will receive a completion event; FP2 will be silently skipped by the use
        // case (simulating a source file that was moved or deleted after selection).
        // The tab must synthesize a FAILED_PERMANENT row for FP2 on onRunEnded.

        GuiMiniRunLauncher miniLauncher = (configPath, filter, observer, token) -> {
            observer.onRunStarted(new RunId("test-run"), 1);
            observer.onDocumentCompleted(
                    new de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionEvent(
                            "a.pdf",
                            FP1,
                            DocumentCompletionStatus.SUCCESS,
                            "2026-01-01 - Titel.pdf",
                            java.time.LocalDate.of(2026, 1, 1),
                            "reasoning",
                            null,
                            Duration.ofMillis(5)));
            observer.onRunEnded(new RunSummary(1, 0, 0));
            return GuiBatchRunLaunchOutcome.completed();
        };

        GuiBatchRunLauncher noOpLauncher = (p, o, t) ->
                GuiBatchRunLaunchOutcome.rejected("not used");
        GuiResetDocumentStatusPort noOpReset = (p, fps) ->
                // Return a successful reset for all fingerprints. The reset doesn't actually
                // change anything in this test, but successCount() returns successfullyReset.size(),
                // so we must return the fingerprints in the successfully reset set.
                new ResetDocumentStatusResult(fps.size(), fps, Map.of());

        CountDownLatch tabReady = new CountDownLatch(1);
        AtomicReferenceCapture<GuiBatchRunTab> tabRef = new AtomicReferenceCapture<>();

        Platform.runLater(() -> {
            GuiBatchRunTab tab = new GuiBatchRunTab(
                    () -> noOpLauncher,
                    () -> miniLauncher,
                    () -> noOpReset,
                    () -> Paths.get("test.properties"),
                    () -> true,
                    () -> { });
            tab.upsertResultRowByFingerprint(row("a.pdf", FP1, DocumentCompletionStatus.SUCCESS));
            tab.upsertResultRowByFingerprint(row("b.pdf", FP2, DocumentCompletionStatus.SUCCESS));
            tab.resultTable().getSelectionModel().selectAll();
            tabRef.set(tab);
            tabReady.countDown();
        });
        assertTrue(tabReady.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS));

        GuiBatchRunTab tab = tabRef.get();

        CountDownLatch runDone = new CountDownLatch(1);
        tab.runningProperty().addListener((obs, was, isNow) -> {
            if (was && !isNow) {
                runDone.countDown();
            }
        });

        Platform.runLater(() -> tab.reprocessButton().fire());

        assertTrue(runDone.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS),
                "Mini-run should complete");

        CountDownLatch verified = new CountDownLatch(1);
        AtomicReferenceCapture<GuiBatchRunResultRow> fp2Row = new AtomicReferenceCapture<>();
        AtomicReferenceCapture<String> messageText = new AtomicReferenceCapture<>();
        Platform.runLater(() -> {
            for (GuiBatchRunResultRow r : tab.resultTable().getItems()) {
                if (r.fingerprint().equals(FP2)) {
                    fp2Row.set(r);
                }
            }
            messageText.set(tab.messageArea().getText());
            verified.countDown();
        });
        assertTrue(verified.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS));

        GuiBatchRunResultRow missing = fp2Row.get();
        assertFalse(missing == null, "FP2 row must still exist after mini-run");
        assertEquals(DocumentCompletionStatus.FAILED_PERMANENT, missing.status(),
                "FP2 row must become FAILED_PERMANENT when source file is missing");
        assertFalse(missing.resetPending(),
                "FP2 row must no longer be reset-pending after run ended");
        assertTrue(messageText.get() != null
                        && messageText.get().contains("Quelldatei nicht gefunden: b.pdf"),
                "Message area must contain German 'Quelldatei nicht gefunden: b.pdf'; was: "
                        + messageText.get());
        assertTrue(messageText.get().contains("1 fehlgeschlagen"),
                "Zusammenfassung muss '1 fehlgeschlagen' enthalten (fehlende Quelldatei zählt als Fehler); war: "
                        + messageText.get());
    }

    /** Minimal thread-safe holder for tests (avoids extra imports). */
    private static final class AtomicReferenceCapture<T> {
        private final java.util.concurrent.atomic.AtomicReference<T> ref =
                new java.util.concurrent.atomic.AtomicReference<>();
        void set(T t) { ref.set(t); }
        T get() { return ref.get(); }
    }

    // -------------------------------------------------------------------------
    // Helpers
    // -------------------------------------------------------------------------

    private static GuiBatchRunTab makeTab() {
        return new GuiBatchRunTab(
                () -> (p, o, t) -> GuiBatchRunLaunchOutcome.rejected("not used"),
                () -> Paths.get("test.properties"),
                () -> true,
                () -> { });
    }

    private static GuiBatchRunResultRow row(String name, DocumentFingerprint fp,
                                             DocumentCompletionStatus status) {
        return new GuiBatchRunResultRow(name, fp, status,
                Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Duration.ofMillis(1));
    }

    private void runOnFx(Runnable action) throws InterruptedException {
        CountDownLatch done = new CountDownLatch(1);
        java.util.concurrent.atomic.AtomicReference<Throwable> error =
                new java.util.concurrent.atomic.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());
    }
}
