package de.gecheckt.pdf.umbenenner.bootstrap;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
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.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.appender.AbstractAppender;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.Property;
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.out.bootstrap.validation.InvalidStartConfigurationException;
import de.gecheckt.pdf.umbenenner.adapter.out.bootstrap.validation.StartConfigurationValidator;
import de.gecheckt.pdf.umbenenner.adapter.out.configuration.ConfigurationLoadingException;
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.in.BatchRunProcessingUseCase;
import de.gecheckt.pdf.umbenenner.application.port.out.AiContentSensitivity;
import de.gecheckt.pdf.umbenenner.application.port.out.ConfigurationPort;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentPersistenceException;
import de.gecheckt.pdf.umbenenner.application.port.out.PersistenceSchemaInitializationPort;
import de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort;
import de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext;

/**
 * Edge-case and boundary tests for {@link BootstrapRunner}.
 * <p>
 * Complements {@link BootstrapRunnerTest} with focused testing of:
 * - Lock file path resolution logic with edge cases
 * - JDBC URL construction with various path formats
 * - Run context creation and lifecycle
 * - Exception types and their handling
 * - Specific outcome-to-exit-code mapping scenarios
 */
class BootstrapRunnerEdgeCasesTest {

    @TempDir
    Path tempDir;

    // =========================================================================
    // Lock File Path Resolution Tests
    // =========================================================================

    @Test
    void resolveLockFilePath_withNullPath_usesDefault() throws Exception {
        // When config.runtimeLockFile() returns null, default should be applied
        StartConfiguration configWithNullLock = new StartConfiguration(
                Files.createDirectories(tempDir.resolve("source")),
                Files.createDirectories(tempDir.resolve("target")),
                Files.createFile(tempDir.resolve("db.sqlite")),
                validMultiProviderConfig(),
                3,
                100,
                50000,
                60,
                Files.createFile(tempDir.resolve("prompt.txt")),
                null,  // null runtimeLockFile
                tempDir.resolve("logs"),
                "INFO",
                false
        );

        AtomicReference<Path> capturedLockPath = new AtomicReference<>();

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

        int exitCode = runner.run();

        assertEquals(0, exitCode);
        assertNotNull(capturedLockPath.get(), "Lock path should have been defaulted");
        assertFalse(capturedLockPath.get().toString().isBlank(),
                "Defaulted lock path must not be blank");
        assertTrue(capturedLockPath.get().toString().contains("pdf-umbenenner.lock"),
                "Default lock path should contain standard name");
    }

    // =========================================================================
    // JDBC URL Construction Tests
    // =========================================================================

    @Test
    void buildJdbcUrl_withSimplePath_constructsValidUrl() throws Exception {
        Path sqliteFile = Files.createFile(tempDir.resolve("test.db"));

        StartConfiguration config = new StartConfiguration(
                Files.createDirectories(tempDir.resolve("source")),
                Files.createDirectories(tempDir.resolve("target")),
                sqliteFile,
                validMultiProviderConfig(),
                3, 100, 50000, 60,
                Files.createFile(tempDir.resolve("prompt.txt")),
                tempDir.resolve("lock.lock"),
                tempDir.resolve("logs"),
                "INFO",
                false
        );

        String jdbcUrl = BootstrapRunner.buildJdbcUrl(config);

        assertTrue(jdbcUrl.startsWith("jdbc:sqlite:"), "JDBC URL should have correct prefix");
        assertTrue(jdbcUrl.contains(sqliteFile.toAbsolutePath().toString().replace('\\', '/')),
                "JDBC URL should contain the absolute file path");
    }

    @Test
    void buildJdbcUrl_withPathContainingSpaces_handlesCorrectly() throws Exception {
        Path baseDir = Files.createDirectories(tempDir.resolve("dir with spaces"));
        Path sqliteFile = Files.createFile(baseDir.resolve("db file.sqlite"));

        StartConfiguration config = new StartConfiguration(
                Files.createDirectories(tempDir.resolve("source")),
                Files.createDirectories(tempDir.resolve("target")),
                sqliteFile,
                validMultiProviderConfig(),
                3, 100, 50000, 60,
                Files.createFile(tempDir.resolve("prompt.txt")),
                tempDir.resolve("lock.lock"),
                tempDir.resolve("logs"),
                "INFO",
                false
        );

        String jdbcUrl = BootstrapRunner.buildJdbcUrl(config);

        assertTrue(jdbcUrl.contains("db file.sqlite"), "JDBC URL should preserve spaces in filename");
        assertFalse(jdbcUrl.contains("\\"), "JDBC URL should use forward slashes, not backslashes");
    }

