package p.studio.workspaces.assets.tiled;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import p.packer.dtos.PackerCodecConfigurationFieldDTO;
import p.packer.messages.AssetReference;
import p.packer.messages.assets.AssetFamilyCatalog;
import p.packer.messages.assets.OutputCodecCatalog;
import p.packer.messages.assets.OutputFormatCatalog;
import p.packer.messages.assets.PackerCodecConfigurationFieldType;
import p.studio.projects.ProjectReference;
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
import p.studio.workspaces.assets.metadata.AssetStudioMetadataService;
import p.studio.workspaces.assets.metadata.AssetStudioSceneBankMetadata;
import p.studio.workspaces.assets.metadata.AssetStudioSceneLayerBinding;
import p.studio.workspaces.assets.metadata.AssetStudioSceneTile;
import p.studio.workspaces.assets.messages.*;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

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

final class TiledAssetGenerationServiceTest {
    private final TiledAssetGenerationService service = new TiledAssetGenerationService();
    private final TiledXmlCodec codec = new TiledXmlCodec();

    @TempDir
    Path tempDir;

    @Test
    void exportsTsxForTilesetSpecializedGlyphBank() throws Exception {
        final Path assetRoot = tempDir.resolve("assets/tilesets/overworld");
        Files.createDirectories(assetRoot);
        Files.writeString(assetRoot.resolve("a.png"), "fixture");
        Files.writeString(assetRoot.resolve("b.png"), "fixture");

        final TiledAssetGenerationResult result = service.exportTilesetTsx(tilesetDetails(assetRoot));

        assertTrue(result.success());
        final Path tsxPath = assetRoot.resolve(TiledAssetGenerationService.GENERATED_TSX_FILE);
        assertTrue(Files.isRegularFile(tsxPath));
        final TiledTilesetDocument tileset = codec.readTileset(tsxPath);
        assertEquals(2, tileset.tiles().size());
        assertEquals("a.png", tileset.tiles().get(0).imageSource());
        assertEquals("glyph_id", tileset.tiles().get(0).properties().getFirst().name());
    }

    @Test
    void exportsSingleSceneTmxFromCanonicalState() throws Exception {
        final Path projectRoot = tempDir.resolve("project");
        final Path tilesetRoot = projectRoot.resolve("assets/tilesets/overworld");
        Files.createDirectories(tilesetRoot);
        Files.writeString(tilesetRoot.resolve("a.png"), "fixture");
        Files.writeString(tilesetRoot.resolve("b.png"), "fixture");
        Files.writeString(tilesetRoot.resolve("asset.json"), """
                {
                  "output": {
                    "metadata": {
                      "tile_size": "16x16"
                    }
                  }
                }
                """);
        assertTrue(service.exportTilesetTsx(tilesetDetails(tilesetRoot)).success());

        final Path sceneRoot = projectRoot.resolve("assets/scenes/overworld");
        Files.createDirectories(sceneRoot);
        final AssetWorkspaceAssetDetails details = sceneDetails(sceneRoot);
        final ProjectReference projectReference = new ProjectReference("main", "1", "pbs", 1, projectRoot);

        final TiledAssetGenerationResult result = service.exportSceneBankTmx(projectReference, details);

        assertTrue(result.success());
        final Path tmxPath = sceneRoot.resolve("scene.tmx");
        assertTrue(Files.isRegularFile(tmxPath));
        final TiledMapDocument map = codec.readMap(tmxPath);
        assertEquals("../../tilesets/overworld/tileset.tsx", map.tilesets().getFirst().source());
        assertEquals(2, map.tileLayers().size());
        assertEquals("Ground", map.tileLayers().getFirst().name());
        assertEquals(1L, map.tileLayers().getFirst().gids().getFirst());
    }

