package de.gecheckt.pdf.umbenenner.application.usecase;

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

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
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.application.port.in.CreateNewDatabaseUseCase.CreateNewDatabaseResult;
import de.gecheckt.pdf.umbenenner.application.port.out.ActiveDatabaseContextPort;
import de.gecheckt.pdf.umbenenner.application.port.out.DatabaseCreationPort;

/**
 * Unit-Tests für {@link DefaultCreateNewDatabaseUseCase}.
 * <p>
 * Prüft den Orchestrierungsablauf, die Pfad-Sicherheitsprüfung, das Aufräumen der
 * temporären Datei im Fehlerfall und das Umstellen der aktiven DB-Referenz im
 * Erfolgsfall. Die DB-Adapter werden über Stubs ersetzt, damit die Tests ohne
 * SQLite-/Flyway-Infrastruktur laufen.
 */
class DefaultCreateNewDatabaseUseCaseTest {

    @Test
    void constructor_shouldThrowNullPointerException_whenAnyPortIsNull() {
        DatabaseCreationPort creation = tempFile -> new DatabaseCreationPort.DatabaseCreationResult.Success(tempFile);
        ActiveDatabaseContextPort context = stubActiveContext();
        DefaultCreateNewDatabaseUseCase.ActiveDatabasePathSupplier supplier = () -> Path.of(".");

        assertThatThrownBy(() -> new DefaultCreateNewDatabaseUseCase(null, context, supplier))
                .isInstanceOf(NullPointerException.class)
                .hasMessageContaining("databaseCreationPort");
        assertThatThrownBy(() -> new DefaultCreateNewDatabaseUseCase(creation, null, supplier))
                .isInstanceOf(NullPointerException.class)
                .hasMessageContaining("activeDatabaseContextPort");
        assertThatThrownBy(() -> new DefaultCreateNewDatabaseUseCase(creation, context, null))
                .isInstanceOf(NullPointerException.class)
                .hasMessageContaining("activeDatabasePathSupplier");
    }

    @Test
    void createNewDatabase_shouldThrowNullPointerException_whenTargetIsNull() {
        DefaultCreateNewDatabaseUseCase useCase = new DefaultCreateNewDatabaseUseCase(
                tempFile -> new DatabaseCreationPort.DatabaseCreationResult.Success(tempFile),
                stubActiveContext(),
                () -> Path.of("active.sqlite"));

        assertThatThrownBy(() -> useCase.createNewDatabase(null))
                .isInstanceOf(NullPointerException.class);
    }

    @Test
    void createNewDatabase_shouldRejectSameAsActiveDatabase(@TempDir Path tempDir) throws IOException {
        Path active = tempDir.resolve("active.sqlite");
        Files.writeString(active, "stub");

        AtomicReference<Path> contextOverride = new AtomicReference<>();
        ActiveDatabaseContextPort context = trackingActiveContext(contextOverride);
        DefaultCreateNewDatabaseUseCase useCase = new DefaultCreateNewDatabaseUseCase(
                tempFile -> {
                    throw new AssertionError("DatabaseCreationPort darf bei selber Datei nicht aufgerufen werden");
                },
                context,
                active::toAbsolutePath);

        CreateNewDatabaseResult result = useCase.createNewDatabase(active);

        assertThat(result).isInstanceOf(CreateNewDatabaseResult.SameAsActiveDatabase.class);
        assertThat(contextOverride.get()).isNull();
    }

