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

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.io.IOException;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;

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

import de.gecheckt.pdf.umbenenner.application.port.out.ConfigurationFileLockException;
import de.gecheckt.pdf.umbenenner.application.port.out.SchedulerSettings;

/**
 * Unit-Tests für {@link FileChannelConfigurationAccessAdapter}.
 */
class FileChannelConfigurationAccessAdapterTest {

    @Test
    void isLocked_returnsFalseBeforeAnyAcquire(@TempDir Path tempDir) throws IOException {
        Path config = createConfigFile(tempDir, "");
        FileChannelConfigurationAccessAdapter adapter =
                new FileChannelConfigurationAccessAdapter(config);

        assertThat(adapter.isLocked()).isFalse();
    }

    @Test
    void acquireLock_setsIsLockedTrue(@TempDir Path tempDir) throws IOException {
        Path config = createConfigFile(tempDir, "");
        FileChannelConfigurationAccessAdapter adapter =
                new FileChannelConfigurationAccessAdapter(config);

        adapter.acquireLock();
        try {
            assertThat(adapter.isLocked()).isTrue();
        } finally {
            adapter.releaseLock();
        }
    }

    @Test
    void releaseLock_setsIsLockedFalse(@TempDir Path tempDir) throws IOException {
        Path config = createConfigFile(tempDir, "");
        FileChannelConfigurationAccessAdapter adapter =
                new FileChannelConfigurationAccessAdapter(config);

        adapter.acquireLock();
        adapter.releaseLock();

        assertThat(adapter.isLocked()).isFalse();
    }

    @Test
    void acquireLock_calledTwice_isIdempotent(@TempDir Path tempDir) throws IOException {
        Path config = createConfigFile(tempDir, "");
        FileChannelConfigurationAccessAdapter adapter =
                new FileChannelConfigurationAccessAdapter(config);

        adapter.acquireLock();
        try {
            assertThatCode(adapter::acquireLock).doesNotThrowAnyException();
            assertThat(adapter.isLocked()).isTrue();
        } finally {
            adapter.releaseLock();
        }
    }

    @Test
    void releaseLock_calledTwice_isIdempotent(@TempDir Path tempDir) throws IOException {
        Path config = createConfigFile(tempDir, "");
        FileChannelConfigurationAccessAdapter adapter =
                new FileChannelConfigurationAccessAdapter(config);

        adapter.acquireLock();
        adapter.releaseLock();

        assertThatCode(adapter::releaseLock).doesNotThrowAnyException();
        assertThat(adapter.isLocked()).isFalse();
    }

    @Test
    void releaseLock_withoutPriorAcquire_doesNotThrow(@TempDir Path tempDir) throws IOException {
        Path config = createConfigFile(tempDir, "");
        FileChannelConfigurationAccessAdapter adapter =
                new FileChannelConfigurationAccessAdapter(config);

        assertThatCode(adapter::releaseLock).doesNotThrowAnyException();
    }

    @Test
    void acquireLock_throwsConfigurationFileLockException_whenFileDoesNotExist(
            @TempDir Path tempDir) {
        Path nonExistent = tempDir.resolve("missing.properties");
        FileChannelConfigurationAccessAdapter adapter =
                new FileChannelConfigurationAccessAdapter(nonExistent);

        assertThatThrownBy(adapter::acquireLock)
                .isInstanceOf(ConfigurationFileLockException.class);
    }

    @Test
    void loadSettings_returnsDefaultsWhenKeysAreMissing(@TempDir Path tempDir) throws IOException {
        Path config = createConfigFile(tempDir, "source.folder=S:\\source\n");
        FileChannelConfigurationAccessAdapter adapter =
                new FileChannelConfigurationAccessAdapter(config);

        SchedulerSettings settings = adapter.loadSettings();

        assertThat(settings.intervalSeconds()).isEqualTo(SchedulerSettings.DEFAULT_INTERVAL_SECONDS);
    }

    @Test
    void loadSettings_returnsConfiguredValues(@TempDir Path tempDir) throws IOException {
        String content = "scheduler.interval.seconds=300\n";
        Path config = createConfigFile(tempDir, content);
        FileChannelConfigurationAccessAdapter adapter =
                new FileChannelConfigurationAccessAdapter(config);

        SchedulerSettings settings = adapter.loadSettings();

        assertThat(settings.intervalSeconds()).isEqualTo(300);
    }

    @Test
    void loadSettings_returnsDefaultIntervalForNonNumericValue(@TempDir Path tempDir)
            throws IOException {
        String content = "scheduler.interval.seconds=not-a-number\n";
        Path config = createConfigFile(tempDir, content);
        FileChannelConfigurationAccessAdapter adapter =
                new FileChannelConfigurationAccessAdapter(config);

        SchedulerSettings settings = adapter.loadSettings();

        assertThat(settings.intervalSeconds()).isEqualTo(SchedulerSettings.DEFAULT_INTERVAL_SECONDS);
    }

