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

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

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.util.HexFormat;

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.port.out.ExistingIdenticalTargetFile;
import de.gecheckt.pdf.umbenenner.application.port.out.ResolvedTargetFilename;
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFilenameResolutionResult;
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFolderTechnicalFailure;
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;

/**
 * Tests for {@link FilesystemTargetFolderAdapter}.
 * <p>
 * Covers duplicate resolution (no conflict, single conflict, multiple conflicts),
 * suffix placement, rollback deletion, and error handling.
 */
class FilesystemTargetFolderAdapterTest {

    /** A fingerprint whose hex value differs from any real file content in tests. */
    private static final DocumentFingerprint DUMMY_FP =
            new DocumentFingerprint("0".repeat(64));

    @TempDir
    Path targetFolder;

    private FilesystemTargetFolderAdapter adapter;

    @BeforeEach
    void setUp() {
        adapter = new FilesystemTargetFolderAdapter(targetFolder);
    }

    // -------------------------------------------------------------------------
    // getTargetFolderLocator
    // -------------------------------------------------------------------------

    @Test
    void getTargetFolderLocator_returnsAbsolutePath() {
        String locator = adapter.getTargetFolderLocator();

        assertThat(locator).isEqualTo(targetFolder.toAbsolutePath().toString());
    }

    @Test
    void getTargetFolderLocator_isNeverNullOrBlank() {
        assertThat(adapter.getTargetFolderLocator()).isNotNull().isNotBlank();
    }

    // -------------------------------------------------------------------------
    // resolveUniqueFilename – no conflict
    // -------------------------------------------------------------------------

    @Test
    void resolveUniqueFilename_noConflict_returnsBaseName() {
        String baseName = "2026-01-15 - Rechnung.pdf";

        TargetFilenameResolutionResult result = adapter.resolveUniqueFilename(baseName, DUMMY_FP);

        assertThat(result).isInstanceOf(ResolvedTargetFilename.class);
        assertThat(((ResolvedTargetFilename) result).resolvedFilename()).isEqualTo(baseName);
    }

    // -------------------------------------------------------------------------
    // resolveUniqueFilename – collision with base name
    // -------------------------------------------------------------------------

    @Test
    void resolveUniqueFilename_baseNameTaken_returnsSuffixOne() throws IOException {
        String baseName = "2026-01-15 - Rechnung.pdf";
        Files.createFile(targetFolder.resolve(baseName));

        TargetFilenameResolutionResult result = adapter.resolveUniqueFilename(baseName, DUMMY_FP);

        assertThat(result).isInstanceOf(ResolvedTargetFilename.class);
        assertThat(((ResolvedTargetFilename) result).resolvedFilename())
                .isEqualTo("2026-01-15 - Rechnung(1).pdf");
    }

    @Test
    void resolveUniqueFilename_baseAndOneTaken_returnsSuffixTwo() throws IOException {
        String baseName = "2026-01-15 - Rechnung.pdf";
        Files.createFile(targetFolder.resolve(baseName));
        Files.createFile(targetFolder.resolve("2026-01-15 - Rechnung(1).pdf"));

        TargetFilenameResolutionResult result = adapter.resolveUniqueFilename(baseName, DUMMY_FP);

        assertThat(result).isInstanceOf(ResolvedTargetFilename.class);
        assertThat(((ResolvedTargetFilename) result).resolvedFilename())
                .isEqualTo("2026-01-15 - Rechnung(2).pdf");
    }

    @Test
    void resolveUniqueFilename_multipleTaken_returnsFirstFree() throws IOException {
        String baseName = "2026-03-31 - Stromabrechnung.pdf";
        // Create base + (1), (2), (3)
        Files.createFile(targetFolder.resolve(baseName));
        Files.createFile(targetFolder.resolve("2026-03-31 - Stromabrechnung(1).pdf"));
        Files.createFile(targetFolder.resolve("2026-03-31 - Stromabrechnung(2).pdf"));
        Files.createFile(targetFolder.resolve("2026-03-31 - Stromabrechnung(3).pdf"));

        TargetFilenameResolutionResult result = adapter.resolveUniqueFilename(baseName, DUMMY_FP);

        assertThat(result).isInstanceOf(ResolvedTargetFilename.class);
        assertThat(((ResolvedTargetFilename) result).resolvedFilename())
                .isEqualTo("2026-03-31 - Stromabrechnung(4).pdf");
    }

