package p.packer.services;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import p.packer.messages.PackerOperationStatus;
import p.packer.messages.PackerProjectContext;
import p.packer.messages.assets.AssetFamilyCatalog;
import p.packer.messages.assets.OutputCodecCatalog;
import p.packer.messages.assets.OutputFormatCatalog;
import p.packer.messages.AssetReference;
import p.packer.messages.assets.PackerBuildParticipation;
import p.packer.messages.assets.PackerAssetState;
import p.packer.events.PackerEvent;
import p.packer.events.PackerEventKind;
import p.packer.messages.CreateAssetRequest;
import p.packer.messages.CreateAssetResult;
import p.packer.messages.GetAssetActionsRequest;
import p.packer.messages.GetAssetDetailsRequest;
import p.packer.messages.ListAssetsRequest;
import p.packer.messages.RegisterAssetRequest;
import p.packer.messages.RegisterAssetResult;
import p.packer.messages.assets.AssetAction;
import p.packer.testing.PackerFixtureLocator;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

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

final class FileSystemPackerWorkspaceServiceTest {
    @TempDir
    Path tempDir;

    @Test
    void listsRegisteredAndUnregisteredAssetsFromWorkspaceScan() throws Exception {
        final Path projectRoot = copyFixture("workspaces/read-mixed", tempDir.resolve("mixed"));
        final FileSystemPackerWorkspaceService service = service();

        final var result = service.listAssets(new ListAssetsRequest(project(projectRoot)));

        assertEquals(PackerOperationStatus.SUCCESS, result.status());
        assertEquals(2, result.assets().size());
        assertTrue(result.assets().stream().anyMatch(asset -> asset.state() == PackerAssetState.REGISTERED));
        assertTrue(result.assets().stream().anyMatch(asset -> asset.state() == PackerAssetState.UNREGISTERED));
        assertTrue(result.assets().stream().allMatch(asset -> asset.identity().assetUuid() != null && !asset.identity().assetUuid().isBlank()));
    }

    @Test
    void surfacesMissingRegisteredRootAsStructuralDiagnostic() throws Exception {
        final Path projectRoot = copyFixture("workspaces/read-missing-root", tempDir.resolve("missing-root"));
        final FileSystemPackerWorkspaceService service = service();

        final var result = service.listAssets(new ListAssetsRequest(project(projectRoot)));

        assertEquals(PackerOperationStatus.PARTIAL, result.status());
        assertTrue(result.diagnostics().stream().anyMatch(diagnostic -> diagnostic.message().contains("missing asset.json")));
    }

    @Test
    void includesInvalidDeclarationsInSnapshotWithDiagnostics() throws Exception {
        final Path projectRoot = copyFixture("workspaces/read-invalid", tempDir.resolve("invalid"));
        final FileSystemPackerWorkspaceService service = service();

        final var result = service.listAssets(new ListAssetsRequest(project(projectRoot)));

        assertEquals(1, result.assets().size());
        assertEquals(PackerAssetState.UNREGISTERED, result.assets().getFirst().state());
        assertEquals(PackerBuildParticipation.EXCLUDED, result.assets().getFirst().buildParticipation());
        assertTrue(result.assets().getFirst().hasDiagnostics());
    }

    @Test
    void emitsDiscoveryAndDiagnosticsEventsDuringScan() throws Exception {
        final Path projectRoot = copyFixture("workspaces/read-invalid", tempDir.resolve("events"));
        final List<PackerEvent> events = new CopyOnWriteArrayList<>();
        final FileSystemPackerWorkspaceService service = service(events::add);

        service.listAssets(new ListAssetsRequest(project(projectRoot)));

        assertTrue(events.stream().anyMatch(event -> event.kind() == PackerEventKind.ASSET_DISCOVERED));
        assertTrue(events.stream().anyMatch(event -> event.kind() == PackerEventKind.DIAGNOSTICS_UPDATED));
        assertTrue(events.stream().allMatch(event -> event.sequence() >= 0L));
    }

    @Test
    void createsRegisteredAssetAndWritesManifest() throws Exception {
        final Path projectRoot = tempDir.resolve("created");
        final List<PackerEvent> events = new CopyOnWriteArrayList<>();
        final FileSystemPackerWorkspaceService service = service(events::add);

        final var result = service.createAsset(new CreateAssetRequest(
                project(projectRoot),
                "ui/new-atlas",
                "new_atlas",
                AssetFamilyCatalog.IMAGE_BANK,
                OutputFormatCatalog.TILES_INDEXED_V1,
                OutputCodecCatalog.NONE,
                true));

        assertEquals(PackerOperationStatus.SUCCESS, result.status());
        assertNotNull(result.assetReference());
        assertTrue(Files.isRegularFile(projectRoot.resolve("assets/ui/new-atlas/asset.json")));
        final String manifestJson = Files.readString(projectRoot.resolve("assets/ui/new-atlas/asset.json"));
        assertTrue(manifestJson.contains("\"asset_uuid\""));

        final var snapshot = service.listAssets(new ListAssetsRequest(project(projectRoot)));
        assertEquals(1, snapshot.assets().size());
        assertEquals(PackerAssetState.REGISTERED, snapshot.assets().getFirst().state());
        assertEquals("new_atlas", snapshot.assets().getFirst().identity().assetName());
        assertNotNull(snapshot.assets().getFirst().identity().assetUuid());
        assertEquals(PackerBuildParticipation.INCLUDED, snapshot.assets().getFirst().buildParticipation());
        assertTrue(events.stream().anyMatch(event -> event.kind() == PackerEventKind.ACTION_APPLIED));
    }