    @Test
    void loadSettings_returnsDefaultsWhenFileIsEmpty(@TempDir Path tempDir) throws IOException {
        Path config = createConfigFile(tempDir, "");
        FileChannelConfigurationAccessAdapter adapter =
                new FileChannelConfigurationAccessAdapter(config);

        SchedulerSettings settings = adapter.loadSettings();

        assertThat(settings).isEqualTo(SchedulerSettings.defaults());
    }

    @Test
    void saveIntervalSeconds_updatesExistingKeyAndPreservesOtherLines(@TempDir Path tempDir)
            throws IOException {
        String initial = "source.folder=/opt/source\nscheduler.interval.seconds=180\ntarget.folder=/opt/target\n";
        Path config = createConfigFile(tempDir, initial);
        FileChannelConfigurationAccessAdapter adapter =
                new FileChannelConfigurationAccessAdapter(config);

        adapter.saveIntervalSeconds(300);

        Properties props = loadProperties(config);
        assertThat(props.getProperty("scheduler.interval.seconds")).isEqualTo("300");
        assertThat(props.getProperty("source.folder")).isEqualTo("/opt/source");
        assertThat(props.getProperty("target.folder")).isEqualTo("/opt/target");
    }

    @Test
    void saveIntervalSeconds_appendsKeyWhenMissing(@TempDir Path tempDir) throws IOException {
        String initial = "source.folder=/opt/source\n";
        Path config = createConfigFile(tempDir, initial);
        FileChannelConfigurationAccessAdapter adapter =
                new FileChannelConfigurationAccessAdapter(config);

        adapter.saveIntervalSeconds(240);

        Properties props = loadProperties(config);
        assertThat(props.getProperty("scheduler.interval.seconds")).isEqualTo("240");
        assertThat(props.getProperty("source.folder")).isEqualTo("/opt/source");
    }

    @Test
    void saveIntervalSeconds_writesCorrectlyThroughChannelWhenLocked(@TempDir Path tempDir)
            throws IOException {
        String initial = "scheduler.interval.seconds=180\n";
        Path config = createConfigFile(tempDir, initial);
        FileChannelConfigurationAccessAdapter adapter =
                new FileChannelConfigurationAccessAdapter(config);

        adapter.acquireLock();
        try {
            adapter.saveIntervalSeconds(300);
        } finally {
            adapter.releaseLock();
        }

        Properties props = loadProperties(config);
        assertThat(props.getProperty("scheduler.interval.seconds")).isEqualTo("300");
    }

    @Test
    void saveIntervalSeconds_preservesCrlfLineEndings(@TempDir Path tempDir) throws IOException {
        String initial = "scheduler.interval.seconds=180\r\nother.key=value\r\n";
        Path config = createConfigFileBinary(tempDir, initial.getBytes(StandardCharsets.UTF_8));
        FileChannelConfigurationAccessAdapter adapter =
                new FileChannelConfigurationAccessAdapter(config);

        adapter.saveIntervalSeconds(300);

        byte[] resultBytes = Files.readAllBytes(config);
        String result = new String(resultBytes, StandardCharsets.UTF_8);
        assertThat(result).contains("scheduler.interval.seconds=300\r\n");
        assertThat(result).contains("other.key=value\r\n");
    }

    @Test
    void saveIntervalSeconds_preservesLfLineEndings(@TempDir Path tempDir) throws IOException {
        String initial = "scheduler.interval.seconds=180\nother.key=value\n";
        Path config = createConfigFile(tempDir, initial);
        FileChannelConfigurationAccessAdapter adapter =
                new FileChannelConfigurationAccessAdapter(config);

        adapter.saveIntervalSeconds(300);

        String result = Files.readString(config, StandardCharsets.UTF_8);
        assertThat(result).contains("scheduler.interval.seconds=300\n");
        assertThat(result).contains("other.key=value\n");
        assertThat(result).doesNotContain("\r\n");
    }

    private static Path createConfigFile(Path tempDir, String content) throws IOException {
        Path config = tempDir.resolve("test.properties");
        Files.writeString(config, content, StandardCharsets.UTF_8);
        return config;
    }

    private static Path createConfigFileBinary(Path tempDir, byte[] bytes) throws IOException {
        Path config = tempDir.resolve("test.properties");
        Files.write(config, bytes);
        return config;
    }

    private static Properties loadProperties(Path file) throws IOException {
        Properties props = new Properties();
        props.load(new StringReader(Files.readString(file, StandardCharsets.UTF_8)));
        return props;
    }
}