    // -------------------------------------------------------------------------
    // Suffix placement: immediately before .pdf
    // -------------------------------------------------------------------------

    @Test
    void resolveUniqueFilename_suffixPlacedImmediatelyBeforePdf() throws IOException {
        String baseName = "2026-04-07 - Bescheid.pdf";
        Files.createFile(targetFolder.resolve(baseName));

        TargetFilenameResolutionResult result = adapter.resolveUniqueFilename(baseName, DUMMY_FP);

        assertThat(result).isInstanceOf(ResolvedTargetFilename.class);
        String resolved = ((ResolvedTargetFilename) result).resolvedFilename();
        // Must end with "(1).pdf", not ".pdf(1)"
        assertThat(resolved).endsWith("(1).pdf");
        assertThat(resolved).doesNotContain(".pdf(");
    }

    // -------------------------------------------------------------------------
    // Suffix does not count against 20-char base title
    // -------------------------------------------------------------------------

    @Test
    void resolveUniqueFilename_20CharTitle_suffixDoesNotViolateTitleLimit() throws IOException {
        // Base title has exactly 20 chars; with (1) suffix the title exceeds 20, but that is expected
        String title = "A".repeat(20); // 20-char title
        String baseName = "2026-01-01 - " + title + ".pdf";
        Files.createFile(targetFolder.resolve(baseName));

        TargetFilenameResolutionResult result = adapter.resolveUniqueFilename(baseName, DUMMY_FP);

        assertThat(result).isInstanceOf(ResolvedTargetFilename.class);
        String resolved = ((ResolvedTargetFilename) result).resolvedFilename();
        // The resolved filename must contain (1) even though overall length > 20 chars
        assertThat(resolved).contains("(1)");
    }

    // -------------------------------------------------------------------------
    // resolveUniqueFilename – base name without .pdf extension
    // -------------------------------------------------------------------------

    @Test
    void resolveUniqueFilename_baseNameWithoutPdfExtension_whenConflict_returnsFailure()
            throws IOException {
        // When there is no conflict (file does not exist), the adapter returns the name as-is
        // because it only checks the extension when it needs to insert a suffix.
        String nameWithoutExt = "2026-01-15 - Rechnung";

        // Create a file with that name (no extension) to trigger conflict handling
        Files.createFile(targetFolder.resolve(nameWithoutExt));

        TargetFilenameResolutionResult result = adapter.resolveUniqueFilename(nameWithoutExt, DUMMY_FP);

        // Without .pdf extension, suffix insertion fails
        assertThat(result).isInstanceOf(TargetFolderTechnicalFailure.class);
    }

    // -------------------------------------------------------------------------
    // resolveUniqueFilename – no conflict, name without .pdf (edge: no conflict → ok)
    // -------------------------------------------------------------------------

    @Test
    void resolveUniqueFilename_baseNameWithoutPdfExtension_whenNoConflict_returnsIt() {
        // If the name does not exist, the adapter returns it without checking the extension
        String nameWithoutExt = "2026-01-15 - Rechnung";

        TargetFilenameResolutionResult result = adapter.resolveUniqueFilename(nameWithoutExt, DUMMY_FP);

        assertThat(result).isInstanceOf(ResolvedTargetFilename.class);
        assertThat(((ResolvedTargetFilename) result).resolvedFilename()).isEqualTo(nameWithoutExt);
    }

    // -------------------------------------------------------------------------
    // resolveUniqueFilename – null guard
    // -------------------------------------------------------------------------

    @Test
    void resolveUniqueFilename_rejectsNullBaseName() {
        assertThatNullPointerException()
                .isThrownBy(() -> adapter.resolveUniqueFilename(null, DUMMY_FP));
    }

    // -------------------------------------------------------------------------
    // tryDeleteTargetFile – file exists, gets deleted
    // -------------------------------------------------------------------------

    @Test
    void tryDeleteTargetFile_fileExists_deletesFile() throws IOException {
        String filename = "2026-01-15 - Rechnung.pdf";
        Files.createFile(targetFolder.resolve(filename));
        assertThat(targetFolder.resolve(filename)).exists();

        adapter.tryDeleteTargetFile(filename);

        assertThat(targetFolder.resolve(filename)).doesNotExist();
    }