    @Test
    void returnsCreatedAssetThroughRuntimeBackedDetailsWithoutRescanMismatch() throws Exception {
        final Path projectRoot = tempDir.resolve("created-details");
        final FileSystemPackerWorkspaceService service = service();

        final CreateAssetResult createResult = service.createAsset(new CreateAssetRequest(
                project(projectRoot),
                "ui/new-atlas",
                "new_atlas",
                AssetFamilyCatalog.IMAGE_BANK,
                OutputFormatCatalog.TILES_INDEXED_V1,
                OutputCodecCatalog.NONE,
                true));

        assertEquals(PackerOperationStatus.SUCCESS, createResult.status());
        final AssetReference assetReference = createResult.assetReference();
        assertNotNull(assetReference);

        final var detailsResult = service.getAssetDetails(new GetAssetDetailsRequest(
                project(projectRoot),
                assetReference));

        assertEquals(PackerOperationStatus.SUCCESS, detailsResult.status());
        assertEquals(PackerAssetState.REGISTERED, detailsResult.details().summary().state());
        assertEquals("new_atlas", detailsResult.details().summary().identity().assetName());
        assertNotNull(detailsResult.details().summary().identity().assetUuid());
        assertTrue(detailsResult.diagnostics().isEmpty());
    }

    @Test
    void createAssetPatchesLoadedSnapshotWithoutWholeProjectReload() throws Exception {
        final Path projectRoot = tempDir.resolve("created-no-reload");
        final CountingLoader loader = countingLoader();
        final FileSystemPackerWorkspaceService service = service(ignored -> { }, loader);

        service.listAssets(new ListAssetsRequest(project(projectRoot)));
        assertEquals(1, loader.loadCount());

        final CreateAssetResult createResult = service.createAsset(new CreateAssetRequest(
                project(projectRoot),
                "ui/new-atlas",
                "new_atlas",
                AssetFamilyCatalog.IMAGE_BANK,
                OutputFormatCatalog.TILES_INDEXED_V1,
                OutputCodecCatalog.NONE,
                true));

        assertEquals(PackerOperationStatus.SUCCESS, createResult.status());
        assertEquals(1, loader.loadCount());

        final var detailsResult = service.getAssetDetails(new GetAssetDetailsRequest(project(projectRoot), createResult.assetReference()));
        assertEquals(PackerOperationStatus.SUCCESS, detailsResult.status());
        assertEquals(1, loader.loadCount());
    }

    @Test
    void exposesRegisterActionForValidUnregisteredAsset() throws Exception {
        final Path projectRoot = copyFixture("workspaces/orphan-valid", tempDir.resolve("orphan-actions"));
        final FileSystemPackerWorkspaceService service = service();

        final var result = service.getAssetActions(new GetAssetActionsRequest(
                project(projectRoot),
                AssetReference.forRelativeAssetRoot("orphans/ui_sounds")));

        assertEquals(PackerOperationStatus.SUCCESS, result.status());
        assertEquals(1, result.actions().size());
        assertEquals(AssetAction.REGISTER, result.actions().getFirst().action());
        assertTrue(result.actions().getFirst().visible());
        assertTrue(result.actions().getFirst().enabled());
        assertNull(result.actions().getFirst().reason());
    }

    @Test
    void hidesRegisterActionForRegisteredAsset() throws Exception {
        final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("managed-actions"));
        final FileSystemPackerWorkspaceService service = service();

        final var result = service.getAssetActions(new GetAssetActionsRequest(
                project(projectRoot),
                AssetReference.forAssetId(1)));

