package de.gecheckt.pdf.umbenenner.bootstrap;

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

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import de.gecheckt.pdf.umbenenner.adapter.in.cli.SchedulerBatchCommand;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiAdapter;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiStartupContext;
import de.gecheckt.pdf.umbenenner.adapter.out.bootstrap.validation.StartConfigurationValidator;
import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
import de.gecheckt.pdf.umbenenner.application.config.provider.MultiProviderConfiguration;
import de.gecheckt.pdf.umbenenner.application.config.provider.ProviderConfiguration;
import de.gecheckt.pdf.umbenenner.application.config.startup.StartConfiguration;
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunOutcome;
import de.gecheckt.pdf.umbenenner.application.port.out.ConfigurationPort;
import de.gecheckt.pdf.umbenenner.bootstrap.startup.StartupArguments;
import de.gecheckt.pdf.umbenenner.bootstrap.startup.StartupMode;

/**
 * Unit tests for Bootstrap startup mode dispatch in {@link BootstrapRunner}.
 * <p>
 * Covers the two startup paths (GUI and headless) and the config path resolution
 * logic, using controlled factory injection to avoid real JavaFX and file I/O.
 */
class BootstrapRunnerStartupDispatchTest {

    @TempDir
    Path tempDir;

    // --- GUI dispatch ---

    @Test
    void run_withGuiMode_returnsZeroWhenGuiStartSucceeds() {
        BootstrapRunner runner = runnerWithGuiFactory(() -> new GuiAdapter() {
            @Override
            public void start(GuiStartupContext startupContext) {
                // normal termination: returns without throwing
            }
        });

        int exitCode = runner.run(new StartupArguments(StartupMode.GUI, Optional.empty()));

        assertEquals(0, exitCode, "Normal GUI shutdown should return exit code 0");
    }

    @Test
    void run_withGuiMode_returnsOneWhenGuiStartThrowsException() {
        BootstrapRunner runner = runnerWithGuiFactory(() -> new GuiAdapter() {
            @Override
            public void start(GuiStartupContext startupContext) {
                throw new RuntimeException("simulated GUI startup failure");
            }
        });

        int exitCode = runner.run(new StartupArguments(StartupMode.GUI, Optional.empty()));

        assertEquals(1, exitCode, "GUI startup exception should return exit code 1");
    }

    @Test
    void run_withGuiMode_returnsOneWhenGuiFactoryThrowsException() {
        BootstrapRunner runner = runnerWithGuiFactory(() -> {
            throw new IllegalStateException("simulated factory failure");
        });

        int exitCode = runner.run(new StartupArguments(StartupMode.GUI, Optional.empty()));

        assertEquals(1, exitCode, "GuiAdapterFactory exception should return exit code 1");
    }

    // --- Headless dispatch ---

    @Test
    void run_withHeadlessMode_runsHeadlessBatch() throws Exception {
        BootstrapRunner runner = new BootstrapRunner(
                configPath -> buildValidConfigPort(),
                lockFile -> new MockRunLockPort(),
                StartConfigurationValidator::new,
                jdbcUrl -> new MockSchemaInitializationPort(),
                (config, lock) -> new MockRunBatchProcessingUseCase(true),
                useCase -> new SchedulerBatchCommand(useCase)
        );

        int exitCode = runner.run(new StartupArguments(StartupMode.HEADLESS, Optional.empty()));

        assertEquals(0, exitCode, "Successful headless batch should return exit code 0");
    }

    @Test
    void run_withHeadlessMode_configPathOverrideIsPassedToFactory() throws Exception {
        AtomicReference<Path> receivedPath = new AtomicReference<>();
        // The file must exist: headless mode requires the --config file to be present.
        Path existingConfigFile = Files.createFile(tempDir.resolve("custom-config.properties"));

        BootstrapRunner runner = new BootstrapRunner(
                configPath -> {
                    receivedPath.set(configPath);
                    return buildValidConfigPort();
                },
                lockFile -> new MockRunLockPort(),
                StartConfigurationValidator::new,
                jdbcUrl -> new MockSchemaInitializationPort(),
                (config, lock) -> new MockRunBatchProcessingUseCase(true),
                useCase -> new SchedulerBatchCommand(useCase)
        );

        runner.run(new StartupArguments(StartupMode.HEADLESS, Optional.of(existingConfigFile.toString())));

        assertEquals(existingConfigFile, receivedPath.get(),
                "The existing config path override should be passed to the ConfigurationPortFactory");
    }

    @Test
    void run_withHeadlessMode_usesDefaultConfigPathWhenNoOverride() {
        AtomicReference<Path> receivedPath = new AtomicReference<>();

        BootstrapRunner runner = new BootstrapRunner(
                configPath -> {
                    receivedPath.set(configPath);
                    return buildValidConfigPort();
                },
                lockFile -> new MockRunLockPort(),
                StartConfigurationValidator::new,
                jdbcUrl -> new MockSchemaInitializationPort(),
                (config, lock) -> new MockRunBatchProcessingUseCase(true),
                useCase -> new SchedulerBatchCommand(useCase)
        );

        runner.run(new StartupArguments(StartupMode.HEADLESS, Optional.empty()));

        assertEquals(Paths.get("config/application.properties"), receivedPath.get(),
                "Default config path should be used when no --config override is present");
    }

    // --- Headless isolation: GUI adapter factory must not be invoked ---

