package de.gecheckt.pdf.umbenenner.adapter.out.configuration;

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.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.FileWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.function.UnaryOperator;

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

import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;

/**
 * Unit tests for {@link PropertiesConfigurationPortAdapter}.
 * <p>
 * Tests cover valid configuration loading, missing mandatory properties,
 * invalid property values, and API-key environment variable precedence
 * for the multi-provider schema.
 */
class PropertiesConfigurationPortAdapterTest {

    private UnaryOperator<String> emptyEnvLookup;

    @TempDir
    Path tempDir;

    @BeforeEach
    void setUp() {
        emptyEnvLookup = key -> null;
    }

    @Test
    void loadConfiguration_successWithValidProperties() throws Exception {
        Path configFile = createConfigFile("valid-config.properties");

        PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);

        var config = adapter.loadConfiguration();

        assertNotNull(config);
        assertTrue(config.sourceFolder().toString().endsWith("source"));
        assertTrue(config.targetFolder().toString().endsWith("target"));
        assertTrue(config.sqliteFile().toString().endsWith("db.sqlite"));
        assertNotNull(config.multiProviderConfiguration());
        assertEquals(AiProviderFamily.OPENAI_COMPATIBLE,
                config.multiProviderConfiguration().activeProviderFamily());
        assertEquals("https://api.example.com",
                config.multiProviderConfiguration().activeProviderConfiguration().baseUrl());
        assertEquals("gpt-4",
                config.multiProviderConfiguration().activeProviderConfiguration().model());
        assertEquals(30,
                config.multiProviderConfiguration().activeProviderConfiguration().timeoutSeconds());
        assertEquals("test-api-key-from-properties",
                config.multiProviderConfiguration().activeProviderConfiguration().apiKey());
        assertEquals(3, config.maxRetriesTransient());
        assertEquals(100, config.maxPages());
        assertEquals(50000, config.maxTextCharacters());
        // Backward compatibility: missing max.title.length must use default 60
        assertEquals(60, config.maxTitleLength());
        assertTrue(config.promptTemplateFile().toString().endsWith("prompt.txt"));
        assertTrue(config.runtimeLockFile().toString().endsWith("lock.lock"));
        assertTrue(config.logDirectory().toString().endsWith("logs"));
        assertEquals("DEBUG", config.logLevel());
    }

    @Test
    void loadConfiguration_rejectsBlankApiKeyWhenAbsentAndNoEnvVar() throws Exception {
        Path configFile = createConfigFile("no-api-key.properties");

        PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);

        assertThrows(
                de.gecheckt.pdf.umbenenner.adapter.out.bootstrap.validation.InvalidStartConfigurationException.class,
                adapter::loadConfiguration,
                "Missing API key must be rejected as invalid configuration");
    }

    @Test
    void loadConfiguration_rejectsBlankApiKeyWhenEnvVarIsNull() throws Exception {
        Path configFile = createConfigFile("no-api-key.properties");

        UnaryOperator<String> envLookup = key -> null;

        PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(envLookup, configFile);

        assertThrows(
                de.gecheckt.pdf.umbenenner.adapter.out.bootstrap.validation.InvalidStartConfigurationException.class,
                adapter::loadConfiguration,
                "Null env var with no properties API key must be rejected as invalid configuration");
    }

    @Test
    void loadConfiguration_rejectsBlankApiKeyWhenEnvVarIsEmpty() throws Exception {
        Path configFile = createConfigFile("no-api-key.properties");

        UnaryOperator<String> envLookup = key -> "";

        PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(envLookup, configFile);

        assertThrows(
                de.gecheckt.pdf.umbenenner.adapter.out.bootstrap.validation.InvalidStartConfigurationException.class,
                adapter::loadConfiguration,
                "Empty env var with no properties API key must be rejected as invalid configuration");
    }

    @Test
    void loadConfiguration_rejectsBlankApiKeyWhenEnvVarIsBlank() throws Exception {
        Path configFile = createConfigFile("no-api-key.properties");

        UnaryOperator<String> envLookup = key -> "   ";

        PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(envLookup, configFile);

        assertThrows(
                de.gecheckt.pdf.umbenenner.adapter.out.bootstrap.validation.InvalidStartConfigurationException.class,
                adapter::loadConfiguration,
                "Blank env var with no properties API key must be rejected as invalid configuration");
    }

    @Test
    void loadConfiguration_envVarOverridesPropertiesApiKey() throws Exception {
        Path configFile = createConfigFile("valid-config.properties");

        UnaryOperator<String> envLookup = key -> {
            if (MultiProviderConfigurationParser.ENV_OPENAI_API_KEY.equals(key)) {
                return "env-api-key-override";
            }
            return null;
        };

        PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(envLookup, configFile);

        var config = adapter.loadConfiguration();

        assertEquals("env-api-key-override",
                config.multiProviderConfiguration().activeProviderConfiguration().apiKey(),
                "Environment variable must override properties API key");
    }

    @Test
    void loadConfiguration_throwsConfigurationLoadingExceptionWhenRequiredPropertyMissing() throws Exception {
        Path configFile = createConfigFile("missing-required.properties");

        PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);

        ConfigurationLoadingException exception = assertThrows(
                ConfigurationLoadingException.class,
                () -> adapter.loadConfiguration()
        );

        assertTrue(exception.getMessage().contains("Required property missing"));
        assertTrue(exception.getMessage().contains("sqlite.file"));
    }

    @Test
    void loadConfiguration_throwsConfigurationLoadingExceptionWhenConfigFileNotFound() {
        Path nonExistentFile = tempDir.resolve("nonexistent.properties");

        PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, nonExistentFile);

        ConfigurationLoadingException exception = assertThrows(
                ConfigurationLoadingException.class,
                () -> adapter.loadConfiguration()
        );

        assertTrue(exception.getMessage().contains("Failed to load configuration"));
        assertTrue(exception.getCause() instanceof java.io.FileNotFoundException);
    }

    @Test
    void loadConfiguration_parsesIntegerValuesCorrectly() throws Exception {
        Path configFile = createInlineConfig(
                "source.folder=/tmp/source\n" +
                "target.folder=/tmp/target\n" +
                "sqlite.file=/tmp/db.sqlite\n" +
                "ai.provider.active=openai-compatible\n" +
                "ai.provider.openai-compatible.baseUrl=https://api.example.com\n" +
                "ai.provider.openai-compatible.model=gpt-4\n" +
                "ai.provider.openai-compatible.timeoutSeconds=60\n" +
                "ai.provider.openai-compatible.apiKey=test-key\n" +
                "max.retries.transient=5\n" +
                "max.pages=200\n" +
                "max.text.characters=100000\n" +
                "prompt.template.file=/tmp/prompt.txt\n"
        );

        PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);

        var config = adapter.loadConfiguration();

        assertEquals(60, config.multiProviderConfiguration().activeProviderConfiguration().timeoutSeconds());
        assertEquals(5, config.maxRetriesTransient());
        assertEquals(200, config.maxPages());
        assertEquals(100000, config.maxTextCharacters());
    }

    @Test
    void loadConfiguration_handlesWhitespaceInIntegerValues() throws Exception {
        Path configFile = createInlineConfig(
                "source.folder=/tmp/source\n" +
                "target.folder=/tmp/target\n" +
                "sqlite.file=/tmp/db.sqlite\n" +
                "ai.provider.active=openai-compatible\n" +
                "ai.provider.openai-compatible.baseUrl=https://api.example.com\n" +
                "ai.provider.openai-compatible.model=gpt-4\n" +
                "ai.provider.openai-compatible.timeoutSeconds=  45  \n" +
                "ai.provider.openai-compatible.apiKey=test-key\n" +
                "max.retries.transient=2\n" +
                "max.pages=150\n" +
                "max.text.characters=75000\n" +
                "prompt.template.file=/tmp/prompt.txt\n"
        );

        PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);

        var config = adapter.loadConfiguration();

        assertEquals(45,
                config.multiProviderConfiguration().activeProviderConfiguration().timeoutSeconds(),
                "Whitespace should be trimmed from integer values");
    }

    @Test
    void loadConfiguration_throwsConfigurationLoadingExceptionForInvalidIntegerValue() throws Exception {
        Path configFile = createInlineConfig(
                "source.folder=/tmp/source\n" +
                "target.folder=/tmp/target\n" +
                "sqlite.file=/tmp/db.sqlite\n" +
                "ai.provider.active=openai-compatible\n" +
                "ai.provider.openai-compatible.baseUrl=https://api.example.com\n" +
                "ai.provider.openai-compatible.model=gpt-4\n" +
                "ai.provider.openai-compatible.timeoutSeconds=not-a-number\n" +
                "ai.provider.openai-compatible.apiKey=test-key\n" +
                "max.retries.transient=2\n" +
                "max.pages=150\n" +
                "max.text.characters=75000\n" +
                "prompt.template.file=/tmp/prompt.txt\n"
        );

        PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);

        ConfigurationLoadingException exception = assertThrows(
                ConfigurationLoadingException.class,
                () -> adapter.loadConfiguration()
        );

        assertTrue(exception.getMessage().contains("Invalid integer value"));
    }

    @Test
    void loadConfiguration_parsesBaseUrlStringCorrectly() throws Exception {
        Path configFile = createInlineConfig(
                "source.folder=/tmp/source\n" +
                "target.folder=/tmp/target\n" +
                "sqlite.file=/tmp/db.sqlite\n" +
                "ai.provider.active=openai-compatible\n" +
                "ai.provider.openai-compatible.baseUrl=https://api.example.com:8080/v1\n" +
                "ai.provider.openai-compatible.model=gpt-4\n" +
                "ai.provider.openai-compatible.timeoutSeconds=30\n" +
                "ai.provider.openai-compatible.apiKey=test-key\n" +
                "max.retries.transient=3\n" +
                "max.pages=100\n" +
                "max.text.characters=50000\n" +
                "prompt.template.file=/tmp/prompt.txt\n"
        );

        PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);

        var config = adapter.loadConfiguration();

        assertEquals("https://api.example.com:8080/v1",
                config.multiProviderConfiguration().activeProviderConfiguration().baseUrl());
    }

    @Test
    void loadConfiguration_defaultsOptionalValuesWhenNotPresent() throws Exception {
        Path configFile = createInlineConfig(
                "source.folder=/tmp/source\n" +
                "target.folder=/tmp/target\n" +
                "sqlite.file=/tmp/db.sqlite\n" +
                "ai.provider.active=openai-compatible\n" +
                "ai.provider.openai-compatible.baseUrl=https://api.example.com\n" +
                "ai.provider.openai-compatible.model=gpt-4\n" +
                "ai.provider.openai-compatible.timeoutSeconds=30\n" +
                "ai.provider.openai-compatible.apiKey=test-key\n" +
                "max.retries.transient=3\n" +
                "max.pages=100\n" +
                "max.text.characters=50000\n" +
                "prompt.template.file=/tmp/prompt.txt\n"
        );

        PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);

        var config = adapter.loadConfiguration();

        assertEquals("", config.runtimeLockFile().toString(), "runtime.lock.file should default to empty");
        assertEquals("", config.logDirectory().toString(), "log.directory should default to empty");
        assertEquals("INFO", config.logLevel(), "log.level should default to INFO");
    }

    @Test
    void allConfigurationFailuresAreClassifiedAsConfigurationLoadingException() throws Exception {
        // File I/O failure
        Path nonExistentFile = tempDir.resolve("nonexistent.properties");
        PropertiesConfigurationPortAdapter adapter1 = new PropertiesConfigurationPortAdapter(emptyEnvLookup, nonExistentFile);
        assertThrows(ConfigurationLoadingException.class, () -> adapter1.loadConfiguration(),
                "File I/O failure should throw ConfigurationLoadingException");

        // Missing required property
        Path missingPropFile = createConfigFile("missing-required.properties");
        PropertiesConfigurationPortAdapter adapter2 = new PropertiesConfigurationPortAdapter(emptyEnvLookup, missingPropFile);
        assertThrows(ConfigurationLoadingException.class, () -> adapter2.loadConfiguration(),
                "Missing required property should throw ConfigurationLoadingException");

        // Invalid integer value
        Path invalidIntFile = createInlineConfig(
                "source.folder=/tmp/source\n" +
                "target.folder=/tmp/target\n" +
                "sqlite.file=/tmp/db.sqlite\n" +
                "ai.provider.active=openai-compatible\n" +
                "ai.provider.openai-compatible.baseUrl=https://api.example.com\n" +
                "ai.provider.openai-compatible.model=gpt-4\n" +
                "ai.provider.openai-compatible.timeoutSeconds=invalid\n" +
                "ai.provider.openai-compatible.apiKey=key\n" +
                "max.retries.transient=2\n" +
                "max.pages=100\n" +
                "max.text.characters=50000\n" +
                "prompt.template.file=/tmp/prompt.txt\n"
        );
        PropertiesConfigurationPortAdapter adapter3 = new PropertiesConfigurationPortAdapter(emptyEnvLookup, invalidIntFile);
        assertThrows(ConfigurationLoadingException.class, () -> adapter3.loadConfiguration(),
                "Invalid integer value should throw ConfigurationLoadingException");

        // Unknown ai.provider.active value
        Path unknownProviderFile = createInlineConfig(
                "source.folder=/tmp/source\n" +
                "target.folder=/tmp/target\n" +
                "sqlite.file=/tmp/db.sqlite\n" +
                "ai.provider.active=unknown-provider\n" +
                "max.retries.transient=2\n" +
                "max.pages=100\n" +
                "max.text.characters=50000\n" +
                "prompt.template.file=/tmp/prompt.txt\n"
        );
        PropertiesConfigurationPortAdapter adapter4 = new PropertiesConfigurationPortAdapter(emptyEnvLookup, unknownProviderFile);
        assertThrows(ConfigurationLoadingException.class, () -> adapter4.loadConfiguration(),
                "Unknown provider identifier should throw ConfigurationLoadingException");
    }

    @Test
    void loadConfiguration_logAiSensitiveDefaultsFalseWhenAbsent() throws Exception {
        Path configFile = createInlineConfig(
                "source.folder=/tmp/source\n" +
                "target.folder=/tmp/target\n" +
                "sqlite.file=/tmp/db.sqlite\n" +
                "ai.provider.active=openai-compatible\n" +
                "ai.provider.openai-compatible.baseUrl=https://api.example.com\n" +
                "ai.provider.openai-compatible.model=gpt-4\n" +
                "ai.provider.openai-compatible.timeoutSeconds=30\n" +
                "ai.provider.openai-compatible.apiKey=test-key\n" +
                "max.retries.transient=3\n" +
                "max.pages=100\n" +
                "max.text.characters=50000\n" +
                "prompt.template.file=/tmp/prompt.txt\n"
                // log.ai.sensitive intentionally omitted
        );

        PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);

        var config = adapter.loadConfiguration();

        assertFalse(config.logAiSensitive(),
                "log.ai.sensitive must default to false when the property is absent");
    }

    @Test
    void loadConfiguration_logAiSensitiveParsedTrueWhenExplicitlySet() throws Exception {
        Path configFile = createInlineConfig(
                "source.folder=/tmp/source\n" +
                "target.folder=/tmp/target\n" +
                "sqlite.file=/tmp/db.sqlite\n" +
                "ai.provider.active=openai-compatible\n" +
                "ai.provider.openai-compatible.baseUrl=https://api.example.com\n" +
                "ai.provider.openai-compatible.model=gpt-4\n" +
                "ai.provider.openai-compatible.timeoutSeconds=30\n" +
                "ai.provider.openai-compatible.apiKey=test-key\n" +
                "max.retries.transient=3\n" +
                "max.pages=100\n" +
                "max.text.characters=50000\n" +
                "prompt.template.file=/tmp/prompt.txt\n" +
                "log.ai.sensitive=true\n"
        );

        PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);

        var config = adapter.loadConfiguration();

        assertTrue(config.logAiSensitive(),
                "log.ai.sensitive must be parsed as true when explicitly set to 'true'");
    }

    @Test
    void loadConfiguration_logAiSensitiveParsedFalseWhenExplicitlySet() throws Exception {
        Path configFile = createInlineConfig(
                "source.folder=/tmp/source\n" +
                "target.folder=/tmp/target\n" +
                "sqlite.file=/tmp/db.sqlite\n" +
                "ai.provider.active=openai-compatible\n" +
                "ai.provider.openai-compatible.baseUrl=https://api.example.com\n" +
                "ai.provider.openai-compatible.model=gpt-4\n" +
                "ai.provider.openai-compatible.timeoutSeconds=30\n" +
                "ai.provider.openai-compatible.apiKey=test-key\n" +
                "max.retries.transient=3\n" +
                "max.pages=100\n" +
                "max.text.characters=50000\n" +
                "prompt.template.file=/tmp/prompt.txt\n" +
                "log.ai.sensitive=false\n"
        );

        PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);

        var config = adapter.loadConfiguration();

        assertFalse(config.logAiSensitive(),
                "log.ai.sensitive must be parsed as false when explicitly set to 'false'");
    }

    @Test
    void loadConfiguration_logAiSensitiveHandlesCaseInsensitiveTrue() throws Exception {
        Path configFile = createInlineConfig(
                "source.folder=/tmp/source\n" +
                "target.folder=/tmp/target\n" +
                "sqlite.file=/tmp/db.sqlite\n" +
                "ai.provider.active=openai-compatible\n" +
                "ai.provider.openai-compatible.baseUrl=https://api.example.com\n" +
                "ai.provider.openai-compatible.model=gpt-4\n" +
                "ai.provider.openai-compatible.timeoutSeconds=30\n" +
                "ai.provider.openai-compatible.apiKey=test-key\n" +
                "max.retries.transient=3\n" +
                "max.pages=100\n" +
                "max.text.characters=50000\n" +
                "prompt.template.file=/tmp/prompt.txt\n" +
                "log.ai.sensitive=TRUE\n"
        );

        PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);

        var config = adapter.loadConfiguration();

        assertTrue(config.logAiSensitive(),
                "log.ai.sensitive must handle case-insensitive 'TRUE'");
    }

    @Test
    void loadConfiguration_logAiSensitiveHandlesCaseInsensitiveFalse() throws Exception {
        Path configFile = createInlineConfig(
                "source.folder=/tmp/source\n" +
                "target.folder=/tmp/target\n" +
                "sqlite.file=/tmp/db.sqlite\n" +
                "ai.provider.active=openai-compatible\n" +
                "ai.provider.openai-compatible.baseUrl=https://api.example.com\n" +
                "ai.provider.openai-compatible.model=gpt-4\n" +
                "ai.provider.openai-compatible.timeoutSeconds=30\n" +
                "ai.provider.openai-compatible.apiKey=test-key\n" +
                "max.retries.transient=3\n" +
                "max.pages=100\n" +
                "max.text.characters=50000\n" +
                "prompt.template.file=/tmp/prompt.txt\n" +
                "log.ai.sensitive=FALSE\n"
        );

        PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);

        var config = adapter.loadConfiguration();

        assertFalse(config.logAiSensitive(),
                "log.ai.sensitive must handle case-insensitive 'FALSE'");
    }

    @Test
    void loadConfiguration_throwsConfigurationLoadingExceptionForInvalidLogAiSensitive() throws Exception {
        Path configFile = createInlineConfig(
                "source.folder=/tmp/source\n" +
                "target.folder=/tmp/target\n" +
                "sqlite.file=/tmp/db.sqlite\n" +
                "ai.provider.active=openai-compatible\n" +
                "ai.provider.openai-compatible.baseUrl=https://api.example.com\n" +
                "ai.provider.openai-compatible.model=gpt-4\n" +
                "ai.provider.openai-compatible.timeoutSeconds=30\n" +
                "ai.provider.openai-compatible.apiKey=test-key\n" +
                "max.retries.transient=3\n" +
                "max.pages=100\n" +
                "max.text.characters=50000\n" +
                "prompt.template.file=/tmp/prompt.txt\n" +
                "log.ai.sensitive=maybe\n"
        );

        PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);

        ConfigurationLoadingException exception = assertThrows(
                ConfigurationLoadingException.class,
                () -> adapter.loadConfiguration()
        );

        assertTrue(exception.getMessage().contains("Invalid value for log.ai.sensitive"),
                "Invalid log.ai.sensitive value should throw ConfigurationLoadingException");
        assertTrue(exception.getMessage().contains("'maybe'"),
                "Error message should include the invalid value");
    }

    @Test
    void loadConfiguration_throwsConfigurationLoadingExceptionForInvalidLogAiSensitiveYes() throws Exception {
        Path configFile = createInlineConfig(
                "source.folder=/tmp/source\n" +
                "target.folder=/tmp/target\n" +
                "sqlite.file=/tmp/db.sqlite\n" +
                "ai.provider.active=openai-compatible\n" +
                "ai.provider.openai-compatible.baseUrl=https://api.example.com\n" +
                "ai.provider.openai-compatible.model=gpt-4\n" +
                "ai.provider.openai-compatible.timeoutSeconds=30\n" +
                "ai.provider.openai-compatible.apiKey=test-key\n" +
                "max.retries.transient=3\n" +
                "max.pages=100\n" +
                "max.text.characters=50000\n" +
                "prompt.template.file=/tmp/prompt.txt\n" +
                "log.ai.sensitive=yes\n"
        );

        PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);

        ConfigurationLoadingException exception = assertThrows(
                ConfigurationLoadingException.class,
                () -> adapter.loadConfiguration()
        );

        assertTrue(exception.getMessage().contains("Invalid value for log.ai.sensitive"),
                "Invalid log.ai.sensitive value 'yes' should throw ConfigurationLoadingException");
    }

    @Test
    void loadConfiguration_throwsConfigurationLoadingExceptionForInvalidLogAiSensitive1() throws Exception {
        Path configFile = createInlineConfig(
                "source.folder=/tmp/source\n" +
                "target.folder=/tmp/target\n" +
                "sqlite.file=/tmp/db.sqlite\n" +
                "ai.provider.active=openai-compatible\n" +
                "ai.provider.openai-compatible.baseUrl=https://api.example.com\n" +
                "ai.provider.openai-compatible.model=gpt-4\n" +
                "ai.provider.openai-compatible.timeoutSeconds=30\n" +
                "ai.provider.openai-compatible.apiKey=test-key\n" +
                "max.retries.transient=3\n" +
                "max.pages=100\n" +
                "max.text.characters=50000\n" +
                "prompt.template.file=/tmp/prompt.txt\n" +
                "log.ai.sensitive=1\n"
        );

        PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);

        ConfigurationLoadingException exception = assertThrows(
                ConfigurationLoadingException.class,
                () -> adapter.loadConfiguration()
        );

        assertTrue(exception.getMessage().contains("Invalid value for log.ai.sensitive"),
                "Invalid log.ai.sensitive value '1' should throw ConfigurationLoadingException");
    }

    private Path createConfigFile(String resourceName) throws Exception {
        Path sourceResource = Path.of("src/test/resources", resourceName);
        Path targetConfigFile = tempDir.resolve("application.properties");

        Files.copy(sourceResource, targetConfigFile);
        return targetConfigFile;
    }

    private Path createInlineConfig(String content) throws Exception {
        Path configFile = tempDir.resolve("config.properties");
        try (FileWriter writer = new FileWriter(configFile.toFile())) {
            writer.write(content);
        }
        return configFile;
    }

    // =========================================================================
    // max.title.length parsing
    // =========================================================================

    @Test
    void loadConfiguration_maxTitleLengthExplicit_readsValue() throws Exception {
        String content = """
                source.folder=/tmp/source
                target.folder=/tmp/target
                sqlite.file=/tmp/db.sqlite
                ai.provider.active=openai-compatible
                ai.provider.openai-compatible.baseUrl=https://api.example.com
                ai.provider.openai-compatible.model=gpt-4
                ai.provider.openai-compatible.timeoutSeconds=30
                ai.provider.openai-compatible.apiKey=test-key
                max.retries.transient=3
                max.pages=100
                max.text.characters=50000
                max.title.length=80
                prompt.template.file=/tmp/prompt.txt
                """;
        Path configFile = createInlineConfig(content);

        PropertiesConfigurationPortAdapter adapter =
                new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);
        var config = adapter.loadConfiguration();

        assertEquals(80, config.maxTitleLength());
    }

    @Test
    void loadConfiguration_maxTitleLengthBlank_usesDefault() throws Exception {
        String content = """
                source.folder=/tmp/source
                target.folder=/tmp/target
                sqlite.file=/tmp/db.sqlite
                ai.provider.active=openai-compatible
                ai.provider.openai-compatible.baseUrl=https://api.example.com
                ai.provider.openai-compatible.model=gpt-4
                ai.provider.openai-compatible.timeoutSeconds=30
                ai.provider.openai-compatible.apiKey=test-key
                max.retries.transient=3
                max.pages=100
                max.text.characters=50000
                max.title.length=
                prompt.template.file=/tmp/prompt.txt
                """;
        Path configFile = createInlineConfig(content);

        PropertiesConfigurationPortAdapter adapter =
                new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);
        var config = adapter.loadConfiguration();

        assertEquals(60, config.maxTitleLength(),
                "Blank max.title.length must fall back to the default 60 (backward compatibility)");
    }

    @Test
    void loadConfiguration_maxTitleLengthNonInteger_throwsConfigurationLoadingException() throws Exception {
        String content = """
                source.folder=/tmp/source
                target.folder=/tmp/target
                sqlite.file=/tmp/db.sqlite
                ai.provider.active=openai-compatible
                ai.provider.openai-compatible.baseUrl=https://api.example.com
                ai.provider.openai-compatible.model=gpt-4
                ai.provider.openai-compatible.timeoutSeconds=30
                ai.provider.openai-compatible.apiKey=test-key
                max.retries.transient=3
                max.pages=100
                max.text.characters=50000
                max.title.length=abc
                prompt.template.file=/tmp/prompt.txt
                """;
        Path configFile = createInlineConfig(content);

        PropertiesConfigurationPortAdapter adapter =
                new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);

        assertThrows(ConfigurationLoadingException.class, adapter::loadConfiguration);
    }
}