    @Test
    void createNewDatabase_shouldDeleteTempFileAndKeepActiveContext_whenAdapterReportsFailure(
            @TempDir Path tempDir) throws IOException {
        Path active = tempDir.resolve("active.sqlite");
        Files.writeString(active, "stub");
        Path target = tempDir.resolve("new.sqlite");

        AtomicReference<Path> capturedTemp = new AtomicReference<>();
        DatabaseCreationPort creation = tempFile -> {
            // Simuliere, dass der Adapter die Temp-Datei zwar anlegt, aber wegen Migration scheitert
            try {
                Files.writeString(tempFile, "x");
            } catch (IOException e) {
                throw new IllegalStateException(e);
            }
            capturedTemp.set(tempFile);
            return new DatabaseCreationPort.DatabaseCreationResult.Failure(
                    DatabaseCreationPort.DatabaseCreationResult.Phase.SCHEMA_MIGRATION,
                    "Migration fehlgeschlagen", null);
        };
        AtomicReference<Path> contextOverride = new AtomicReference<>();
        ActiveDatabaseContextPort context = trackingActiveContext(contextOverride);
        DefaultCreateNewDatabaseUseCase useCase = new DefaultCreateNewDatabaseUseCase(
                creation, context, active::toAbsolutePath);

        CreateNewDatabaseResult result = useCase.createNewDatabase(target);

        assertThat(result).isInstanceOf(CreateNewDatabaseResult.CreationFailed.class);
        CreateNewDatabaseResult.CreationFailed failed = (CreateNewDatabaseResult.CreationFailed) result;
        assertThat(failed.phase()).isEqualTo(CreateNewDatabaseResult.Phase.SCHEMA_MIGRATION);
        assertThat(capturedTemp.get()).isNotNull();
        assertThat(Files.exists(capturedTemp.get())).isFalse();
        assertThat(Files.exists(target)).isFalse();
        assertThat(contextOverride.get()).isNull();
    }

    @Test
    void createNewDatabase_shouldMoveTempToTargetAndSwitchContext_onSuccess(@TempDir Path tempDir)
            throws IOException {
        Path active = tempDir.resolve("active.sqlite");
        Files.writeString(active, "stub");
        Path target = tempDir.resolve("new.sqlite");

        AtomicReference<Path> capturedTemp = new AtomicReference<>();
        DatabaseCreationPort creation = tempFile -> {
            try {
                Files.writeString(tempFile, "migrated-content");
            } catch (IOException e) {
                throw new IllegalStateException(e);
            }
            capturedTemp.set(tempFile);
            return new DatabaseCreationPort.DatabaseCreationResult.Success(tempFile);
        };
        AtomicReference<Path> contextOverride = new AtomicReference<>();
        ActiveDatabaseContextPort context = trackingActiveContext(contextOverride);
        DefaultCreateNewDatabaseUseCase useCase = new DefaultCreateNewDatabaseUseCase(
                creation, context, active::toAbsolutePath);

        CreateNewDatabaseResult result = useCase.createNewDatabase(target);

        assertThat(result).isInstanceOf(CreateNewDatabaseResult.Success.class);
        assertThat(Files.exists(target)).isTrue();
        assertThat(Files.readString(target)).isEqualTo("migrated-content");
        assertThat(capturedTemp.get()).isNotNull();
        assertThat(Files.exists(capturedTemp.get())).isFalse();
        assertThat(contextOverride.get()).isEqualTo(target.toAbsolutePath().normalize());
    }

    @Test
    void createNewDatabase_shouldOverwriteExistingTargetFileAtomically(@TempDir Path tempDir) throws IOException {
        Path active = tempDir.resolve("active.sqlite");
        Files.writeString(active, "active");
        Path target = tempDir.resolve("existing.sqlite");
        Files.writeString(target, "alter-inhalt");

        DatabaseCreationPort creation = tempFile -> {
            try {
                Files.writeString(tempFile, "neu-und-leer");
            } catch (IOException e) {
                throw new IllegalStateException(e);
            }
            return new DatabaseCreationPort.DatabaseCreationResult.Success(tempFile);
        };
        AtomicReference<Path> contextOverride = new AtomicReference<>();
        DefaultCreateNewDatabaseUseCase useCase = new DefaultCreateNewDatabaseUseCase(
                creation, trackingActiveContext(contextOverride), active::toAbsolutePath);

        CreateNewDatabaseResult result = useCase.createNewDatabase(target);

        assertThat(result).isInstanceOf(CreateNewDatabaseResult.Success.class);
        assertThat(Files.readString(target)).isEqualTo("neu-und-leer");
        assertThat(contextOverride.get()).isEqualTo(target.toAbsolutePath().normalize());
    }

