package p.packer.repositories;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import p.packer.messages.assets.AssetFamilyCatalog;
import p.packer.messages.assets.OutputCodecCatalog;
import p.packer.messages.assets.OutputFormatCatalog;
import p.packer.models.*;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

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

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

    @TempDir
    Path tempDir;

    @Test
    void reusesPriorAssetCacheOnlyWhenContractFingerprintMatches() throws Exception {
        final var walker = new TrackingWalker();
        final var materializer = new PackerRuntimeAssetMaterializer(walker);
        final Path assetRoot = Files.createDirectories(tempDir.resolve("asset"));
        final Path manifestPath = assetRoot.resolve("asset.json");
        Files.writeString(manifestPath, "{}");

        final PackerAssetDeclaration declaration = declaration(Map.of("tile_size", "32x32"));
        final PackerAssetDeclarationParseResult parseResult = new PackerAssetDeclarationParseResult(declaration, List.of());
        final String fingerprint = PackerContractFingerprint.metadataFingerprint(declaration.outputMetadata());
        final Optional<PackerAssetCacheEntry> priorAssetCache = Optional.of(new PackerAssetCacheEntry(1, fingerprint, List.of()));

        materializer.materialize(
                assetRoot,
                manifestPath,
                Optional.of(new PackerRegistryEntry(1, "uuid", "asset", true)),
                parseResult,
                priorAssetCache);

        assertTrue(walker.lastPriorAssetCache().isPresent());
    }

    @Test
    void invalidatesPriorAssetCacheWhenContractFingerprintChanges() throws Exception {
        final var walker = new TrackingWalker();
        final var materializer = new PackerRuntimeAssetMaterializer(walker);
        final Path assetRoot = Files.createDirectories(tempDir.resolve("asset"));
        final Path manifestPath = assetRoot.resolve("asset.json");
        Files.writeString(manifestPath, "{}");

        materializer.materialize(
                assetRoot,
                manifestPath,
                Optional.of(new PackerRegistryEntry(1, "uuid", "asset", true)),
                new PackerAssetDeclarationParseResult(declaration(Map.of("tile_size", "32x32")), List.of()),
                Optional.of(new PackerAssetCacheEntry(
                        1,
                        PackerContractFingerprint.metadataFingerprint(Map.of("tile_size", "16x16")),
                        List.of())));

        assertTrue(walker.lastPriorAssetCache().isEmpty());
    }

    @Test
    void runtimeDefaultProjectionSuppressesContentBytes() throws Exception {
        final var materializer = new PackerRuntimeAssetMaterializer(new PackerAssetWalker(MAPPER));
        final Path assetRoot = Files.createDirectories(tempDir.resolve("runtime-default"));
        final Path manifestPath = assetRoot.resolve("asset.json");
        Files.writeString(manifestPath, "{}");
        Files.writeString(assetRoot.resolve("studio.asset.json"), "{}");
        writeTile(assetRoot.resolve("selected.png"), 16);
        writeTile(assetRoot.resolve("extra.png"), 16);

        final PackerAssetDeclaration declaration = declaration(
                Map.of("tile_size", "16x16"),
                List.of(new PackerAssetArtifactSelection(null, "selected.png", 0, null)),
                palettePipeline());
        final var materialized = materializer.materialize(
                assetRoot,
                manifestPath,
                Optional.of(new PackerRegistryEntry(1, "uuid", "asset", true)),
                new PackerAssetDeclarationParseResult(declaration, List.of()),
                Optional.empty());

        assertEquals(2, materialized.runtimeAsset().walkProjection().buildCandidateFiles().size());
        assertEquals(List.of("extra.png", "selected.png"), materialized.runtimeAsset().walkProjection().availableFiles());
        assertTrue(materialized.runtimeAsset().walkProjection().buildCandidateFiles().stream()
                .allMatch(file -> file.contentBytes().isEmpty()));
    }

    @Test
    void packingProjectionKeepsSelectedFilesAndInjectsContentBytes() throws Exception {
        final var materializer = new PackerRuntimeAssetMaterializer(new PackerAssetWalker(MAPPER));
        final Path assetRoot = Files.createDirectories(tempDir.resolve("packing"));
        final Path manifestPath = assetRoot.resolve("asset.json");
        Files.writeString(manifestPath, "{}");
        writeTile(assetRoot.resolve("selected.png"), 16);
        writeTile(assetRoot.resolve("extra.png"), 16);

        final PackerAssetDeclaration declaration = declaration(
                Map.of("tile_size", "16x16"),
                List.of(new PackerAssetArtifactSelection(null, "selected.png", 0, null)),
                palettePipeline());
        final var materialized = materializer.materialize(
                assetRoot,
                manifestPath,
                Optional.of(new PackerRegistryEntry(1, "uuid", "asset", true)),
                new PackerAssetDeclarationParseResult(declaration, List.of()),
                Optional.empty(),
                PackerRuntimeMaterializationConfig.packingBuild());

        assertEquals(1, materialized.runtimeAsset().walkProjection().buildCandidateFiles().size());
        final var file = materialized.runtimeAsset().walkProjection().buildCandidateFiles().getFirst();
        assertEquals("selected.png", file.relativePath());
        assertTrue(file.contentBytes().isPresent());
        assertTrue(file.contentBytes().get().length > 0);
    }

    @Test
    void sceneBankPackingProjectionIncludesAcceptedRequestArtifact() throws Exception {
        final var materializer = new PackerRuntimeAssetMaterializer(new PackerAssetWalker(MAPPER));
        final Path assetRoot = Files.createDirectories(tempDir.resolve("scene-packing"));
        final Path manifestPath = assetRoot.resolve("asset.json");
        Files.writeString(manifestPath, "{}");
        Files.writeString(assetRoot.resolve("scene-bank.pack-request.json"), """
                {
                  "schema_version": 1,
                  "format": "SCENE/tilemap_v1",
                  "version": 1,
                  "layers": [
                    {
                      "active": true,
                      "glyph_asset_id": 7,
                      "tile_size": 16,
                      "parallax_factor_x": 1.0,
                      "parallax_factor_y": 1.0,
                      "width": 2,
                      "height": 2,
                      "tiles": [
                        { "active": true, "glyph_id": 1, "palette_id": 0, "flip_x": false, "flip_y": false },
                        { "active": false, "glyph_id": 0, "palette_id": 0, "flip_x": false, "flip_y": false },
                        { "active": false, "glyph_id": 0, "palette_id": 0, "flip_x": false, "flip_y": false },
                        { "active": false, "glyph_id": 0, "palette_id": 0, "flip_x": false, "flip_y": false }
                      ]
                    },
                    {
                      "active": false,
                      "glyph_asset_id": 0,
                      "tile_size": 0,
                      "parallax_factor_x": 1.0,
                      "parallax_factor_y": 1.0,
                      "width": 2,
                      "height": 2,
                      "tiles": [
                        { "active": false, "glyph_id": 0, "palette_id": 0, "flip_x": false, "flip_y": false },
                        { "active": false, "glyph_id": 0, "palette_id": 0, "flip_x": false, "flip_y": false },
                        { "active": false, "glyph_id": 0, "palette_id": 0, "flip_x": false, "flip_y": false },
                        { "active": false, "glyph_id": 0, "palette_id": 0, "flip_x": false, "flip_y": false }
                      ]
                    },
                    {
                      "active": false,
                      "glyph_asset_id": 0,
                      "tile_size": 0,
                      "parallax_factor_x": 1.0,
                      "parallax_factor_y": 1.0,
                      "width": 2,
                      "height": 2,
                      "tiles": [
                        { "active": false, "glyph_id": 0, "palette_id": 0, "flip_x": false, "flip_y": false },
                        { "active": false, "glyph_id": 0, "palette_id": 0, "flip_x": false, "flip_y": false },
                        { "active": false, "glyph_id": 0, "palette_id": 0, "flip_x": false, "flip_y": false },
                        { "active": false, "glyph_id": 0, "palette_id": 0, "flip_x": false, "flip_y": false }
                      ]
                    },
                    {
                      "active": false,
                      "glyph_asset_id": 0,
                      "tile_size": 0,
                      "parallax_factor_x": 1.0,
                      "parallax_factor_y": 1.0,
                      "width": 2,
                      "height": 2,
                      "tiles": [
                        { "active": false, "glyph_id": 0, "palette_id": 0, "flip_x": false, "flip_y": false },
                        { "active": false, "glyph_id": 0, "palette_id": 0, "flip_x": false, "flip_y": false },
                        { "active": false, "glyph_id": 0, "palette_id": 0, "flip_x": false, "flip_y": false },
                        { "active": false, "glyph_id": 0, "palette_id": 0, "flip_x": false, "flip_y": false }
                      ]
                    }
                  ]
                }
                """);

        final PackerAssetDeclaration declaration = new PackerAssetDeclaration(
                1,
                "uuid",
                "scene_asset",
                AssetFamilyCatalog.SCENE_BANK,
                List.of(),
                OutputFormatCatalog.SCENE_TILED_V1,
                OutputCodecCatalog.NONE,
                Map.of(),
                Map.of(),
                true);
        final var materialized = materializer.materialize(
                assetRoot,
                manifestPath,
                Optional.of(new PackerRegistryEntry(1, "uuid", "scene_asset", true)),
                new PackerAssetDeclarationParseResult(declaration, List.of()),
                Optional.empty(),
                PackerRuntimeMaterializationConfig.packingBuild());

        assertEquals(1, materialized.runtimeAsset().walkProjection().buildCandidateFiles().size());
        final var file = materialized.runtimeAsset().walkProjection().buildCandidateFiles().getFirst();
        assertEquals("scene-bank.pack-request.json", file.relativePath());
        assertTrue(file.contentBytes().isPresent());
        assertTrue(file.metadata().get("scenePackRequest") instanceof p.packer.messages.assets.ScenePackRequest);
    }

    private static PackerAssetDeclaration declaration(Map<String, String> metadata) {
        return declaration(metadata, List.of(), Map.of());
    }

    private static PackerAssetDeclaration declaration(
            Map<String, String> metadata,
            List<PackerAssetArtifactSelection> artifacts,
            Map<String, JsonNode> pipelineMetadata) {
        return new PackerAssetDeclaration(
                1,
                "uuid",
                "asset",
                AssetFamilyCatalog.GLYPH_BANK,
                artifacts,
                OutputFormatCatalog.GLYPH_INDEXED_V1,
                OutputCodecCatalog.NONE,
                metadata,
                pipelineMetadata,
                true);
    }

    private static Map<String, JsonNode> palettePipeline() {
        final var payload = MAPPER.createObjectNode();
        payload.putArray("originalArgb8888").add(0xFFFF0000);
        payload.putArray("convertedRgb565").add(0xF800);
        final var declaration = MAPPER.createObjectNode();
        declaration.put("index", 0);
        declaration.set("palette", payload);
        final var palettes = MAPPER.createArrayNode();
        palettes.add(declaration);
        return Map.of("palettes", palettes);
    }

    private static void writeTile(Path path, int tileSize) throws Exception {
        final BufferedImage image = new BufferedImage(tileSize, tileSize, BufferedImage.TYPE_INT_ARGB);
        for (int y = 0; y < tileSize; y += 1) {
            for (int x = 0; x < tileSize; x += 1) {
                image.setRGB(x, y, 0xFFFF0000);
            }
        }
        ImageIO.write(image, "png", path.toFile());
    }

    private static final class TrackingWalker extends PackerAssetWalker {
        private Optional<PackerAssetCacheEntry> lastPriorAssetCache = Optional.empty();

        private TrackingWalker() {
            super(MAPPER);
        }

        @Override
        public PackerWalkResult walk(
                Path assetRoot,
                PackerAssetDeclaration declaration,
                Optional<PackerAssetCacheEntry> priorAssetCache) {
            lastPriorAssetCache = Objects.requireNonNull(priorAssetCache, "priorAssetCache");
            return PackerWalkResult.EMPTY;
        }

        private Optional<PackerAssetCacheEntry> lastPriorAssetCache() {
            return lastPriorAssetCache;
        }
    }
}
