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

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

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 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.GuiMessageEntry;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiMessageSeverity;
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor;
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult;
import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorConfigurationValidator;
import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorValidationInput;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.PathCheckPort;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.TechnicalTestOrchestrator;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.TechnicalTestReport;
import javafx.application.Platform;
import javafx.scene.control.Button;

/**
 * Monocle-based headless smoke tests for the "Technische Tests ausführen" button
 * and {@link GuiTechnicalTestCoordinator}.
 * <p>
 * Verifies the following scenarios:
 * <ul>
 *   <li>The "Technische Tests ausführen" button is findable by CSS ID
 *       {@code technical-tests-button}.</li>
 *   <li>Triggering the coordinator synchronously populates {@code pendingMessages}
 *       with entries tagged {@link GuiTechnicalTestCoordinator#SOURCE_TAG}.</li>
 *   <li>A second trigger replaces the previous batch of test entries.</li>
 *   <li>The post-result callback is invoked after the result is applied.</li>
 * </ul>
 * <p>
 * The coordinator's {@code testThreadFactory} is overridden to run the task inline
 * (on the calling thread) and {@code resultDelivery} is overridden with a direct
 * synchronous call, so results are available immediately after
 * {@link GuiTechnicalTestCoordinator#triggerTechnicalTests()} returns.
 */
class GuiTechnicalTestCoordinatorSmokeTest {

    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: button is findable by CSS ID
    // =========================================================================

    /**
     * Smoke test: after constructing a workspace, the "Technische Tests ausführen" button
     * exists and carries the CSS ID {@code technical-tests-button}.
     *
     * @throws Exception if the FX thread task fails or times out
     */
    @Test
    void technicalTestsButton_hasCssId() throws Exception {
        runOnFx(() -> {
            GuiConfigurationEditorWorkspace ws =
                    new GuiConfigurationEditorWorkspace(Optional.empty());
            Button btn = ws.technicalTestsButton;
            assertNotNull(btn, "technicalTestsButton must not be null");
            assertEquals("technical-tests-button", btn.getId(),
                    "technicalTestsButton must carry CSS ID 'technical-tests-button'");
        });
    }

    // =========================================================================
    // Scenario: triggering populates pendingMessages with SOURCE_TAG entries
    // =========================================================================

    /**
     * Smoke test: triggering the coordinator synchronously populates
     * {@code pendingMessages} with at least one entry tagged with the source
     * {@link GuiTechnicalTestCoordinator#SOURCE_TAG}.
     *
     * @throws Exception if the FX thread task fails or times out
     */
    @Test
    void trigger_populatesPendingMessagesWithSourceTag() throws Exception {
        runOnFx(() -> {
            List<GuiMessageEntry> messages = new ArrayList<>();
            GuiTechnicalTestCoordinator coordinator = buildSyncCoordinator(messages, report -> { });

            coordinator.triggerTechnicalTests();

            long taggedCount = messages.stream()
                    .filter(m -> m.source().isPresent()
                            && GuiTechnicalTestCoordinator.SOURCE_TAG.equals(m.source().get()))
                    .count();

            assertTrue(taggedCount > 0,
                    "Triggering 'Technische Tests ausführen' must add entries tagged '"
                            + GuiTechnicalTestCoordinator.SOURCE_TAG + "'");
        });
    }

    // =========================================================================
    // Scenario: 11 checkpoint entries + 1 summary = 12 entries total
    // =========================================================================

    /**
     * Smoke test: after one trigger, the number of entries tagged SOURCE_TAG equals
     * 12 (one per checkpoint) plus 1 summary entry = 13.
     *
     * @throws Exception if the FX thread task fails or times out
     */
    @Test
    void trigger_producesTwelveCheckpointEntriesPlusSummary() throws Exception {
        runOnFx(() -> {
            List<GuiMessageEntry> messages = new ArrayList<>();
            GuiTechnicalTestCoordinator coordinator = buildSyncCoordinator(messages, report -> { });

            coordinator.triggerTechnicalTests();

            long taggedCount = messages.stream()
                    .filter(m -> m.source().isPresent()
                            && GuiTechnicalTestCoordinator.SOURCE_TAG.equals(m.source().get()))
                    .count();

            // 12 checkpoint entries + 1 summary entry = 13
            assertEquals(13, taggedCount,
                    "Expected 12 checkpoint entries + 1 summary entry = 13 tagged messages");
        });
    }

    // =========================================================================
    // Scenario: replace semantics – second trigger replaces the previous batch
    // =========================================================================