    @Test
    void exportsSceneTmxKeepingExistingFileAsBackup() throws Exception {
        final Path projectRoot = tempDir.resolve("project-export-backup");
        final Path tilesetRoot = projectRoot.resolve("assets/tilesets/overworld");
        Files.createDirectories(tilesetRoot);
        Files.writeString(tilesetRoot.resolve("a.png"), "fixture");
        Files.writeString(tilesetRoot.resolve("b.png"), "fixture");
        Files.writeString(tilesetRoot.resolve("asset.json"), """
                {
                  "output": {
                    "metadata": {
                      "tile_size": "16x16"
                    }
                  }
                }
                """);
        assertTrue(service.exportTilesetTsx(tilesetDetails(tilesetRoot)).success());

        final Path sceneRoot = projectRoot.resolve("assets/scenes/overworld");
        Files.createDirectories(sceneRoot);
        final AssetWorkspaceAssetDetails details = sceneDetails(sceneRoot);
        final ProjectReference projectReference = new ProjectReference("main", "1", "pbs", 1, projectRoot);

        final Path tmxPath = sceneRoot.resolve("scene.tmx");
        Files.writeString(tmxPath, "legacy");

        final TiledAssetGenerationResult result = service.exportSceneBankTmx(
                projectReference,
                details,
                TiledAssetGenerationService.SceneTmxExportDisposition.KEEP_BOTH);

        assertTrue(result.success());
        final Path backupPath = sceneRoot.resolve("scene.bkp-1.tmx");
        assertTrue(Files.isRegularFile(backupPath));
        assertEquals("legacy", Files.readString(backupPath));
        assertNotEquals("legacy", Files.readString(tmxPath));
        assertTrue(result.writtenFiles().contains(backupPath));
        assertTrue(result.writtenFiles().contains(tmxPath));
    }

    @Test
    void importsSceneTmxIntoCanonicalTiles() throws Exception {
        final Path projectRoot = tempDir.resolve("project-import");
        final Path tilesetRoot = projectRoot.resolve("assets/tilesets/overworld");
        Files.createDirectories(tilesetRoot);
        Files.writeString(tilesetRoot.resolve("a.png"), "fixture");
        Files.writeString(tilesetRoot.resolve("b.png"), "fixture");
        Files.writeString(tilesetRoot.resolve("asset.json"), """
                {
                  "output": {
                    "metadata": {
                      "tile_size": "16x16"
                    }
                  }
                }
                """);
        assertTrue(service.exportTilesetTsx(tilesetDetails(tilesetRoot)).success());

        final Path sceneRoot = projectRoot.resolve("assets/scenes/overworld");
        Files.createDirectories(sceneRoot);
        final AssetWorkspaceAssetDetails details = sceneDetails(sceneRoot);
        final ProjectReference projectReference = new ProjectReference("main", "1", "pbs", 1, projectRoot);
        assertTrue(service.exportSceneBankTmx(projectReference, details).success());

        final Path tmxPath = sceneRoot.resolve("scene.tmx");
        Files.writeString(tmxPath, Files.readString(tmxPath).replaceFirst("1,0,0,0", "2147483650,0,0,0"));

        final TiledAssetGenerationResult result = service.importSceneBankTmx(projectReference, details);

        assertTrue(result.success());
        final AssetStudioMetadataService metadataService = new AssetStudioMetadataService();
        final var snapshot = metadataService.read(sceneRoot, AssetFamilyCatalog.SCENE_BANK);
        assertNotNull(snapshot.sceneBankMetadata());
        assertTrue(snapshot.sceneBankMetadata().layerBindings().getFirst().tiles().getFirst().active());
        assertEquals(1, snapshot.sceneBankMetadata().layerBindings().getFirst().tiles().getFirst().glyphId());
        assertTrue(snapshot.sceneBankMetadata().layerBindings().getFirst().tiles().getFirst().flipX());
    }

