package p.packer.services;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import p.packer.PackerWorkspacePaths;
import p.packer.messages.PackerProjectContext;
import p.packer.repositories.FileSystemPackerCacheRepository;
import p.packer.repositories.PackerAssetWalker;
import p.packer.repositories.PackerRuntimeAssetMaterializer;
import p.packer.repositories.PackerRuntimeLoader;
import p.packer.repositories.PackerRuntimeRegistry;
import p.packer.testing.PackerFixtureLocator;

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

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

final class PackerRuntimeRegistryTest {
    @TempDir
    Path tempDir;

    @Test
    void bootstrapsOneRuntimePerProjectAndReusesItUntilRefresh() throws Exception {
        final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("managed"));
        final PackerRuntimeRegistry registry = runtimeRegistry();
        final PackerProjectContext project = project(projectRoot);

        final PackerProjectRuntime first = registry.getOrLoad(project);
        final PackerProjectRuntime second = registry.getOrLoad(project);

        assertSame(first, second);
        assertEquals(1L, first.snapshot().generation());
        assertEquals(1, first.snapshot().registry().assets().size());
        assertEquals(1, first.snapshot().assets().size());
    }

    @Test
    void refreshRebuildsSnapshotWithoutReplacingRuntimeInstance() throws Exception {
        final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("refresh"));
        final PackerRuntimeRegistry registry = runtimeRegistry();
        final PackerProjectContext project = project(projectRoot);
        final PackerProjectRuntime runtime = registry.getOrLoad(project);

        Files.createDirectories(projectRoot.resolve("assets/audio/ui_sounds"));
        Files.writeString(projectRoot.resolve("assets/audio/ui_sounds/asset.json"), """
                {
                  "schema_version": 1,
                  "asset_uuid": "runtime-refresh-uuid",
                  "name": "ui_sounds",
                  "type": "audio.bank",
                  "output": {
                    "format": "SOUND/bank_v1",
                    "codec": "none"
                  },
                  "preload": {
                    "enabled": false
                  }
                }
                """);

        final PackerProjectRuntime refreshed = registry.refresh(project);

        assertSame(runtime, refreshed);
        assertTrue(refreshed.snapshot().generation() > 1L);
        assertEquals(2, refreshed.snapshot().assets().size());
    }

    @Test
    void runtimeSnapshotRetainsWalkProjectionAndPersistsCacheForRegisteredAssets() throws Exception {
        final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("cache"));
        writeTilePng(projectRoot.resolve("assets/ui/atlas/tile.png"));
        final PackerRuntimeRegistry registry = runtimeRegistry();
        final PackerProjectContext project = project(projectRoot);

        final PackerProjectRuntime runtime = registry.getOrLoad(project);

        assertTrue(Files.isRegularFile(PackerWorkspacePaths.cachePath(project)));
        assertTrue(runtime.snapshot().cacheState().findAsset(1).isPresent());
        final var runtimeAsset = runtime.snapshot().assets().getFirst();
        assertEquals(List.of("tile.png"), runtimeAsset.walkProjection().availableFiles());
        assertEquals(1, runtimeAsset.walkProjection().buildCandidateFiles().size());
        assertEquals("tile.png", runtimeAsset.walkProjection().buildCandidateFiles().getFirst().relativePath());
        assertTrue(runtimeAsset.walkProjection().measuredBankSizeBytes() > 0L);
    }

    @Test
    void disposeMarksRuntimeInactiveAndRemovesItFromRegistry() throws Exception {
        final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("dispose"));
        final PackerRuntimeRegistry registry = runtimeRegistry();
        final PackerProjectContext project = project(projectRoot);
        final PackerProjectRuntime runtime = registry.getOrLoad(project);

        registry.dispose(project);

        assertTrue(runtime.disposed());
        assertTrue(registry.find(project).isEmpty());
        assertThrows(IllegalStateException.class, runtime::snapshot);
    }

    private PackerRuntimeRegistry runtimeRegistry() {
        final var mapper = new ObjectMapper();
        final var foundation = new PackerWorkspaceFoundation(mapper);
        final var parser = new PackerAssetDeclarationParser(mapper);
        final var assetWalker = new PackerAssetWalker(mapper);
        final var cacheRepository = new FileSystemPackerCacheRepository(mapper);
        final var assetMaterializer = new PackerRuntimeAssetMaterializer(assetWalker);
        return new PackerRuntimeRegistry(new PackerRuntimeLoader(
                foundation,
                parser,
                cacheRepository,
                assetMaterializer));
    }

    private PackerProjectContext project(Path root) {
        return new PackerProjectContext("main", root);
    }

    private Path copyFixture(String relativePath, Path targetRoot) throws Exception {
        final Path sourceRoot = PackerFixtureLocator.fixtureRoot(relativePath);
        try (var stream = Files.walk(sourceRoot)) {
            for (Path source : stream.sorted(Comparator.naturalOrder()).toList()) {
                final Path target = targetRoot.resolve(sourceRoot.relativize(source).toString());
                if (Files.isDirectory(source)) {
                    Files.createDirectories(target);
                } else {
                    Files.createDirectories(target.getParent());
                    Files.copy(source, target);
                }
            }
        }
        return targetRoot;
    }

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