    // =========================================================================
    // Run Context Tests
    // =========================================================================

    @Test
    void createRunContext_generatesUniqueRunId() throws Exception {
        new StartConfiguration(
                Files.createDirectories(tempDir.resolve("source")),
                Files.createDirectories(tempDir.resolve("target")),
                Files.createFile(tempDir.resolve("db.sqlite")),
                validMultiProviderConfig(),
                3, 100, 50000, 60,
                Files.createFile(tempDir.resolve("prompt.txt")),
                tempDir.resolve("lock.lock"),
                tempDir.resolve("logs"),
                "INFO",
                false
        );

        // Verify BatchRunContext can be created (used internally by BootstrapRunner)
        assertDoesNotThrow(() -> {
            de.gecheckt.pdf.umbenenner.domain.model.RunId runId =
                    new de.gecheckt.pdf.umbenenner.domain.model.RunId("test-run-id");
            BatchRunContext context = new BatchRunContext(runId, java.time.Instant.now());
            assertNotNull(context, "Run context should be creatable");
        }, "Run context creation should not throw");
    }

    // =========================================================================
    // Exception Handling Edge Cases
    // =========================================================================

    @Test
    void run_returnsOneWhenGenericExceptionDuringExecution() throws Exception {
        ConfigurationPort mockConfigPort = new MockConfigurationPort(tempDir, true);

        BootstrapRunner runner = new BootstrapRunner(
                configPath -> mockConfigPort,
                lockFile -> new MockRunLockPort(),
                StartConfigurationValidator::new,
                jdbcUrl -> new MockSchemaInitializationPort(),
                (config, lock) -> {
                    throw new NullPointerException("Simulated NPE in use case factory");
                },
                useCase -> new SchedulerBatchCommand(useCase)
        );

        int exitCode = runner.run();

        assertEquals(1, exitCode, "Generic exception during execution should return exit code 1");
    }