    @Test
    void importsSceneTmxUsingGlyphIdPropertyFromTsx() throws Exception {
        final Path projectRoot = tempDir.resolve("project-import-glyph-property");
        final Path tilesetRoot = projectRoot.resolve("assets/tilesets/overworld");
        Files.createDirectories(tilesetRoot);
        Files.writeString(tilesetRoot.resolve("a.png"), "fixture");
        Files.writeString(tilesetRoot.resolve("b.png"), "fixture");
        Files.writeString(tilesetRoot.resolve("asset.json"), """
                {
                  "output": {
                    "metadata": {
                      "tile_size": "16x16"
                    }
                  }
                }
                """);
        assertTrue(service.exportTilesetTsx(tilesetDetails(tilesetRoot)).success());

        final Path sceneRoot = projectRoot.resolve("assets/scenes/overworld");
        Files.createDirectories(sceneRoot);
        final AssetWorkspaceAssetDetails details = sceneDetails(sceneRoot);
        final ProjectReference projectReference = new ProjectReference("main", "1", "pbs", 1, projectRoot);
        assertTrue(service.exportSceneBankTmx(projectReference, details).success());

        final Path tsxPath = tilesetRoot.resolve(TiledAssetGenerationService.GENERATED_TSX_FILE);
        Files.writeString(tsxPath, Files.readString(tsxPath)
                .replace("value=\"0\"", "value=\"7\"")
                .replace("value=\"1\"", "value=\"3\""));

        final Path tmxPath = sceneRoot.resolve("scene.tmx");
        Files.writeString(tmxPath, Files.readString(tmxPath).replaceFirst("1,0,0,0", "2,0,0,0"));

        final TiledAssetGenerationResult result = service.importSceneBankTmx(projectReference, details);

        assertTrue(result.success());
        final AssetStudioMetadataService metadataService = new AssetStudioMetadataService();
        final var snapshot = metadataService.read(sceneRoot, AssetFamilyCatalog.SCENE_BANK);
        assertNotNull(snapshot.sceneBankMetadata());
        assertEquals(3, snapshot.sceneBankMetadata().layerBindings().getFirst().tiles().getFirst().glyphId());
    }

    @Test
    void importsSceneTmxAndPropagatesLayerDimensionsFromTmx() throws Exception {
        final Path projectRoot = tempDir.resolve("project-import-resize");
        final Path tilesetRoot = projectRoot.resolve("assets/tilesets/overworld");
        Files.createDirectories(tilesetRoot);
        Files.writeString(tilesetRoot.resolve("a.png"), "fixture");
        Files.writeString(tilesetRoot.resolve("b.png"), "fixture");
        Files.writeString(tilesetRoot.resolve("asset.json"), """
                {
                  "output": {
                    "metadata": {
                      "tile_size": "16x16"
                    }
                  }
                }
                """);
        assertTrue(service.exportTilesetTsx(tilesetDetails(tilesetRoot)).success());

        final Path sceneRoot = projectRoot.resolve("assets/scenes/overworld");
        Files.createDirectories(sceneRoot);
        final AssetWorkspaceAssetDetails details = sceneDetails(sceneRoot);
        final ProjectReference projectReference = new ProjectReference("main", "1", "pbs", 1, projectRoot);
        assertTrue(service.exportSceneBankTmx(projectReference, details).success());

        final Path tmxPath = sceneRoot.resolve("scene.tmx");
        final String resizedTmx = """
                <?xml version="1.0" encoding="UTF-8"?>
                <map version="1.10" tiledversion="1.12.1" orientation="orthogonal" renderorder="right-down" width="6" height="5" tilewidth="16" tileheight="16" infinite="0" nextlayerid="3" nextobjectid="1">
                 <tileset firstgid="1" source="../../tilesets/overworld/tileset.tsx"/>
                 <layer id="1" name="Ground" width="6" height="5">
                  <data encoding="csv">
                1,0,0,0,0,0,
                0,0,0,0,0,0,
                0,0,0,0,0,0,
                0,0,0,0,0,0,
                0,0,0,0,0,0
                </data>
                 </layer>
                 <layer id="2" name="Decor" width="3" height="2">
                  <data encoding="csv">
                2,0,0,
                0,0,0
                </data>
                 </layer>
                </map>
                """;
        Files.writeString(tmxPath, resizedTmx);

        final TiledAssetGenerationResult result = service.importSceneBankTmx(projectReference, details);

        assertTrue(result.success());
        final AssetStudioMetadataService metadataService = new AssetStudioMetadataService();
        final var snapshot = metadataService.read(sceneRoot, AssetFamilyCatalog.SCENE_BANK);
        assertNotNull(snapshot.sceneBankMetadata());
        assertEquals(6, snapshot.sceneBankMetadata().mapWidth());
        assertEquals(5, snapshot.sceneBankMetadata().mapHeight());
        assertEquals(6, snapshot.sceneBankMetadata().layerBindings().get(0).width());
        assertEquals(5, snapshot.sceneBankMetadata().layerBindings().get(0).height());
        assertEquals(3, snapshot.sceneBankMetadata().layerBindings().get(1).width());
        assertEquals(2, snapshot.sceneBankMetadata().layerBindings().get(1).height());
        assertEquals(30, snapshot.sceneBankMetadata().layerBindings().get(0).tiles().size());
        assertEquals(6, snapshot.sceneBankMetadata().layerBindings().get(1).tiles().size());
        assertEquals("Ground", snapshot.sceneBankMetadata().layerBindings().get(0).layerName());
        assertEquals("Decor", snapshot.sceneBankMetadata().layerBindings().get(1).layerName());
    }