    @Test
    void createNewDatabase_shouldProceed_whenNoActiveDatabase(@TempDir Path tempDir) throws IOException {
        // Szenario: GUI ohne geladene Konfiguration und ohne Override → Supplier liefert null.
        // Es gibt keine aktive DB zu schützen; die Anlage muss durchlaufen.
        Path target = tempDir.resolve("neu.sqlite");

        AtomicReference<Path> capturedTemp = new AtomicReference<>();
        DatabaseCreationPort creation = tempFile -> {
            try {
                Files.writeString(tempFile, "migrated-content");
            } catch (IOException e) {
                throw new IllegalStateException(e);
            }
            capturedTemp.set(tempFile);
            return new DatabaseCreationPort.DatabaseCreationResult.Success(tempFile);
        };
        AtomicReference<Path> contextOverride = new AtomicReference<>();
        DefaultCreateNewDatabaseUseCase useCase = new DefaultCreateNewDatabaseUseCase(
                creation, trackingActiveContext(contextOverride), () -> null);

        CreateNewDatabaseResult result = useCase.createNewDatabase(target);

        assertThat(result).isInstanceOf(CreateNewDatabaseResult.Success.class);
        assertThat(Files.exists(target)).isTrue();
        assertThat(Files.readString(target)).isEqualTo("migrated-content");
        assertThat(capturedTemp.get()).isNotNull();
        assertThat(Files.exists(capturedTemp.get())).isFalse();
        assertThat(contextOverride.get()).isEqualTo(target.toAbsolutePath().normalize());
    }

    @Test
    void createNewDatabase_shouldOverwriteExistingTarget_whenNoActiveDatabase(@TempDir Path tempDir)
            throws IOException {
        // Kein aktiver DB-Kontext (Supplier liefert null), aber Zieldatei existiert bereits →
        // Überschreiben ist erlaubt, weil keine in Benutzung befindliche DB betroffen ist.
        Path target = tempDir.resolve("existing.sqlite");
        Files.writeString(target, "alter-inhalt");

        DatabaseCreationPort creation = tempFile -> {
            try {
                Files.writeString(tempFile, "neu-und-leer");
            } catch (IOException e) {
                throw new IllegalStateException(e);
            }
            return new DatabaseCreationPort.DatabaseCreationResult.Success(tempFile);
        };
        AtomicReference<Path> contextOverride = new AtomicReference<>();
        DefaultCreateNewDatabaseUseCase useCase = new DefaultCreateNewDatabaseUseCase(
                creation, trackingActiveContext(contextOverride), () -> null);

        CreateNewDatabaseResult result = useCase.createNewDatabase(target);

        assertThat(result).isInstanceOf(CreateNewDatabaseResult.Success.class);
        assertThat(Files.readString(target)).isEqualTo("neu-und-leer");
        assertThat(contextOverride.get()).isEqualTo(target.toAbsolutePath().normalize());
    }

    @Test
    void isSameFile_shouldReturnTrue_forIdenticalNonExistingFiles(@TempDir Path tempDir) throws IOException {
        Path a = tempDir.resolve("data.sqlite");
        Path b = tempDir.resolve("data.sqlite");
        assertThat(DefaultCreateNewDatabaseUseCase.isSameFile(a, b)).isTrue();
    }

    @Test
    void isSameFile_shouldReturnFalse_forDifferentFilesInSameDirectory(@TempDir Path tempDir) throws IOException {
        Path a = tempDir.resolve("a.sqlite");
        Path b = tempDir.resolve("b.sqlite");
        assertThat(DefaultCreateNewDatabaseUseCase.isSameFile(a, b)).isFalse();
    }

    private static ActiveDatabaseContextPort stubActiveContext() {
        return new ActiveDatabaseContextPort() {
            @Override
            public void switchActiveDatabase(Path newDbFile) { /* no-op */ }
            @Override
            public Optional<Path> activeDatabaseOverride() { return Optional.empty(); }
        };
    }

    /**
     * Aktive Context-Implementierung, die das übergebene Override in der gegebenen
     * {@link AtomicReference} festhält, damit Tests die Aufruf-Sequenz prüfen können.
     */
    private static ActiveDatabaseContextPort trackingActiveContext(AtomicReference<Path> sink) {
        return new ActiveDatabaseContextPort() {
            @Override
            public void switchActiveDatabase(Path newDbFile) {
                sink.set(newDbFile);
            }
            @Override
            public Optional<Path> activeDatabaseOverride() {
                return Optional.ofNullable(sink.get());
            }
        };
    }
}