    // -------------------------------------------------------------------------
    // tryDeleteTargetFile – file does not exist, no error
    // -------------------------------------------------------------------------

    @Test
    void tryDeleteTargetFile_fileDoesNotExist_doesNotThrow() {
        assertThatCode(() -> adapter.tryDeleteTargetFile("nonexistent.pdf"))
                .doesNotThrowAnyException();
    }

    // -------------------------------------------------------------------------
    // tryDeleteTargetFile – null guard
    // -------------------------------------------------------------------------

    @Test
    void tryDeleteTargetFile_rejectsNullFilename() {
        assertThatNullPointerException()
                .isThrownBy(() -> adapter.tryDeleteTargetFile(null));
    }

    // -------------------------------------------------------------------------
    // resolveUniqueFilename – non-existent target folder
    // -------------------------------------------------------------------------

    @Test
    void resolveUniqueFilename_nonExistentTargetFolder_returnsFailure() {
        Path nonExistentFolder = targetFolder.resolve("does-not-exist");
        FilesystemTargetFolderAdapter adapterWithMissingFolder =
                new FilesystemTargetFolderAdapter(nonExistentFolder);

        String baseName = "2026-01-01 - Test.pdf";

        // Files.exists() on a file in a non-existent folder does not throw;
        // it simply returns false, so the adapter returns the base name.
        // This is consistent behaviour: no folder access error when just checking existence.
        TargetFilenameResolutionResult result = adapterWithMissingFolder.resolveUniqueFilename(baseName, DUMMY_FP);

        // Adapter returns the base name since no conflict is detected for a non-existent folder
        assertThat(result).isInstanceOf(ResolvedTargetFilename.class);
        assertThat(((ResolvedTargetFilename) result).resolvedFilename()).isEqualTo(baseName);
    }

    // -------------------------------------------------------------------------
    // Construction – null guard
    // -------------------------------------------------------------------------

    @Test
    void constructor_rejectsNullTargetFolderPath() {
        assertThatNullPointerException()
                .isThrownBy(() -> new FilesystemTargetFolderAdapter(null));
    }

    // -------------------------------------------------------------------------
    // resolveUniqueFilename – identical-content shortcut
    // -------------------------------------------------------------------------

    @Test
    void resolveUniqueFilename_existingFileWithIdenticalContent_returnsExistingIdenticalTargetFile()
            throws Exception {
        // Arrange: write a file with known content and compute its SHA-256
        String baseName = "2026-01-15 - Identisch.pdf";
        byte[] content = "identical content for test".getBytes();
        Files.write(targetFolder.resolve(baseName), content);

        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        digest.update(content);
        String sha256Hex = HexFormat.of().formatHex(digest.digest());
        DocumentFingerprint matchingFp = new DocumentFingerprint(sha256Hex);

        // Act
        TargetFilenameResolutionResult result = adapter.resolveUniqueFilename(baseName, matchingFp);

        // Assert: shortcut path returns ExistingIdenticalTargetFile, not a new suffix
        assertThat(result).isInstanceOf(ExistingIdenticalTargetFile.class);
        assertThat(((ExistingIdenticalTargetFile) result).existingFilename()).isEqualTo(baseName);
    }

    @Test
    void resolveUniqueFilename_existingFileWithDifferentContent_returnsSuffixedFilename()
            throws IOException {
        // Arrange: existing file with some content; source fingerprint differs
        String baseName = "2026-01-15 - Verschieden.pdf";
        Files.write(targetFolder.resolve(baseName), "existing content".getBytes());

        // Use a fingerprint whose hex does not match the SHA-256 of the existing file
        DocumentFingerprint differentFp = new DocumentFingerprint("0".repeat(64));

        // Act
        TargetFilenameResolutionResult result = adapter.resolveUniqueFilename(baseName, differentFp);

        // Assert: different content → suffix appended
        assertThat(result).isInstanceOf(ResolvedTargetFilename.class);
        assertThat(((ResolvedTargetFilename) result).resolvedFilename())
                .isEqualTo("2026-01-15 - Verschieden(1).pdf");
    }
}