    @Test
    void importsSceneTmxAndPromotesNewLayerIntoDisabledStudioSlot() throws Exception {
        final Path projectRoot = tempDir.resolve("project-import-new-layer");
        final Path tilesetRoot = projectRoot.resolve("assets/tilesets/overworld");
        Files.createDirectories(tilesetRoot);
        Files.writeString(tilesetRoot.resolve("a.png"), "fixture");
        Files.writeString(tilesetRoot.resolve("b.png"), "fixture");
        Files.writeString(tilesetRoot.resolve("asset.json"), """
                {
                  "output": {
                    "metadata": {
                      "tile_size": "16x16"
                    }
                  }
                }
                """);
        assertTrue(service.exportTilesetTsx(tilesetDetails(tilesetRoot)).success());

        final Path sceneRoot = projectRoot.resolve("assets/scenes/overworld");
        Files.createDirectories(sceneRoot);
        final AssetWorkspaceAssetDetails details = sceneDetails(sceneRoot);
        final ProjectReference projectReference = new ProjectReference("main", "1", "pbs", 1, projectRoot);
        assertTrue(service.exportSceneBankTmx(projectReference, details).success());

        final Path tmxPath = sceneRoot.resolve("scene.tmx");
        final String expandedTmx = """
                <?xml version="1.0" encoding="UTF-8"?>
                <map version="1.10" tiledversion="1.12.1" orientation="orthogonal" renderorder="right-down" width="4" height="4" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="1">
                 <tileset firstgid="1" source="../../tilesets/overworld/tileset.tsx"/>
                 <layer id="1" name="Ground" width="4" height="4">
                  <data encoding="csv">
                1,0,0,0,
                0,0,0,0,
                0,0,0,0,
                0,0,0,0
                </data>
                 </layer>
                 <layer id="2" name="Decor" width="4" height="4">
                  <data encoding="csv">
                2,0,0,0,
                0,0,0,0,
                0,0,0,0,
                0,0,0,0
                </data>
                 </layer>
                 <layer id="3" name="Collision" width="2" height="2">
                  <data encoding="csv">
                1,0,
                0,0
                </data>
                 </layer>
                </map>
                """;
        Files.writeString(tmxPath, expandedTmx);

        final TiledAssetGenerationResult result = service.importSceneBankTmx(projectReference, details);

        assertTrue(result.success());
        final AssetStudioMetadataService metadataService = new AssetStudioMetadataService();
        final var snapshot = metadataService.read(sceneRoot, AssetFamilyCatalog.SCENE_BANK);
        assertNotNull(snapshot.sceneBankMetadata());
        assertEquals(3, snapshot.sceneBankMetadata().enabledLayerCount());
        assertTrue(snapshot.sceneBankMetadata().layerBindings().get(2).enabled());
        assertEquals("Layer 3", snapshot.sceneBankMetadata().layerBindings().get(2).layerName());
        assertEquals("tilesets/overworld", snapshot.sceneBankMetadata().layerBindings().get(2).tilesetAssetRoot());
        assertEquals(2, snapshot.sceneBankMetadata().layerBindings().get(2).width());
        assertEquals(2, snapshot.sceneBankMetadata().layerBindings().get(2).height());
        assertEquals(0, snapshot.sceneBankMetadata().layerBindings().get(2).tiles().getFirst().glyphId());
        assertFalse(snapshot.sceneBankMetadata().layerBindings().get(3).enabled());
    }