    /**
     * Smoke test: triggering the coordinator twice replaces the previous batch;
     * the second trigger clears the shared message list before applying its own
     * SOURCE_TAG entries, so the count after the second run equals the count
     * after the first run.
     *
     * @throws Exception if the FX thread task fails or times out
     */
    @Test
    void trigger_twice_replacesTestEntries() throws Exception {
        runOnFx(() -> {
            List<GuiMessageEntry> messages = new ArrayList<>();
            GuiTechnicalTestCoordinator coordinator = buildSyncCoordinator(messages, report -> { });

            coordinator.triggerTechnicalTests();
            long countAfterFirst = messages.stream()
                    .filter(m -> m.source().isPresent()
                            && GuiTechnicalTestCoordinator.SOURCE_TAG.equals(m.source().get()))
                    .count();

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

            assertEquals(countAfterFirst, countAfterSecond,
                    "Second trigger must clear and replace the previous SOURCE_TAG batch");
        });
    }

    // =========================================================================
    // Scenario: post-result callback is invoked
    // =========================================================================

    /**
     * Smoke test: the post-result callback is invoked after the result is applied.
     *
     * @throws Exception if the FX thread task fails or times out
     */
    @Test
    void trigger_postResultCallbackIsInvoked() throws Exception {
        runOnFx(() -> {
            AtomicBoolean callbackInvoked = new AtomicBoolean(false);
            List<GuiMessageEntry> messages = new ArrayList<>();
            GuiTechnicalTestCoordinator coordinator = buildSyncCoordinator(
                    messages, report -> callbackInvoked.set(true));

            coordinator.triggerTechnicalTests();

            assertTrue(callbackInvoked.get(),
                    "The post-result callback must be invoked after the technical tests finish");
        });
    }

    // =========================================================================
    // Scenario: input supplier is consulted at trigger time — reflects current (unsaved) state
    // =========================================================================

    /**
     * Smoke test: the coordinator must read the current editor state at trigger time via the
     * injected {@link java.util.function.Supplier}, not from a cached snapshot. This verifies
     * that unsaved changes are always reflected in the technical test results.
     *
     * <p>The test wires the coordinator with a mutable {@link AtomicReference} as input supplier.
     * Before the first trigger the supplier returns valid input; before the second trigger the
     * supplier is updated to return input with an empty active-provider (which causes failures).
     * After the second trigger the message list must contain ERROR entries, proving that the
     * coordinator forwarded the new, unsaved input value rather than the previous one.
     *
     * @throws Exception if the FX thread task fails or times out
     */
    @Test
    void trigger_readsCurrentInputFromSupplier_unsavedChangesAreReflected() throws Exception {
        runOnFx(() -> {
            List<GuiMessageEntry> messages = new ArrayList<>();

            // Mutable supplier: starts with valid input, will be swapped before second trigger.
            AtomicReference<EditorValidationInput> currentInput = new AtomicReference<>(
                    new EditorValidationInput(
                            "claude",
                            "/src", "/tgt", "/db.sqlite", "/prompt.txt",
                            "3", "10", "500", "60",
                            "https://api.anthropic.com", "claude-3-sonnet", "30",
                            EffectiveApiKeyDescriptor.absent(), "",
                            "https://api.openai.com", "gpt-4", "30",
                            EffectiveApiKeyDescriptor.absent(), ""));

            TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator(
                    new EditorConfigurationValidator(),
                    noOpPathCheckPort(),
                    noOpProviderService(),
                    () -> java.util.Optional.empty());

            GuiTechnicalTestCoordinator coordinator = new GuiTechnicalTestCoordinator(
                    orchestrator,
                    currentInput::get,  // always reads the current reference
                    () -> "",
                    () -> "",
                    messages,
                    report -> { });

            coordinator.testThreadFactory = task -> new Thread(task, "sync-test-thread") {
                @Override public void start() { run(); }
            };
            coordinator.resultDelivery = Runnable::run;

            // First trigger with valid input — all failures are from path checks (no-op port),
            // but no CONFIGURATION_BASIC_VALIDATION error because activeProvider is valid.
            coordinator.triggerTechnicalTests();
            long errorCountFirst = messages.stream()
                    .filter(m -> m.source().isPresent()
                            && GuiTechnicalTestCoordinator.SOURCE_TAG.equals(m.source().get()))
                    .filter(m -> m.severity() == GuiMessageSeverity.ERROR)
                    .count();

            // Simulate unsaved edit: replace input with one having an empty active-provider.
            currentInput.set(new EditorValidationInput(
                    "",   // empty active provider → validation error in block 1
                    "/src", "/tgt", "/db.sqlite", "/prompt.txt",
                    "3", "10", "500", "60",
                    "https://api.anthropic.com", "claude-3-sonnet", "30",
                    EffectiveApiKeyDescriptor.absent(), "",
                    "https://api.openai.com", "gpt-4", "30",
                    EffectiveApiKeyDescriptor.absent(), ""));

            // Second trigger with the updated (unsaved) input.
            coordinator.triggerTechnicalTests();
            long errorCountSecond = messages.stream()
                    .filter(m -> m.source().isPresent()
                            && GuiTechnicalTestCoordinator.SOURCE_TAG.equals(m.source().get()))
                    .filter(m -> m.severity() == GuiMessageSeverity.ERROR)
                    .count();

            // After the change, there must be at least as many errors as before,
            // because the empty active-provider introduces additional validation errors.
            assertTrue(errorCountSecond >= errorCountFirst,
                    "Second trigger with empty active-provider must produce at least as many "
                    + "errors as the first trigger, proving the supplier is read at trigger time");
        });
    }