        assertEquals(PackerOperationStatus.SUCCESS, result.status());
        assertTrue(result.actions().isEmpty());
    }

    @Test
    void registersExistingUnregisteredAssetAndRefreshesRuntimeView() throws Exception {
        final Path projectRoot = copyFixture("workspaces/orphan-valid", tempDir.resolve("register-orphan"));
        final List<PackerEvent> events = new CopyOnWriteArrayList<>();
        final FileSystemPackerWorkspaceService service = service(events::add);

        final RegisterAssetResult registerResult = service.registerAsset(new RegisterAssetRequest(
                project(projectRoot),
                AssetReference.forRelativeAssetRoot("orphans/ui_sounds")));

        assertEquals(PackerOperationStatus.SUCCESS, registerResult.status());
        assertNotNull(registerResult.assetReference());
        assertTrue(registerResult.assetReference().isAssetIdReference());

        final var detailsResult = service.getAssetDetails(new GetAssetDetailsRequest(
                project(projectRoot),
                registerResult.assetReference()));

        assertEquals(PackerOperationStatus.SUCCESS, detailsResult.status());
        assertEquals(PackerAssetState.REGISTERED, detailsResult.details().summary().state());
        assertEquals("orphan-uuid-1", detailsResult.details().summary().identity().assetUuid());
        assertTrue(events.stream().anyMatch(event -> event.kind() == PackerEventKind.ACTION_APPLIED));
    }

    @Test
    void registerAssetPatchesLoadedSnapshotWithoutWholeProjectReload() throws Exception {
        final Path projectRoot = copyFixture("workspaces/orphan-valid", tempDir.resolve("register-no-reload"));
        final CountingLoader loader = countingLoader();
        final FileSystemPackerWorkspaceService service = service(ignored -> { }, loader);

        service.listAssets(new ListAssetsRequest(project(projectRoot)));
        assertEquals(1, loader.loadCount());

        final RegisterAssetResult registerResult = service.registerAsset(new RegisterAssetRequest(
                project(projectRoot),
                AssetReference.forRelativeAssetRoot("orphans/ui_sounds")));

        assertEquals(PackerOperationStatus.SUCCESS, registerResult.status());
        assertEquals(1, loader.loadCount());

        final var actionsResult = service.getAssetActions(new GetAssetActionsRequest(
                project(projectRoot),
                registerResult.assetReference()));
        assertTrue(actionsResult.actions().isEmpty());
        assertEquals(1, loader.loadCount());
    }

    @Test
    void blocksRegisterActionForInvalidDeclaration() throws Exception {
        final Path projectRoot = copyFixture("workspaces/invalid-missing-fields", tempDir.resolve("invalid-actions"));
        final FileSystemPackerWorkspaceService service = service();

        final var result = service.getAssetActions(new GetAssetActionsRequest(
                project(projectRoot),
                AssetReference.forRelativeAssetRoot("bad")));

        assertEquals(PackerOperationStatus.PARTIAL, result.status());
        assertEquals(1, result.actions().size());
        assertFalse(result.actions().getFirst().enabled());
        assertNotNull(result.actions().getFirst().reason());
    }

    @Test
    void rejectsRegisterWhenAssetUuidIsAlreadyRegisteredElsewhere() throws Exception {
        final Path projectRoot = tempDir.resolve("duplicate-uuid");
        Files.createDirectories(projectRoot.resolve("assets/registered/ui_atlas"));
        Files.createDirectories(projectRoot.resolve("assets/orphans/ui_clone"));
        Files.createDirectories(projectRoot.resolve("assets/.prometeu"));
        Files.writeString(projectRoot.resolve("assets/registered/ui_atlas/asset.json"), """
                {
                  "schema_version": 1,
                  "asset_uuid": "shared-uuid",
                  "name": "ui_atlas",
                  "type": "IMAGE/bank",
                  "inputs": {},
                  "output": {
                    "format": "TILES/indexed_v1",
                    "codec": "none"
                  },
                  "preload": {
                    "enabled": true
                  }
                }
                """);
        Files.writeString(projectRoot.resolve("assets/orphans/ui_clone/asset.json"), """
                {
                  "schema_version": 1,
                  "asset_uuid": "shared-uuid",
                  "name": "ui_clone",
                  "type": "IMAGE/bank",
                  "inputs": {},
                  "output": {
                    "format": "TILES/indexed_v1",
                    "codec": "none"
                  },
                  "preload": {
                    "enabled": false
                  }
                }
                """);
        Files.writeString(projectRoot.resolve("assets/.prometeu/index.json"), """
                {
                  "schema_version": 1,
                  "next_asset_id": 2,
                  "assets": [
                    {
                      "asset_id": 1,
                      "asset_uuid": "shared-uuid",
                      "root": "registered/ui_atlas",
                      "included_in_build": true
                    }
                  ]
                }
                """);
        final FileSystemPackerWorkspaceService service = service();

        final var actions = service.getAssetActions(new GetAssetActionsRequest(
                project(projectRoot),
                AssetReference.forRelativeAssetRoot("orphans/ui_clone")));
        assertEquals(1, actions.actions().size());
        assertFalse(actions.actions().getFirst().enabled());
        assertNotNull(actions.actions().getFirst().reason());

        final RegisterAssetResult registerResult = service.registerAsset(new RegisterAssetRequest(
                project(projectRoot),
                AssetReference.forRelativeAssetRoot("orphans/ui_clone")));
        assertEquals(PackerOperationStatus.FAILED, registerResult.status());
        assertNull(registerResult.assetReference());
    }

    @Test
    void rejectsUnsupportedFormatForSelectedFamily() {
        final Path projectRoot = tempDir.resolve("unsupported");
        final List<PackerEvent> events = new CopyOnWriteArrayList<>();
        final FileSystemPackerWorkspaceService service = service(events::add);

        final var result = service.createAsset(new CreateAssetRequest(
                project(projectRoot),
                "audio/bad",
                "bad_asset",
                AssetFamilyCatalog.IMAGE_BANK,
                OutputFormatCatalog.SOUND_BANK_V1,
                OutputCodecCatalog.NONE,
                false));

        assertEquals(PackerOperationStatus.FAILED, result.status());
        assertNull(result.assetReference());
        assertTrue(result.summary().contains("not supported"));
        assertTrue(events.stream().anyMatch(event -> event.kind() == PackerEventKind.ACTION_FAILED));
    }

    @Test
    void serializesConcurrentCreateAssetRequestsPerProject() throws Exception {
        final Path projectRoot = tempDir.resolve("concurrent-create");
        final FileSystemPackerWorkspaceService service = service();
        try (var executor = Executors.newFixedThreadPool(2)) {
            final Future<CreateAssetResult> first = executor.submit(() -> service.createAsset(new CreateAssetRequest(
                    project(projectRoot),
                    "ui/atlas-a",
                    "atlas_a",
                    AssetFamilyCatalog.IMAGE_BANK,
                    OutputFormatCatalog.TILES_INDEXED_V1,
                    OutputCodecCatalog.NONE,
                    true)));
            final Future<CreateAssetResult> second = executor.submit(() -> service.createAsset(new CreateAssetRequest(
                    project(projectRoot),
                    "ui/atlas-b",
                    "atlas_b",
                    AssetFamilyCatalog.IMAGE_BANK,
                    OutputFormatCatalog.TILES_INDEXED_V1,
                    OutputCodecCatalog.NONE,
                    false)));

            assertEquals(PackerOperationStatus.SUCCESS, first.get().status());
            assertEquals(PackerOperationStatus.SUCCESS, second.get().status());
        }

        final var snapshot = service.listAssets(new ListAssetsRequest(project(projectRoot)));
        assertEquals(2, snapshot.assets().size());
        assertEquals(2, new HashSet<>(snapshot.assets().stream().map(asset -> asset.identity().assetId()).toList()).size());
    }

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

    private FileSystemPackerWorkspaceService service() {
        return service(ignored -> { });
    }

    private FileSystemPackerWorkspaceService service(p.packer.events.PackerEventSink eventSink) {
        return service(eventSink, countingLoader());
    }

    private FileSystemPackerWorkspaceService service(
            p.packer.events.PackerEventSink eventSink,
            PackerRuntimeSnapshotLoader loader) {
        final var foundation = new p.packer.services.PackerWorkspaceFoundation();
        final var parser = new p.packer.services.PackerAssetDeclarationParser();
        final var runtimeRegistry = new p.packer.services.PackerRuntimeRegistry(loader);
        final var resolver = new p.packer.services.PackerAssetReferenceResolver(foundation.lookup());
        final var detailsService = new p.packer.services.PackerAssetDetailsService(runtimeRegistry, resolver);
        final var actionReadService = new p.packer.services.PackerAssetActionReadService(runtimeRegistry, resolver, foundation.lookup());
        final var runtimePatchService = new p.packer.services.PackerRuntimePatchService(parser);
        final var writeCoordinator = new p.packer.services.PackerProjectWriteCoordinator();
        return new FileSystemPackerWorkspaceService(
                foundation,
                detailsService,
                actionReadService,
                runtimePatchService,
                runtimeRegistry,
                writeCoordinator,
                eventSink);
    }

    private CountingLoader countingLoader() {
        final var foundation = new p.packer.services.PackerWorkspaceFoundation();
        final var parser = new p.packer.services.PackerAssetDeclarationParser();
        return new CountingLoader(new p.packer.services.PackerRuntimeLoader(foundation, parser));
    }

    private static final class CountingLoader implements PackerRuntimeSnapshotLoader {
        private final PackerRuntimeSnapshotLoader delegate;
        private int loadCount;

        private CountingLoader(PackerRuntimeSnapshotLoader delegate) {
            this.delegate = delegate;
        }

        @Override
        public synchronized p.packer.models.PackerRuntimeSnapshot load(PackerProjectContext project, long generation) {
            loadCount += 1;
            return delegate.load(project, generation);
        }

        private synchronized int loadCount() {
            return loadCount;
        }
    }

    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;
    }
}