    @Test
    void rejectsSceneTmxImportWithoutTileLayers() throws Exception {
        final Path projectRoot = tempDir.resolve("project-import-no-layers");
        final Path tilesetRoot = projectRoot.resolve("assets/tilesets/overworld");
        Files.createDirectories(tilesetRoot);
        Files.writeString(tilesetRoot.resolve("a.png"), "fixture");
        Files.writeString(tilesetRoot.resolve("b.png"), "fixture");
        Files.writeString(tilesetRoot.resolve("asset.json"), """
                {
                  "output": {
                    "metadata": {
                      "tile_size": "16x16"
                    }
                  }
                }
                """);
        assertTrue(service.exportTilesetTsx(tilesetDetails(tilesetRoot)).success());

        final Path sceneRoot = projectRoot.resolve("assets/scenes/overworld");
        Files.createDirectories(sceneRoot);
        final AssetWorkspaceAssetDetails details = sceneDetails(sceneRoot);
        final ProjectReference projectReference = new ProjectReference("main", "1", "pbs", 1, projectRoot);

        final Path tmxPath = sceneRoot.resolve("scene.tmx");
        Files.writeString(tmxPath, """
                <?xml version="1.0" encoding="UTF-8"?>
                <map version="1.10" tiledversion="1.12.1" orientation="orthogonal" renderorder="right-down" width="4" height="4" tilewidth="16" tileheight="16" infinite="0" nextlayerid="1" nextobjectid="1">
                 <tileset firstgid="1" source="../../tilesets/overworld/tileset.tsx"/>
                </map>
                """);

        final TiledAssetGenerationResult result = service.importSceneBankTmx(projectReference, details);

        assertFalse(result.success());
        assertEquals("TMX import rejected: at least one tile layer is required.", result.message());
    }

    @Test
    void rejectsSceneTmxImportWithMoreThanFourTileLayers() throws Exception {
        final Path projectRoot = tempDir.resolve("project-import-too-many-layers");
        final Path tilesetRoot = projectRoot.resolve("assets/tilesets/overworld");
        Files.createDirectories(tilesetRoot);
        Files.writeString(tilesetRoot.resolve("a.png"), "fixture");
        Files.writeString(tilesetRoot.resolve("b.png"), "fixture");
        Files.writeString(tilesetRoot.resolve("asset.json"), """
                {
                  "output": {
                    "metadata": {
                      "tile_size": "16x16"
                    }
                  }
                }
                """);
        assertTrue(service.exportTilesetTsx(tilesetDetails(tilesetRoot)).success());

        final Path sceneRoot = projectRoot.resolve("assets/scenes/overworld");
        Files.createDirectories(sceneRoot);
        final AssetWorkspaceAssetDetails details = sceneDetails(sceneRoot);
        final ProjectReference projectReference = new ProjectReference("main", "1", "pbs", 1, projectRoot);

        final Path tmxPath = sceneRoot.resolve("scene.tmx");
        Files.writeString(tmxPath, """
                <?xml version="1.0" encoding="UTF-8"?>
                <map version="1.10" tiledversion="1.12.1" orientation="orthogonal" renderorder="right-down" width="4" height="4" tilewidth="16" tileheight="16" infinite="0" nextlayerid="6" nextobjectid="1">
                 <tileset firstgid="1" source="../../tilesets/overworld/tileset.tsx"/>
                 <layer id="1" name="A" width="4" height="4"><data encoding="csv">1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0</data></layer>
                 <layer id="2" name="B" width="4" height="4"><data encoding="csv">1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0</data></layer>
                 <layer id="3" name="C" width="4" height="4"><data encoding="csv">1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0</data></layer>
                 <layer id="4" name="D" width="4" height="4"><data encoding="csv">1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0</data></layer>
                 <layer id="5" name="E" width="4" height="4"><data encoding="csv">1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0</data></layer>
                </map>
                """);

        final TiledAssetGenerationResult result = service.importSceneBankTmx(projectReference, details);

        assertFalse(result.success());
        assertEquals("TMX import rejected: more than four tile layers are not supported.", result.message());
    }