    /**
     * Verifies that the GUI adapter factory is never invoked during a headless startup.
     * <p>
     * This is the automated proof that the headless start path does not initialize the
     * JavaFX Application class. The {@link BootstrapRunner.GuiAdapterFactory} is the sole
     * gateway through which the JavaFX lifecycle would be entered. If the factory is never
     * called, the JavaFX Application class is never loaded or initialized in the headless path.
     * <p>
     * A tracking factory is injected that records whether it was called. The test runs a
     * full headless batch cycle and then asserts the factory was not invoked.
     */
    @Test
    void headlessStart_doesNotInvokeGuiAdapterFactory() {
        AtomicReference<Boolean> guiFactoryCalled = new AtomicReference<>(false);

        BootstrapRunner runner = new BootstrapRunner(
                path -> { /* no-op migration */ },
                configPath -> buildValidConfigPort(),
                lockFile -> new MockRunLockPort(),
                StartConfigurationValidator::new,
                jdbcUrl -> new MockSchemaInitializationPort(),
                (config, lock) -> new MockRunBatchProcessingUseCase(true),
                useCase -> new SchedulerBatchCommand(useCase),
                () -> {
                    guiFactoryCalled.set(true);
                    throw new AssertionError(
                            "GuiAdapterFactory must not be invoked during headless startup — "
                            + "invoking it would trigger the JavaFX Application class lifecycle");
                }
        );

        int exitCode = runner.run(new StartupArguments(StartupMode.HEADLESS, Optional.empty()));

        assertEquals(0, exitCode, "Headless batch must complete successfully");
        assertFalse(guiFactoryCalled.get(),
                "GuiAdapterFactory must not be invoked during headless startup: "
                + "the JavaFX Application class must not be initialized in the headless path");
    }

    // --- resolveEffectiveConfigPath static helper ---

    @Test
    void resolveEffectiveConfigPath_returnsOverrideWhenPresent() {
        Path result = BootstrapRunner.resolveEffectiveConfigPath(Optional.of("custom/my.properties"));
        assertEquals(Paths.get("custom/my.properties"), result);
    }

    @Test
    void resolveEffectiveConfigPath_returnsDefaultWhenEmpty() {
        Path result = BootstrapRunner.resolveEffectiveConfigPath(Optional.empty());
        assertEquals(Paths.get("config/application.properties"), result);
    }

    // --- Helpers ---

    /**
     * Builds a minimal valid {@link ConfigurationPort} backed by real temp-dir paths
     * so that headless dispatch tests can pass schema initialization and validation.
     */
    private ConfigurationPort buildValidConfigPort() {
        try {
            java.nio.file.Files.createDirectories(tempDir.resolve("source"));
            java.nio.file.Files.createDirectories(tempDir.resolve("target"));
            Path sqliteFile = tempDir.resolve("db.sqlite");
            if (!java.nio.file.Files.exists(sqliteFile)) {
                java.nio.file.Files.createFile(sqliteFile);
            }
            Path promptFile = tempDir.resolve("prompt.txt");
            if (!java.nio.file.Files.exists(promptFile)) {
                java.nio.file.Files.createFile(promptFile);
            }
            ProviderConfiguration providerConfig = new ProviderConfiguration(
                    "gpt-4", 30, "https://api.example.com", "test-key");
            MultiProviderConfiguration multiConfig = new MultiProviderConfiguration(
                    AiProviderFamily.OPENAI_COMPATIBLE, providerConfig, null);
            StartConfiguration config = new StartConfiguration(
                    tempDir.resolve("source"),
                    tempDir.resolve("target"),
                    sqliteFile,
                    multiConfig,
                    3, 100, 50000, 60,
                    promptFile,
                    tempDir.resolve("lock.lock"),
                    tempDir.resolve("logs"),
                    "INFO",
                    false
            );
            return () -> config;
        } catch (Exception e) {
            throw new RuntimeException("Failed to build valid ConfigurationPort for test", e);
        }
    }

    /**
     * Creates a {@link BootstrapRunner} wired with a controllable GUI factory and
     * stub factories for all headless-path dependencies (which are not exercised in GUI tests).
     */
    private BootstrapRunner runnerWithGuiFactory(BootstrapRunner.GuiAdapterFactory guiAdapterFactory) {
        return new BootstrapRunner(
                path -> { /* no-op migration */ },
                configPath -> { throw new AssertionError("ConfigurationPort must not be called in GUI mode"); },
                lockFile -> { throw new AssertionError("RunLockPort must not be called in GUI mode"); },
                () -> { throw new AssertionError("Validator must not be called in GUI mode"); },
                jdbcUrl -> { throw new AssertionError("SchemaInitPort must not be called in GUI mode"); },
                (config, lock) -> { throw new AssertionError("UseCaseFactory must not be called in GUI mode"); },
                useCase -> { throw new AssertionError("CommandFactory must not be called in GUI mode"); },
                guiAdapterFactory
        );
    }

    // --- Shared mock helpers (mirroring BootstrapRunnerTest pattern) ---

    private static class MockRunLockPort implements de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort {
        @Override public void acquire() {
            // intentionally empty
        }
        @Override public void release() {
            // intentionally empty
        }
        @Override
        public java.util.Optional<de.gecheckt.pdf.umbenenner.application.port.out.RunLockHandle> tryAcquire() {
            return java.util.Optional.empty();
        }
    }

    private static class MockSchemaInitializationPort
            implements de.gecheckt.pdf.umbenenner.application.port.out.PersistenceSchemaInitializationPort {
        @Override public void initializeSchema() {
            // intentionally empty
        }
    }

    private static class MockRunBatchProcessingUseCase
            implements de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProcessingUseCase {
        private final boolean success;
        MockRunBatchProcessingUseCase(boolean success) { this.success = success; }
        @Override
        public BatchRunOutcome execute(de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext context) {
            return success ? BatchRunOutcome.SUCCESS : BatchRunOutcome.FAILURE;
        }
    }
}