    @Test
    void run_distinguishesBetweenConfigLoadingAndValidationFailure() throws Exception {
        // Test 1: Configuration loading exception
        BootstrapRunner runnerLoadFailure = new BootstrapRunner(
                configPath -> {
                    throw new ConfigurationLoadingException("Load failed");
                },
                lockFile -> new MockRunLockPort(),
                StartConfigurationValidator::new,
                jdbcUrl -> new MockSchemaInitializationPort(),
                (config, lock) -> new MockRunBatchProcessingUseCase(true),
                useCase -> new SchedulerBatchCommand(useCase)
        );

        assertEquals(1, runnerLoadFailure.run(),
                "Configuration loading failure should return exit code 1");

        // Test 2: Configuration validation exception
        BootstrapRunner runnerValidationFailure = new BootstrapRunner(
                configPath -> () -> {
                    try {
                        Path sourceDir = Files.createDirectories(tempDir.resolve("source"));
                        Path targetDir = Files.createDirectories(tempDir.resolve("target"));
                        Path dbFile = Files.createFile(tempDir.resolve("db.sqlite"));
                        Path promptFile = Files.createFile(tempDir.resolve("prompt.txt"));
                        return new StartConfiguration(sourceDir, targetDir, dbFile,
                                validMultiProviderConfig(), 3, 100, 50000, 60,
                                promptFile, tempDir.resolve("lock.lock"), tempDir.resolve("logs"),
                                "INFO", false);
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                },
                lockFile -> new MockRunLockPort(),
                () -> new StartConfigurationValidator() {
                    @Override
                    public void validate(StartConfiguration config) {
                        throw new InvalidStartConfigurationException("Validation failed");
                    }
                },
                jdbcUrl -> new MockSchemaInitializationPort(),
                (config, lock) -> new MockRunBatchProcessingUseCase(true),
                useCase -> new SchedulerBatchCommand(useCase)
        );

        assertEquals(1, runnerValidationFailure.run(),
                "Configuration validation failure should return exit code 1");
    }

    @Test
    void run_persistenceExceptionDuringSchemaInitIsHardFailure() throws Exception {
        ConfigurationPort mockConfigPort = new MockConfigurationPort(tempDir, true);

        BootstrapRunner runner = new BootstrapRunner(
                configPath -> mockConfigPort,
                lockFile -> new MockRunLockPort(),
                StartConfigurationValidator::new,
                jdbcUrl -> new PersistenceSchemaInitializationPort() {
                    @Override
                    public void initializeSchema() {
                        throw new DocumentPersistenceException("Database connection failed");
                    }
                },
                (config, lock) -> new MockRunBatchProcessingUseCase(true),
                useCase -> new SchedulerBatchCommand(useCase)
        );

        int exitCode = runner.run();

        assertEquals(1, exitCode, "Persistence exception during schema init should return exit code 1");
    }

    // =========================================================================
    // Outcome to Exit Code Mapping
    // =========================================================================

    @Test
    void mapOutcomeToExitCode_successOutcomeReturnsZero() throws Exception {
        ConfigurationPort mockConfigPort = new MockConfigurationPort(tempDir, true);

        BootstrapRunner runner = new BootstrapRunner(
                configPath -> mockConfigPort,
                lockFile -> new MockRunLockPort(),
                StartConfigurationValidator::new,
                jdbcUrl -> new MockSchemaInitializationPort(),
                (config, lock) -> context -> BatchRunOutcome.SUCCESS,
                useCase -> new SchedulerBatchCommand(useCase)
        );

        assertEquals(0, runner.run(), "SUCCESS outcome should map to exit code 0");
    }

    @Test
    void mapOutcomeToExitCode_failureOutcomeReturnsOne() throws Exception {
        ConfigurationPort mockConfigPort = new MockConfigurationPort(tempDir, true);

        BootstrapRunner runner = new BootstrapRunner(
                configPath -> mockConfigPort,
                lockFile -> new MockRunLockPort(),
                StartConfigurationValidator::new,
                jdbcUrl -> new MockSchemaInitializationPort(),
                (config, lock) -> context -> BatchRunOutcome.FAILURE,
                useCase -> new SchedulerBatchCommand(useCase)
        );

        assertEquals(1, runner.run(), "FAILURE outcome should map to exit code 1");
    }

    @Test
    void mapOutcomeToExitCode_lockUnavailableReturnsOne() throws Exception {
        ConfigurationPort mockConfigPort = new MockConfigurationPort(tempDir, true);

        BootstrapRunner runner = new BootstrapRunner(
                configPath -> mockConfigPort,
                lockFile -> new MockRunLockPort(),
                StartConfigurationValidator::new,
                jdbcUrl -> new MockSchemaInitializationPort(),
                (config, lock) -> context -> BatchRunOutcome.LOCK_UNAVAILABLE,
                useCase -> new SchedulerBatchCommand(useCase)
        );

        assertEquals(1, runner.run(), "LOCK_UNAVAILABLE outcome should map to exit code 1");
    }

    // =========================================================================
    // AI Content Sensitivity Resolution
    // =========================================================================

    @Test
    void resolveAiContentSensitivity_falseYieldsProtectSensitiveContent() {
        assertEquals(AiContentSensitivity.PROTECT_SENSITIVE_CONTENT,
                BootstrapRunner.resolveAiContentSensitivity(false),
                "logAiSensitive=false must resolve to PROTECT_SENSITIVE_CONTENT (safe default)");
    }

    @Test
    void resolveAiContentSensitivity_trueYieldsLogSensitiveContent() {
        assertEquals(AiContentSensitivity.LOG_SENSITIVE_CONTENT,
                BootstrapRunner.resolveAiContentSensitivity(true),
                "logAiSensitive=true must resolve to LOG_SENSITIVE_CONTENT");
    }

    // =========================================================================
    // mapOutcomeToExitCode: log level semantics
    // =========================================================================

    /**
     * LOCK_UNAVAILABLE is an expected operational situation (another instance is already running)
     * and must be logged at WARN level, not ERROR. Verifies that the conditional branch
     * for {@link BatchRunOutcome#LOCK_UNAVAILABLE} is distinct from the generic failure branch.
     */
    @Test
    void mapOutcomeToExitCode_lockUnavailableLogsAtWarnLevel() throws Exception {
        ConfigurationPort mockConfigPort = new MockConfigurationPort(tempDir, true);
        List<LogEvent> capturedEvents = new ArrayList<>();
        String appenderName = "TestCapture-LockWarn-" + UUID.randomUUID();

        LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
        Configuration cfg = ctx.getConfiguration();
        AbstractAppender captureAppender = new AbstractAppender(
                appenderName, null, null, false, Property.EMPTY_ARRAY) {
            @Override
            public void append(LogEvent event) {
                capturedEvents.add(event.toImmutable());
            }
        };
        captureAppender.start();
        cfg.addAppender(captureAppender);
        cfg.getRootLogger().addAppender(captureAppender, Level.ALL, null);
        ctx.updateLoggers();

        BootstrapRunner runner = new BootstrapRunner(
                configPath -> mockConfigPort,
                lockFile -> new MockRunLockPort(),
                StartConfigurationValidator::new,
                jdbcUrl -> new MockSchemaInitializationPort(),
                (config, lock) -> context -> BatchRunOutcome.LOCK_UNAVAILABLE,
                SchedulerBatchCommand::new
        );
        try {
            runner.run();
        } finally {
            cfg.getRootLogger().removeAppender(appenderName);
            ctx.updateLoggers();
            captureAppender.stop();
        }

        assertTrue(
                capturedEvents.stream().anyMatch(e ->
                        e.getLevel() == Level.WARN
                        && e.getMessage().getFormattedMessage().contains("another instance")),
                "LOCK_UNAVAILABLE must produce a WARN-level log mentioning another instance is running");
        assertTrue(
                capturedEvents.stream().noneMatch(e ->
                        e.getLevel() == Level.ERROR
                        && e.getMessage().getFormattedMessage().contains("another instance")),
                "LOCK_UNAVAILABLE must not produce an ERROR-level log");
    }

    /**
     * A true batch FAILURE (infrastructure error) must be logged at ERROR level, not WARN.
     * Verifies the else branch of {@link BatchRunOutcome#FAILURE} uses the error log path.
     */
    @Test
    void mapOutcomeToExitCode_failureLogsAtErrorLevel() throws Exception {
        ConfigurationPort mockConfigPort = new MockConfigurationPort(tempDir, true);
        List<LogEvent> capturedEvents = new ArrayList<>();
        String appenderName = "TestCapture-Failure-" + UUID.randomUUID();

        LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
        Configuration cfg = ctx.getConfiguration();
        AbstractAppender captureAppender = new AbstractAppender(
                appenderName, null, null, false, Property.EMPTY_ARRAY) {
            @Override
            public void append(LogEvent event) {
                capturedEvents.add(event.toImmutable());
            }
        };
        captureAppender.start();
        cfg.addAppender(captureAppender);
        cfg.getRootLogger().addAppender(captureAppender, Level.ALL, null);
        ctx.updateLoggers();

        BootstrapRunner runner = new BootstrapRunner(
                configPath -> mockConfigPort,
                lockFile -> new MockRunLockPort(),
                StartConfigurationValidator::new,
                jdbcUrl -> new MockSchemaInitializationPort(),
                (config, lock) -> context -> BatchRunOutcome.FAILURE,
                SchedulerBatchCommand::new
        );
        try {
            runner.run();
        } finally {
            cfg.getRootLogger().removeAppender(appenderName);
            ctx.updateLoggers();
            captureAppender.stop();
        }

        assertTrue(
                capturedEvents.stream().anyMatch(e ->
                        e.getLevel() == Level.ERROR
                        && e.getMessage().getFormattedMessage().contains("failed")),
                "Batch FAILURE must produce an ERROR-level log mentioning the run failed");
    }

    // =========================================================================
    // executeWithStartConfiguration: run context lifecycle
    // =========================================================================

    /**
     * The end instant on the {@link BatchRunContext} must be set after the batch command returns.
     * A missing setEndInstant call would leave the context without a completion timestamp,
     * breaking run completion logging.
     */
    @Test
    void executeWithStartConfiguration_setsEndInstantOnBatchRunContext() throws Exception {
        AtomicReference<BatchRunContext> capturedContext = new AtomicReference<>();
        ConfigurationPort mockConfigPort = new MockConfigurationPort(tempDir, true);

        BootstrapRunner runner = new BootstrapRunner(
                configPath -> mockConfigPort,
                lockFile -> new MockRunLockPort(),
                StartConfigurationValidator::new,
                jdbcUrl -> new MockSchemaInitializationPort(),
                (config, lock) -> context -> {
                    capturedContext.set(context);
                    return BatchRunOutcome.SUCCESS;
                },
                SchedulerBatchCommand::new
        );

        runner.run();

        assertNotNull(capturedContext.get(), "BatchRunContext should have been passed to the use case");
        assertTrue(capturedContext.get().isCompleted(),
                "End instant must be set on BatchRunContext after the batch command completes");
    }

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

    private static MultiProviderConfiguration validMultiProviderConfig() {
        ProviderConfiguration openAiConfig = new ProviderConfiguration(
                "gpt-4", 30, "https://api.example.com", "test-api-key");
        return new MultiProviderConfiguration(AiProviderFamily.OPENAI_COMPATIBLE, openAiConfig, null);
    }

    // -------------------------------------------------------------------------
    // Mocks
    // -------------------------------------------------------------------------

    private static class MockConfigurationPort implements ConfigurationPort {
        private final Path tempDir;
        private final boolean shouldSucceed;

        MockConfigurationPort(Path tempDir, boolean shouldSucceed) {
            this.tempDir = tempDir;
            this.shouldSucceed = shouldSucceed;
        }

        @Override
        public StartConfiguration loadConfiguration() {
            if (!shouldSucceed) {
                throw new IllegalStateException("Mock configuration loading failed");
            }

            try {
                Path sourceFolder = Files.createDirectories(tempDir.resolve("source"));
                Path targetFolder = Files.createDirectories(tempDir.resolve("target"));
                Path sqliteFile = tempDir.resolve("db.sqlite");
                if (!Files.exists(sqliteFile)) {
                    Files.createFile(sqliteFile);
                }
                Path promptTemplateFile = tempDir.resolve("prompt.txt");
                if (!Files.exists(promptTemplateFile)) {
                    Files.createFile(promptTemplateFile);
                }

                ProviderConfiguration openAiConfig = new ProviderConfiguration(
                        "gpt-4", 30, "https://api.example.com", "test-api-key");
                MultiProviderConfiguration multiConfig = new MultiProviderConfiguration(
                        AiProviderFamily.OPENAI_COMPATIBLE, openAiConfig, null);

                return new StartConfiguration(
                        sourceFolder,
                        targetFolder,
                        sqliteFile,
                        multiConfig,
                        3,
                        100,
                        50000,
                        60,
                        promptTemplateFile,
                        tempDir.resolve("lock.lock"),
                        tempDir.resolve("logs"),
                        "INFO",
                        false
                );
            } catch (Exception e) {
                throw new RuntimeException("Failed to create mock configuration", e);
            }
        }
    }

    private static class MockRunBatchProcessingUseCase implements BatchRunProcessingUseCase {
        private final boolean shouldSucceed;

        MockRunBatchProcessingUseCase(boolean shouldSucceed) {
            this.shouldSucceed = shouldSucceed;
        }

        @Override
        public BatchRunOutcome execute(BatchRunContext context) {
            return shouldSucceed ? BatchRunOutcome.SUCCESS : BatchRunOutcome.FAILURE;
        }
    }

    private static class MockRunLockPort implements 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 PersistenceSchemaInitializationPort {
        @Override
        public void initializeSchema() {
            // Success by default
        }
    }
}