    // =========================================================================
    // Scenario: workspace button is wired to coordinator
    // =========================================================================

    /**
     * Smoke test: after constructing a workspace, the button is not null and
     * the coordinator is not null.
     *
     * @throws Exception if the FX thread task fails or times out
     */
    @Test
    void workspace_buttonAndCoordinatorAreWired() throws Exception {
        runOnFx(() -> {
            GuiConfigurationEditorWorkspace ws =
                    new GuiConfigurationEditorWorkspace(Optional.empty());
            assertNotNull(ws.technicalTestsButton,
                    "technicalTestsButton must not be null");
            assertNotNull(ws.technicalTestCoordinator,
                    "technicalTestCoordinator must not be null");
        });
    }

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

    /** No-op {@link PathCheckPort}: all checks return {@code false}. */
    private static PathCheckPort noOpPathCheckPort() {
        return new PathCheckPort() {
            @Override public boolean isDirectoryReadable(String p) { return false; }
            @Override public boolean isDirectoryWritableOrCreatable(String p) { return false; }
            @Override public boolean isFileReadable(String p) { return false; }
            @Override public boolean isSqlitePathUsable(String p) { return false; }
        };
    }

    /** No-op {@link ProviderTechnicalTestService}. */
    private static ProviderTechnicalTestService noOpProviderService() {
        return new ProviderTechnicalTestService(
                req -> new ModelCatalogResult.IncompleteConfiguration(
                        req.providerIdentifier(), "kein Port im Test"),
                (fam, pv) -> EffectiveApiKeyDescriptor.absent());
    }

    /**
     * Builds a {@link GuiTechnicalTestCoordinator} that runs synchronously (inline thread
     * factory, direct result delivery) so results are available immediately after
     * {@link GuiTechnicalTestCoordinator#triggerTechnicalTests()} returns.
     *
     * @param messages           mutable list to collect message entries
     * @param postResultCallback callback to invoke after result is applied
     * @return synchronously-wired coordinator
     */
    private static GuiTechnicalTestCoordinator buildSyncCoordinator(
            List<GuiMessageEntry> messages,
            java.util.function.Consumer<TechnicalTestReport> postResultCallback) {

        TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator(
                new EditorConfigurationValidator(),
                noOpPathCheckPort(),
                noOpProviderService(),
                () -> java.util.Optional.empty());

        EditorValidationInput blankInput = new EditorValidationInput(
                "claude",
                "/src", "/tgt", "/db.sqlite", "/prompt.txt",
                "3", "10", "2000", "60",
                "https://api.anthropic.com", "claude-3-sonnet", "30",
                EffectiveApiKeyDescriptor.absent(), "",
                "https://api.openai.com", "gpt-4", "30",
                EffectiveApiKeyDescriptor.absent(), "");

        GuiTechnicalTestCoordinator coordinator = new GuiTechnicalTestCoordinator(
                orchestrator,
                () -> blankInput,
                () -> "",
                () -> "",
                messages,
                postResultCallback);

        // Override thread factory to run task inline (synchronous, no real thread spawn)
        coordinator.testThreadFactory = task -> new Thread(task, "sync-test-thread") {
            @Override
            public void start() {
                // Run the task inline instead of starting a new thread
                run();
            }
        };
        // Override result delivery to be synchronous (direct call)
        coordinator.resultDelivery = Runnable::run;

        return coordinator;
    }

    private static void runOnFx(ThrowingRunnable task) throws Exception {
        AtomicReference<Throwable> error = new 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");
        rethrow(error);
    }

    private static void rethrow(AtomicReference<Throwable> error) throws Exception {
        Throwable t = error.get();
        if (t == null) {
            return;
        }
        if (t instanceof Exception ex) {
            throw ex;
        }
        throw new AssertionError("Unexpected error", t);
    }

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