    private AssetWorkspaceAssetDetails tilesetDetails(Path assetRoot) {
        return new AssetWorkspaceAssetDetails(
                new AssetWorkspaceAssetSummary(
                        AssetReference.forAssetId(1),
                        "overworld_tileset",
                        AssetWorkspaceAssetState.REGISTERED,
                        AssetWorkspaceBuildParticipation.INCLUDED,
                        1,
                        AssetFamilyCatalog.GLYPH_BANK,
                        AssetStudioGlyphSpecialization.TILESET,
                        assetRoot,
                        false,
                        false),
                List.of(),
                OutputFormatCatalog.GLYPH_INDEXED_V1,
                OutputCodecCatalog.NONE,
                List.of(OutputCodecCatalog.NONE),
                Map.of(OutputCodecCatalog.NONE, List.of()),
                List.of(new PackerCodecConfigurationFieldDTO(
                        "tile_size",
                        "Tile Size",
                        PackerCodecConfigurationFieldType.ENUM,
                        "16x16",
                        true,
                        List.of("8x8", "16x16", "32x32"))),
                Map.of(),
                new AssetWorkspaceBankCompositionDetails(
                        List.of(),
                        List.of(
                                new AssetWorkspaceBankCompositionFile("a.png", "a.png", 1L, 1L, null, Map.of()),
                                new AssetWorkspaceBankCompositionFile("b.png", "b.png", 1L, 1L, null, Map.of())),
                        0L),
                List.of(),
                null,
                null,
                AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
                List.of());
    }

    private AssetWorkspaceAssetDetails sceneDetails(Path assetRoot) {
        return new AssetWorkspaceAssetDetails(
                new AssetWorkspaceAssetSummary(
                        AssetReference.forAssetId(2),
                        "overworld_scene",
                        AssetWorkspaceAssetState.REGISTERED,
                        AssetWorkspaceBuildParticipation.EXCLUDED,
                        2,
                        AssetFamilyCatalog.SCENE_BANK,
                        AssetStudioGlyphSpecialization.NONE,
                        assetRoot,
                        false,
                        false),
                List.of(),
                OutputFormatCatalog.SCENE_TILED_V1,
                OutputCodecCatalog.NONE,
                List.of(OutputCodecCatalog.NONE),
                Map.of(OutputCodecCatalog.NONE, List.of()),
                List.of(),
                Map.of(),
                new AssetWorkspaceBankCompositionDetails(List.of(), List.of(), 0L),
                List.of(),
                null,
                new AssetStudioSceneBankMetadata(
                        4,
                        4,
                        4,
                        List.of(
                                new AssetStudioSceneLayerBinding(true, 1, "Ground", 4, 4, "scene.tmx", "tilesets/overworld", 1.0f, 1.0f, 0, canonicalTiles(4, 4, 0)),
                                new AssetStudioSceneLayerBinding(true, 2, "Decor", 4, 4, "scene.tmx", "tilesets/overworld", 1.0f, 1.0f, 0, canonicalTiles(4, 4, 1)),
                                new AssetStudioSceneLayerBinding(false, 3, "Layer 3", 4, 4, "scene.tmx", "", 1.0f, 1.0f, 0),
                                new AssetStudioSceneLayerBinding(false, 4, "Layer 4", 4, 4, "scene.tmx", "", 1.0f, 1.0f, 0)),
                        assetRoot.resolve("scene-bank.studio.json")),
                AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
                List.of());
    }

    private List<AssetStudioSceneTile> canonicalTiles(int width, int height, int glyphId) {
        final ArrayList<AssetStudioSceneTile> tiles = new ArrayList<>(width * height);
        tiles.add(new AssetStudioSceneTile(true, glyphId, 0, false, false));
        while (tiles.size() < width * height) {
            tiles.add(AssetStudioSceneTile.inactive(0));
        }
        return List.copyOf(tiles);
    }
}
