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.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
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 org.junit.jupiter.api.io.TempDir;

import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorState;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorStateFactory;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationFileSnapshot;
import javafx.application.Platform;

/**
 * Integration tests for the GUI startup context and configuration loading path.
 * <p>
 * Verifies that a valid {@code --config} path supplied at startup reaches the workspace as a
 * loaded editor state, and that starting without a configuration path leaves the workspace in
 * the defined welcome-text state.
 *
 * <h2>Test scope</h2>
 * <ul>
 *   <li>CLI argument path → resolved config path → {@link GuiConfigurationEditorState} populated
 *       with file values, header showing the path, all fields filled from the file.</li>
 *   <li>No {@code --config} argument → blank start state, header path empty, welcome guidance
 *       visible.</li>
 *   <li>Invalid / non-existent {@code --config} path → startup notice present, blank state used.</li>
 * </ul>
 *
 * <h2>Design</h2>
 * <p>
 * These tests exercise the file-loading callback and workspace initialization directly without
 * starting the full Bootstrap or a real JavaFX {@link javafx.application.Application}. The
 * workspace is created on the FX Application Thread under the Monocle headless configuration.
 */
class GuiEditorIntegrationTest {

    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().
    }

    // =========================================================================
    // GUI startup with a valid --config path
    // =========================================================================

    /**
     * Verifies the end-to-end path: CLI argument → config path → file loader → workspace.
     * <p>
     * When Bootstrap resolves a valid {@code --config} path, {@link GuiConfigurationEditorState}
     * is populated from the file contents. The workspace header shows the path, the editor is
     * not in blank state, and fields reflect the values stored in the file.
     *
     * @param tempDir JUnit-provided temporary directory for the test configuration file
     * @throws Exception if the FX thread task fails or times out
     */
    @Test
    void guiStartup_withValidConfigPath_loadsFileIntoWorkspace(@TempDir Path tempDir) throws Exception {
        Path configFile = tempDir.resolve("test-application.properties");
        writeMinimalPropertiesFile(configFile, "./my/source", "./my/target", "claude");

        // Simulate what Bootstrap does: file loader delegates to BootstrapRunner.loadGuiConfigurationState.
        // Here we use the factory directly since Bootstrap's private method is not testable from outside
        // the bootstrap module. The contract tested here is the file → editor state → workspace flow.
        GuiConfigurationFileLoader fileLoader = path -> {
            try {
                java.util.Properties props = new java.util.Properties();
                String content = Files.readString(path, StandardCharsets.UTF_8);
                props.load(new java.io.StringReader(content));
                GuiConfigurationFileSnapshot snapshot = new GuiConfigurationFileSnapshot(path, props);
                return GuiConfigurationEditorStateFactory.fromPropertiesSnapshot(snapshot, Optional.empty());
            } catch (IOException e) {
                throw new GuiConfigurationLoadException("Failed to load " + path, e);
            }
        };

        GuiConfigurationEditorState loadedState = fileLoader.load(configFile);
        GuiConfigurationFileWriter noOpWriter = (values, path) -> GuiConfigurationSaveResult.saved(path);
        GuiStartupContext context = new GuiStartupContext(loadedState, Optional.empty(), fileLoader, noOpWriter,
                req -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req.providerIdentifier(), "kein Port im Test"),
                (family, propertyValue) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent(),
                        new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService(
                                req2 -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req2.providerIdentifier(), "kein Port im Test"),
                                (fam2, pv2) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent()),
                        new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.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; }
                        },
                new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.TechnicalTestOrchestrator(
                        new de.gecheckt.pdf.umbenenner.application.validation.editor.EditorConfigurationValidator(),
                        new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.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; }
                        },
                        new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService(
                                req99 -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req99.providerIdentifier(), "kein Port im Test"),
                                (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent()),
                        () -> java.util.Optional.empty()),
                new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService(
                        new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() {
                            @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome createDirectory(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.CreateDirectory s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); }
                            @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome createPromptFile(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.CreatePromptFile s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); }
                            @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome prepareSqlitePath(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.PrepareSqlitePath s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); }
                        }));

        AtomicReference<Throwable> error = new AtomicReference<>();
        CountDownLatch latch = new CountDownLatch(1);

        Platform.runLater(() -> {
            try {
                GuiConfigurationEditorWorkspace workspace = new GuiConfigurationEditorWorkspace(context);

                // Header must show the config file path.
                String headerPath = workspace.configurationPathText();
                assertEquals(configFile.toString(), headerPath,
                        "Header path must reflect the loaded configuration file path");

                // Workspace must not be in blank/welcome state.
                assertFalse(workspace.isWelcomeGuidanceVisible(),
                        "Welcome guidance must not be visible when a configuration is loaded at startup");

                // Editor state must carry the loaded file snapshot.
                assertTrue(workspace.editorState().hasLoadedFileSnapshot(),
                        "Editor state must have a file snapshot after loading via startup context");

                // Field values must match what was written to the file.
                assertEquals("./my/source", workspace.editorState().values().sourceFolder(),
                        "Source folder must be populated from the loaded configuration file");
                assertEquals("./my/target", workspace.editorState().values().targetFolder(),
                        "Target folder must be populated from the loaded configuration file");
                assertEquals("claude", workspace.editorState().values().activeProviderFamily(),
                        "Active provider must be populated from the loaded configuration file");

                // Editor must not be dirty right after loading.
                assertFalse(workspace.editorState().isDirty(),
                        "Editor state must be clean immediately after loading from disk");

            } catch (Throwable t) {
                error.set(t);
            } finally {
                latch.countDown();
            }
        });

        assertTrue(latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS),
                "FX task must complete within timeout");
        if (error.get() != null) {
            throw new AssertionError("FX thread threw an exception", error.get());
        }
    }

    // =========================================================================
    // GUI startup without a --config path
    // =========================================================================

    /**
     * Verifies that starting the GUI without a {@code --config} argument shows the standard
     * template defaults immediately: header path is empty, welcome guidance is hidden, the
     * editor is not in dirty state, and the standard default values are populated.
     *
     * @throws Exception if the FX thread task fails or times out
     */
    @Test
    void guiStartup_withoutConfigPath_showsStandardTemplateDefaults() throws Exception {
        GuiStartupContext blankContext = GuiStartupContext.blank(Optional.empty());

        AtomicReference<Throwable> error = new AtomicReference<>();
        CountDownLatch latch = new CountDownLatch(1);

        Platform.runLater(() -> {
            try {
                GuiConfigurationEditorWorkspace workspace = new GuiConfigurationEditorWorkspace(blankContext);

                assertEquals("", workspace.configurationPathText(),
                        "Header path must be empty when no configuration is loaded");
                assertFalse(workspace.isWelcomeGuidanceVisible(),
                        "Welcome guidance must stay hidden because the standard template is shown immediately");
                assertFalse(workspace.editorState().hasLoadedFileSnapshot(),
                        "Editor state must have no file snapshot in default start state");
                assertFalse(workspace.editorState().isDirty(),
                        "Default start state must not be dirty");
                assertEquals("./work/local/source", workspace.editorState().values().sourceFolder(),
                        "Default start state must populate the standard source folder");

            } catch (Throwable t) {
                error.set(t);
            } finally {
                latch.countDown();
            }
        });

        assertTrue(latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS),
                "FX task must complete within timeout");
        if (error.get() != null) {
            throw new AssertionError("FX thread threw an exception", error.get());
        }
    }

    // =========================================================================
    // GUI startup with a non-existent --config path (mirrors Bootstrap behavior)
    // =========================================================================

    /**
     * Verifies that when Bootstrap receives a {@code --config} path that does not exist, it
     * builds a startup context with a startup notice and a blank editor state. The workspace
     * starts without a configuration but shows the notice in the status area.
     * <p>
     * This test mirrors the Bootstrap behavior documented in {@code BootstrapRunner}:
     * a missing GUI config path is logged, and the context carries a notice but falls back
     * to the blank start state.
     *
     * @throws Exception if the FX thread task fails or times out
     */
    @Test
    void guiStartup_withNonExistentConfigPath_usesBlankStateAndCarriesStartupNotice()
            throws Exception {
        // Simulate what Bootstrap does when --config points to a missing file.
        String notice = "Konfigurationsdatei nicht gefunden: /no/such/file.properties\n"
                + "Die GUI startet ohne Konfigurationsdatei.";
        GuiConfigurationEditorState blankState = GuiConfigurationEditorStateFactory.createBlankStartState();
        GuiConfigurationFileWriter noOpWriter = (values, path) -> GuiConfigurationSaveResult.saved(path);
        GuiStartupContext context = new GuiStartupContext(
                blankState,
                Optional.of(notice),
                configFilePath -> GuiConfigurationEditorStateFactory.createBlankStartState(),
                noOpWriter,
                req -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req.providerIdentifier(), "kein Port im Test"),
                (family, propertyValue) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent(),
                        new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService(
                                req2 -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req2.providerIdentifier(), "kein Port im Test"),
                                (fam2, pv2) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent()),
                        new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.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; }
                        },
                new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.TechnicalTestOrchestrator(
                        new de.gecheckt.pdf.umbenenner.application.validation.editor.EditorConfigurationValidator(),
                        new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.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; }
                        },
                        new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService(
                                req99 -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req99.providerIdentifier(), "kein Port im Test"),
                                (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent()),
                        () -> java.util.Optional.empty()),
                new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService(
                        new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() {
                            @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome createDirectory(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.CreateDirectory s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); }
                            @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome createPromptFile(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.CreatePromptFile s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); }
                            @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome prepareSqlitePath(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.PrepareSqlitePath s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); }
                        }));

        AtomicReference<Throwable> error = new AtomicReference<>();
        CountDownLatch latch = new CountDownLatch(1);

        Platform.runLater(() -> {
            try {
                GuiConfigurationEditorWorkspace workspace = new GuiConfigurationEditorWorkspace(context);

                assertFalse(workspace.isWelcomeGuidanceVisible(),
                        "Welcome guidance must stay hidden because the standard template is shown immediately");
                assertEquals("", workspace.configurationPathText(),
                        "Header path must be empty when config file was not found");
                assertFalse(workspace.editorState().hasLoadedFileSnapshot(),
                        "No file snapshot must be present when config file was not found");

            } catch (Throwable t) {
                error.set(t);
            } finally {
                latch.countDown();
            }
        });

        assertTrue(latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS),
                "FX task must complete within timeout");
        if (error.get() != null) {
            throw new AssertionError("FX thread threw an exception", error.get());
        }
    }

    // =========================================================================
    // GUI startup with a non-existent --config path: startup notice rendered in header
    // =========================================================================

    /**
     * Verifies that when the workspace is constructed with a startup notice (as Bootstrap does
     * when {@code --config} points to a non-existent file), the notice text is rendered in the
     * visible header status label.
     * <p>
     * This complements {@link #guiStartup_withNonExistentConfigPath_usesBlankStateAndCarriesStartupNotice}
     * which only verifies the blank editor state. This test verifies the user-visible side: the
     * notice must be rendered so the user can read it.
     *
     * @throws Exception if the FX thread task fails or times out
     */
    @Test
    void guiStartup_withNonExistentConfigPath_noticeIsRenderedInHeaderStatusLabel()
            throws Exception {
        String notice = "Konfigurationsdatei nicht gefunden: /no/such/file.properties\n"
                + "Die GUI startet ohne Konfigurationsdatei.";
        GuiConfigurationEditorState blankState = GuiConfigurationEditorStateFactory.createBlankStartState();
        GuiConfigurationFileWriter noOpWriter = (values, path) -> GuiConfigurationSaveResult.saved(path);
        GuiStartupContext context = new GuiStartupContext(
                blankState,
                Optional.of(notice),
                configFilePath -> GuiConfigurationEditorStateFactory.createBlankStartState(),
                noOpWriter,
                req -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req.providerIdentifier(), "kein Port im Test"),
                (family, propertyValue) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent(),
                        new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService(
                                req2 -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req2.providerIdentifier(), "kein Port im Test"),
                                (fam2, pv2) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent()),
                        new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.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; }
                        },
                new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.TechnicalTestOrchestrator(
                        new de.gecheckt.pdf.umbenenner.application.validation.editor.EditorConfigurationValidator(),
                        new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.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; }
                        },
                        new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService(
                                req99 -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req99.providerIdentifier(), "kein Port im Test"),
                                (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent()),
                        () -> java.util.Optional.empty()),
                new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService(
                        new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() {
                            @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome createDirectory(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.CreateDirectory s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); }
                            @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome createPromptFile(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.CreatePromptFile s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); }
                            @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome prepareSqlitePath(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.PrepareSqlitePath s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); }
                        }));

        AtomicReference<Throwable> error = new AtomicReference<>();
        CountDownLatch latch = new CountDownLatch(1);

        Platform.runLater(() -> {
            try {
                GuiConfigurationEditorWorkspace workspace = new GuiConfigurationEditorWorkspace(context);

                // The startup notice must be rendered in the visible header status label.
                javafx.scene.control.Label noticeLabel = workspace.statusNoticeLabel();
                assertNotNull(noticeLabel, "Header status label must not be null");
                assertTrue(noticeLabel.isVisible(),
                        "Header status label must be visible when a startup notice is present");
                assertTrue(noticeLabel.isManaged(),
                        "Header status label must be managed when a startup notice is present");
                assertFalse(noticeLabel.getText().isBlank(),
                        "Header status label text must not be blank when startup notice is present");
                assertTrue(noticeLabel.getText().contains("Konfigurationsdatei nicht gefunden"),
                        "Header status label must contain the notice text; got: " + noticeLabel.getText());

            } catch (Throwable t) {
                error.set(t);
            } finally {
                latch.countDown();
            }
        });

        assertTrue(latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS),
                "FX task must complete within timeout");
        if (error.get() != null) {
            throw new AssertionError("FX thread threw an exception", error.get());
        }
    }

    // =========================================================================
    // --config path resolution: static helper (no FX thread needed)
    // =========================================================================

    /**
     * Verifies that the startup argument containing a config path string is correctly
     * resolved to a {@link Path} that can be forwarded to the file loader.
     * <p>
     * This test exercises the contract: a non-empty {@code --config} argument string becomes
     * the config path used for loading; an absent argument leads to the blank start state.
     *
     * @param tempDir JUnit-provided temporary directory
     * @throws Exception if file operations fail
     */
    @Test
    void configPathFromCliArg_validFile_resolvedPathMatchesArgument(@TempDir Path tempDir)
            throws Exception {
        Path configFile = tempDir.resolve("cli-config.properties");
        writeMinimalPropertiesFile(configFile, "./src", "./tgt", "openai-compatible");

        // The path string from --config must resolve to the same canonical path.
        Optional<String> configArgValue = Optional.of(configFile.toString());
        Path resolvedPath = configArgValue.map(java.nio.file.Paths::get).orElseThrow();

        assertTrue(Files.exists(resolvedPath),
                "Resolved path from --config argument must point to an existing file");
        assertEquals(configFile.toAbsolutePath(), resolvedPath.toAbsolutePath(),
                "Resolved path must match the path provided in the --config argument");
    }

    // =========================================================================
    // Helper
    // =========================================================================

    /**
     * Writes a minimal valid properties file to the given path for use in loading tests.
     *
     * @param path           the target file path
     * @param sourceFolder   value for {@code source.folder}
     * @param targetFolder   value for {@code target.folder}
     * @param activeProvider value for {@code ai.provider.active}
     * @throws IOException if writing fails
     */
    private static void writeMinimalPropertiesFile(Path path,
                                                    String sourceFolder,
                                                    String targetFolder,
                                                    String activeProvider) throws IOException {
        String content = "source.folder=" + sourceFolder + "\n"
                + "target.folder=" + targetFolder + "\n"
                + "ai.provider.active=" + activeProvider + "\n"
                + "sqlite.file=./work/test.db\n"
                + "max.retries.transient=3\n"
                + "max.pages=10\n"
                + "max.text.characters=1000\n"
                + "prompt.template.file=./config/prompt.txt\n";
        Files.writeString(path, content, StandardCharsets.UTF_8);
    }
}
