package p.packer.repositories;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import p.packer.models.*;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.security.MessageDigest;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static org.junit.jupiter.api.Assertions.*;

final class PackerAbstractBankWalkerTest {
    private static final ObjectMapper MAPPER = new ObjectMapper();

    @TempDir
    Path tempDir;

    @Test
    void invalidatesReuseImmediatelyWhenSizeChanges() throws Exception {
        final var walker = new CountingWalker();
        final var assetRoot = Files.createDirectories(tempDir.resolve("asset"));
        final var filePath = writeText(assetRoot.resolve("entry.txt"), "hello world");
        final long lastModified = Files.getLastModifiedTime(filePath).toMillis();
        final var priorCache = new PackerAssetCacheEntry(1, "contract", List.of(
                new PackerFileCacheEntry("entry.txt", "text/plain", 1L, lastModified, "cached", Map.of("source", "cache"), List.of())));

        final var result = walker.walk(assetRoot, "requirements", java.util.Optional.of(priorCache));

        assertEquals(1, walker.processCount());
        assertEquals(0, walker.fingerprintCount());
        assertEquals(Boolean.TRUE, result.probeResults().getFirst().metadata().get("processed"));
    }

    @Test
    void invalidatesReuseImmediatelyWhenCurrentLastModifiedIsNewer() throws Exception {
        final var walker = new CountingWalker();
        final var assetRoot = Files.createDirectories(tempDir.resolve("asset"));
        final var filePath = writeText(assetRoot.resolve("entry.txt"), "hello world");
        final long currentLastModified = Files.getLastModifiedTime(filePath).toMillis();
        final var priorCache = new PackerAssetCacheEntry(1, "contract", List.of(
                new PackerFileCacheEntry(
                        "entry.txt",
                        "text/plain",
                        Files.size(filePath),
                        currentLastModified - 1L,
                        "cached",
                        Map.of("source", "cache"),
                        List.of())));

        final var result = walker.walk(assetRoot, "requirements", java.util.Optional.of(priorCache));

        assertEquals(1, walker.processCount());
        assertEquals(0, walker.fingerprintCount());
        assertEquals(Boolean.TRUE, result.probeResults().getFirst().metadata().get("processed"));
    }

    @Test
    void evaluatesFingerprintOnlyAfterCheaperChecksRemainValid() throws Exception {
        final var walker = new CountingWalker();
        final var assetRoot = Files.createDirectories(tempDir.resolve("asset"));
        final var filePath = writeText(assetRoot.resolve("entry.txt"), "hello world");
        final long lastModified = Files.getLastModifiedTime(filePath).toMillis();
        final String fingerprint = sha256(Files.readAllBytes(filePath));
        final var priorCache = new PackerAssetCacheEntry(1, "contract", List.of(
                new PackerFileCacheEntry(
                        "entry.txt",
                        "text/plain",
                        Files.size(filePath),
                        lastModified,
                        fingerprint,
                        Map.of("source", "cache"),
                        List.of(new PackerDiagnostic(
                                p.packer.messages.diagnostics.PackerDiagnosticSeverity.WARNING,
                                p.packer.messages.diagnostics.PackerDiagnosticCategory.HYGIENE,
                                "cached file-scoped diagnostic",
                                filePath,
                                false)))));

        final var result = walker.walk(assetRoot, "requirements", java.util.Optional.of(priorCache));

        assertEquals(0, walker.processCount());
        assertEquals(1, walker.fingerprintCount());
        assertEquals("cache", result.probeResults().getFirst().metadata().get("source"));
        assertEquals(1, result.probeResults().getFirst().diagnostics().size());
        assertTrue(result.probeResults().getFirst().diagnostics().getFirst().message().contains("cached"));
    }

    @Test
    void reusesStableMetadataWithoutFingerprintWhenCheaperChecksStayValid() throws Exception {
        final var walker = new CountingWalker();
        final var assetRoot = Files.createDirectories(tempDir.resolve("asset"));
        final var filePath = writeText(assetRoot.resolve("entry.txt"), "hello world");
        final long lastModified = Files.getLastModifiedTime(filePath).toMillis();
        final var priorCache = new PackerAssetCacheEntry(1, "contract", List.of(
                new PackerFileCacheEntry(
                        "entry.txt",
                        "text/plain",
                        Files.size(filePath),
                        lastModified,
                        null,
                        Map.of("source", "cache"),
                        List.of())));

        final var result = walker.walk(assetRoot, "requirements", java.util.Optional.of(priorCache));

        assertEquals(0, walker.processCount());
        assertEquals(0, walker.fingerprintCount());
        assertEquals("cache", result.probeResults().getFirst().metadata().get("source"));
    }

    @Test
    void preservesFileScopedDiagnosticsInFreshWalkOutput() throws Exception {
        final var walker = new CountingWalker();
        final var assetRoot = Files.createDirectories(tempDir.resolve("asset"));
        writeText(assetRoot.resolve("entry.txt"), "hello world");

        final var result = walker.walk(assetRoot, "requirements");

        assertEquals(1, result.probeResults().size());
        assertEquals(1, result.probeResults().getFirst().diagnostics().size());
        assertTrue(result.probeResults().getFirst().diagnostics().getFirst().message().contains("file-scoped"));
    }

    private Path writeText(Path path, String content) throws Exception {
        Files.createDirectories(path.getParent());
        Files.writeString(path, content);
        Files.setLastModifiedTime(path, FileTime.fromMillis(Math.max(1L, Files.getLastModifiedTime(path).toMillis())));
        return path;
    }

    private static String sha256(byte[] bytes) throws Exception {
        return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes));
    }

    private static final class CountingWalker extends PackerAbstractBankWalker<String> {
        private int processCount;
        private int fingerprintCount;

        private CountingWalker() {
            super(MAPPER);
        }

        @Override
        protected Set<String> getSupportedMimeTypes() {
            return Set.of("text/plain");
        }

        @Override
        protected PackerProbeResult processFileProbe(PackerFileProbe fileProbe, java.nio.file.Path assetRoot, String requirements) {
            processCount += 1;
            return new PackerProbeResult(
                    fileProbe,
                    Map.of("processed", true),
                    List.of(new PackerDiagnostic(
                            p.packer.messages.diagnostics.PackerDiagnosticSeverity.WARNING,
                            p.packer.messages.diagnostics.PackerDiagnosticCategory.HYGIENE,
                            "file-scoped diagnostic",
                            fileProbe.path(),
                            false)));
        }

        @Override
        protected String computeFingerprint(PackerFileProbe fileProbe) {
            fingerprintCount += 1;
            try {
                return sha256(fileProbe.content());
            } catch (Exception exception) {
                throw new RuntimeException(exception);
            }
        }

        private int processCount() {
            return processCount;
        }

        private int fingerprintCount() {
            return fingerprintCount;
        }
    }
}
