package p.studio.compiler.pbs;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import p.studio.compiler.messages.BuildingIssueSink;
import p.studio.compiler.messages.FrontendPhaseContext;
import p.studio.compiler.models.*;
import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.pbs.lexer.PbsLexer;
import p.studio.compiler.pbs.linking.PbsLinkErrors;
import p.studio.compiler.pbs.linking.PbsModuleVisibilityValidator;
import p.studio.compiler.pbs.parser.ParseErrors;
import p.studio.compiler.pbs.parser.PbsBarrelParser;
import p.studio.compiler.pbs.parser.PbsParser;
import p.studio.compiler.pbs.semantics.PbsSemanticsErrors;
import p.studio.compiler.services.PBSFrontendPhaseService;
import p.studio.compiler.source.diagnostics.Diagnostic;
import p.studio.compiler.source.diagnostics.DiagnosticPhase;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.identifiers.FileId;
import p.studio.compiler.source.identifiers.ProjectId;
import p.studio.compiler.source.tables.FileTable;
import p.studio.compiler.source.tables.ProjectTable;
import p.studio.compiler.utilities.SourceProviderFactory;
import p.studio.utilities.logs.LogAggregator;
import p.studio.utilities.structures.ReadOnlyList;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;

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

class PbsGateUSdkInterfaceConformanceTest {

    @TempDir
    Path tempDir;

    @Test
    void gateU_shouldClassifySourceKindForInterfaceDeclarations() {
        final var source = """
                [BuiltinType(name = "Color", version = 1)]
                declare builtin type Color(
                  pub r: int
                ) {
                }

                declare host Gfx {
                  [Host(module = "gfx", name = "draw_pixel", version = 1)]
                  [Capability(name = "gfx")]
                  fn draw_pixel(x: int, y: int, color: Color) -> void;
                }
                """;

        final var projectDiagnostics = DiagnosticSink.empty();
        final var projectBackend = new PbsFrontendCompiler().compileFile(
                new FileId(10),
                source,
                projectDiagnostics,
                SourceKind.PROJECT);
        assertTrue(projectDiagnostics.stream().anyMatch(d ->
                d.getCode().equals(ParseErrors.E_PARSE_ATTRIBUTES_NOT_ALLOWED.name())
                        || d.getCode().equals(ParseErrors.E_PARSE_RESERVED_DECLARATION.name())));
        assertEquals(0, projectBackend.functions().size());

        final var sdkDiagnostics = DiagnosticSink.empty();
        final var sdkBackend = new PbsFrontendCompiler().compileFile(
                new FileId(11),
                source,
                sdkDiagnostics,
                SourceKind.SDK_INTERFACE);
        assertFalse(sdkDiagnostics.hasErrors());
        assertEquals(0, sdkBackend.functions().size());
        assertEquals(1, sdkBackend.reservedMetadata().builtinTypeSurfaces().size());
        assertEquals(1, sdkBackend.reservedMetadata().hostMethodBindings().size());
    }

    @Test
    void gateU_shouldResolveReservedImportsAndRejectMissingReservedModuleDeterministically() throws IOException {
        final var positive = compileWorkspaceModule(
                tempDir.resolve("gate-u-reserved-import-positive"),
                """
                import { Color } from @core:color;
                import { Composer } from @sdk:composer;
                import { Gfx } from @sdk:gfx;
                import { Input, InputPad, InputButton } from @sdk:input;
                import { Assets } from @sdk:asset;
                import { Log } from @sdk:log;

                declare contract Renderer {
                  fn render(composer: Composer, gfx: Gfx, color: Color, input: Input, pad: InputPad, button: InputButton, assets: Assets, log: Log) -> void;
                }
                """,
                "pub contract Renderer;",
                1,
                null);
        assertFalse(positive.diagnostics().stream().anyMatch(d ->
                d.getCode().equals(PbsLinkErrors.E_LINK_IMPORT_MODULE_NOT_FOUND.name())));
        assertFalse(positive.diagnostics().stream().anyMatch(d ->
                d.getCode().equals(PbsLinkErrors.E_LINK_IMPORT_SYMBOL_UNRESOLVED.name())));
        assertTrue(positive.irBackend().getReservedMetadata().builtinTypeSurfaces().stream()
                .anyMatch(t -> t.sourceTypeName().equals("Color")));
        assertTrue(positive.irBackend().getReservedMetadata().builtinTypeSurfaces().stream()
                .anyMatch(t -> t.sourceTypeName().equals("InputType") && t.canonicalTypeName().equals("input")));
        assertTrue(positive.irBackend().getReservedMetadata().builtinConstSurfaces().stream()
                .anyMatch(c -> c.sourceConstName().equals("LowInput") && c.canonicalTarget().equals("input")));
        assertTrue(positive.irBackend().getReservedMetadata().builtinTypeSurfaces().stream()
                .anyMatch(t -> t.sourceTypeName().equals("InputButton")
                        && t.intrinsics().stream().anyMatch(i -> i.canonicalName().equals("input.button.hold"))));
        assertTrue(positive.irBackend().getReservedMetadata().hostMethodBindings().stream()
                .anyMatch(h -> h.ownerName().equals("LowGfx")
                        && h.abiModule().equals("gfx")
                        && h.abiMethod().equals("clear")
                        && h.abiVersion() == 1));
        assertTrue(positive.irBackend().getReservedMetadata().hostMethodBindings().stream()
                .anyMatch(h -> h.ownerName().equals("LowComposer")
                        && h.abiModule().equals("composer")
                        && h.abiMethod().equals("bind_scene")
                        && h.abiVersion() == 1));
        assertTrue(positive.irBackend().getReservedMetadata().hostMethodBindings().stream()
                .anyMatch(h -> h.ownerName().equals("LowComposer")
                        && h.abiModule().equals("composer")
                        && h.abiMethod().equals("unbind_scene")
                        && h.abiVersion() == 1));
        assertTrue(positive.irBackend().getReservedMetadata().hostMethodBindings().stream()
                .anyMatch(h -> h.ownerName().equals("LowComposer")
                        && h.abiModule().equals("composer")
                        && h.abiMethod().equals("emit_sprite")
                        && h.abiVersion() == 1));
        assertTrue(positive.irBackend().getReservedMetadata().hostMethodBindings().stream()
                .anyMatch(h -> h.ownerName().equals("LowLog")
                        && h.abiModule().equals("log")
                        && h.abiMethod().equals("write")
                        && h.abiVersion() == 1));
        assertTrue(positive.irBackend().getReservedMetadata().hostMethodBindings().stream()
                .anyMatch(h -> h.ownerName().equals("LowLog")
                        && h.abiModule().equals("log")
                        && h.abiMethod().equals("write_tag")
                        && h.abiVersion() == 1));
        assertTrue(positive.irBackend().getReservedMetadata().hostMethodBindings().stream()
                .anyMatch(h -> h.ownerName().equals("LowAssets")
                        && h.abiModule().equals("asset")
                        && h.abiMethod().equals("load")
                        && h.abiVersion() == 1
                        && java.util.Objects.equals(h.assetLoweringParam(), 0)));

        final var negative = compileWorkspaceModule(
                tempDir.resolve("gate-u-reserved-import-negative"),
                """
                import { Missing } from @sdk:missing;
                fn run() -> int { return 1; }
                """,
                "pub fn run() -> int;",
                1,
                d -> d.getCode().equals(PbsLinkErrors.E_LINK_IMPORT_MODULE_NOT_FOUND.name()));
        final var missingModule = firstDiagnostic(negative.diagnostics(),
                d -> d.getCode().equals(PbsLinkErrors.E_LINK_IMPORT_MODULE_NOT_FOUND.name()));
        assertStableDiagnosticIdentity(missingModule, PbsLinkErrors.E_LINK_IMPORT_MODULE_NOT_FOUND.name(), DiagnosticPhase.LINKING);

        final var lowLogNotPublic = compileWorkspaceModule(
                tempDir.resolve("gate-u-reserved-import-lowlog-hidden"),
                """
                import { LowLog } from @sdk:log;
                fn run() -> int { return 1; }
                """,
                "pub fn run() -> int;",
                1,
                d -> d.getCode().equals(PbsLinkErrors.E_LINK_IMPORT_SYMBOL_NOT_PUBLIC.name()));
        final var notPublic = firstDiagnostic(lowLogNotPublic.diagnostics(),
                d -> d.getCode().equals(PbsLinkErrors.E_LINK_IMPORT_SYMBOL_NOT_PUBLIC.name()));
        assertStableDiagnosticIdentity(notPublic, PbsLinkErrors.E_LINK_IMPORT_SYMBOL_NOT_PUBLIC.name(), DiagnosticPhase.LINKING);
    }

    @Test
    void gateU_shouldRejectUnsupportedControlFlowInExecutableSdkServiceBodies() {
        final var diagnostics = DiagnosticSink.empty();
        new PbsFrontendCompiler().compileFile(
                new FileId(60),
                """
                [BuiltinType(name = "input", version = 1)]
                declare builtin type InputType() {
                  [IntrinsicCall(name = "touch", version = 1)]
                  fn touch() -> InputTouch;
                }

                [BuiltinType(name = "input.touch", version = 1)]
                declare builtin type InputTouch() {
                  [IntrinsicCall(name = "x", version = 1)]
                  fn x() -> int;
                }

                [BuiltinConst(target = "input", name = "global", version = 1)]
                declare const LowInput: InputType;

                declare service Input {
                  fn touch() -> InputTouch {
                    if (true) {
                      return LowInput.touch();
                    }
                    return LowInput.touch();
                  }
                }
                """,
                diagnostics,
                SourceKind.SDK_INTERFACE);

        final var diagnostic = firstDiagnostic(diagnostics,
                d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_SDK_EXEC_SERVICE_UNSUPPORTED_CONSTRUCT.name()));
        assertStableDiagnosticIdentity(
                diagnostic,
                PbsSemanticsErrors.E_SEM_SDK_EXEC_SERVICE_UNSUPPORTED_CONSTRUCT.name(),
                DiagnosticPhase.STATIC_SEMANTICS);
    }

    @Test
    void gateU_shouldParseReservedDeclarationsInInterfaceModeOnly() {
        final var source = """
                declare host Gfx {
                  fn draw_pixel(x: int, y: int) -> void;
                }
                """;
        final var interfaceDiagnostics = DiagnosticSink.empty();
        final var interfaceAst = PbsParser.parse(
                PbsLexer.lex(source, new FileId(20), interfaceDiagnostics),
                new FileId(20),
                interfaceDiagnostics,
                PbsParser.ParseMode.INTERFACE_MODULE);
        assertFalse(interfaceDiagnostics.hasErrors());
        assertEquals(1, interfaceAst.topDecls().size());
        assertInstanceOf(PbsAst.HostDecl.class, interfaceAst.topDecls().getFirst());

        final var ordinaryDiagnostics = DiagnosticSink.empty();
        PbsParser.parse(
                PbsLexer.lex(source, new FileId(21), ordinaryDiagnostics),
                new FileId(21),
                ordinaryDiagnostics,
                PbsParser.ParseMode.ORDINARY);
        final var reservedDeclDiagnostic = firstDiagnostic(ordinaryDiagnostics,
                d -> d.getCode().equals(ParseErrors.E_PARSE_RESERVED_DECLARATION.name()));
        assertStableDiagnosticIdentity(reservedDeclDiagnostic, ParseErrors.E_PARSE_RESERVED_DECLARATION.name(), DiagnosticPhase.SYNTAX);
    }

    @Test
    void gateU_shouldValidateAndLinkHostBuiltinShellsWithDeterministicFailures() {
        final var sourceId = new FileId(30);
        final var barrelId = new FileId(31);
        final var source = """
                [BuiltinType(name = "Color", version = 1)]
                declare builtin type Color(
                  pub r: int
                ) {
                }

                declare host Gfx {
                  [Host(module = "gfx", name = "draw_pixel", version = 1)]
                  [Capability(name = "gfx")]
                  fn draw_pixel(x: int, y: int, color: Color) -> void;
                }
                """;
        final var semanticsDiagnostics = DiagnosticSink.empty();
        new PbsFrontendCompiler().compileFile(sourceId, source, semanticsDiagnostics, SourceKind.SDK_INTERFACE);
        assertFalse(semanticsDiagnostics.hasErrors());

        final var linkDiagnostics = DiagnosticSink.empty();
        final var sourceAst = PbsParser.parse(
                PbsLexer.lex(source, sourceId, linkDiagnostics),
                sourceId,
                linkDiagnostics,
                PbsParser.ParseMode.INTERFACE_MODULE);
        final var validBarrelAst = PbsBarrelParser.parse(
                PbsLexer.lex("pub struct Color;\npub host Gfx;\n", barrelId, linkDiagnostics),
                barrelId,
                linkDiagnostics);
        final var validModule = new PbsModuleVisibilityValidator.ModuleUnit(
                new PbsModuleVisibilityValidator.ModuleCoordinates("sdk", ReadOnlyList.wrap(List.of("gfx"))),
                ReadOnlyList.wrap(List.of(new PbsModuleVisibilityValidator.SourceFile(sourceId, sourceAst))),
                ReadOnlyList.wrap(List.of(new PbsModuleVisibilityValidator.BarrelFile(barrelId, validBarrelAst))));
        new PbsModuleVisibilityValidator().validate(ReadOnlyList.wrap(List.of(validModule)), linkDiagnostics);
        assertFalse(linkDiagnostics.stream().anyMatch(d ->
                d.getCode().equals(PbsLinkErrors.E_LINK_UNRESOLVED_BARREL_ENTRY.name())));

        final var negativeSemanticsDiagnostics = DiagnosticSink.empty();
        new PbsFrontendCompiler().compileFile(
                new FileId(32),
                "fn run() -> int { return 1; }",
                negativeSemanticsDiagnostics,
                SourceKind.SDK_INTERFACE);
        final var nonDeclarative = firstDiagnostic(negativeSemanticsDiagnostics,
                d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_INTERFACE_NON_DECLARATIVE_DECLARATION.name()));
        assertStableDiagnosticIdentity(nonDeclarative, PbsSemanticsErrors.E_SEM_INTERFACE_NON_DECLARATIVE_DECLARATION.name(), DiagnosticPhase.STATIC_SEMANTICS);

        final var negativeLinkDiagnostics = DiagnosticSink.empty();
        final var invalidBarrelAst = PbsBarrelParser.parse(
                PbsLexer.lex("pub host Missing;", new FileId(33), negativeLinkDiagnostics),
                new FileId(33),
                negativeLinkDiagnostics);
        final var invalidModule = new PbsModuleVisibilityValidator.ModuleUnit(
                new PbsModuleVisibilityValidator.ModuleCoordinates("sdk", ReadOnlyList.wrap(List.of("gfx"))),
                ReadOnlyList.wrap(List.of(new PbsModuleVisibilityValidator.SourceFile(sourceId, sourceAst))),
                ReadOnlyList.wrap(List.of(new PbsModuleVisibilityValidator.BarrelFile(new FileId(33), invalidBarrelAst))));
        new PbsModuleVisibilityValidator().validate(ReadOnlyList.wrap(List.of(invalidModule)), negativeLinkDiagnostics);
        final var unresolvedBarrel = firstDiagnostic(negativeLinkDiagnostics,
                d -> d.getCode().equals(PbsLinkErrors.E_LINK_UNRESOLVED_BARREL_ENTRY.name()));
        assertStableDiagnosticIdentity(unresolvedBarrel, PbsLinkErrors.E_LINK_UNRESOLVED_BARREL_ENTRY.name(), DiagnosticPhase.LINKING);
    }

    @Test
    void gateU_shouldExposeReservedMetadataAndRejectInvalidBuiltinLayoutAtSemantics() {
        final var validSource = """
                [BuiltinType(name = "Color", version = 1)]
                declare builtin type Color(
                  pub r: int,
                  pub g: int
                ) {
                  [IntrinsicCall(name = "core.color.pack", version = 1)]
                  fn pack() -> int;
                }

                declare host Gfx {
                  [Host(module = "gfx", name = "draw_pixel", version = 1)]
                  [Capability(name = "gfx")]
                  fn draw_pixel(x: int, y: int, color: Color) -> void;
                }

                [BuiltinConst(target = "Color", name = "white", version = 1)]
                declare const WHITE: Color;
                """;
        final var validDiagnostics = DiagnosticSink.empty();
        final var validBackend = new PbsFrontendCompiler().compileFile(
                new FileId(40),
                validSource,
                validDiagnostics,
                SourceKind.SDK_INTERFACE);
        assertFalse(validDiagnostics.hasErrors());
        assertEquals(1, validBackend.reservedMetadata().builtinTypeSurfaces().size());
        assertEquals(1, validBackend.reservedMetadata().hostMethodBindings().size());
        assertEquals(1, validBackend.reservedMetadata().builtinConstSurfaces().size());

        final var invalidSource = """
                declare struct RGBA(raw: int);
                [BuiltinType(name = "Color", version = 1)]
                declare builtin type Color(
                  pub raw: RGBA
                ) {
                }
                """;
        final var invalidDiagnostics = DiagnosticSink.empty();
        new PbsFrontendCompiler().compileFile(
                new FileId(41),
                invalidSource,
                invalidDiagnostics,
                SourceKind.SDK_INTERFACE);
        final var invalidLayout = firstDiagnostic(invalidDiagnostics,
                d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_BUILTIN_LAYOUT_FIELD_TYPE_NOT_ADMISSIBLE.name()));
        assertStableDiagnosticIdentity(
                invalidLayout,
                PbsSemanticsErrors.E_SEM_BUILTIN_LAYOUT_FIELD_TYPE_NOT_ADMISSIBLE.name(),
                DiagnosticPhase.STATIC_SEMANTICS);
    }

    @Test
    void gateU_shouldEmitHostAdmissionDiagnosticForMissingCapabilityMetadata() {
        final var source = """
                declare host Gfx {
                  [Host(module = "gfx", name = "draw_pixel", version = 1)]
                  fn draw_pixel(x: int, y: int) -> void;
                }
                """;
        final var diagnostics = DiagnosticSink.empty();

        new PbsFrontendCompiler().compileFile(
                new FileId(42),
                source,
                diagnostics,
                SourceKind.SDK_INTERFACE);

        final var hostAdmission = firstDiagnostic(diagnostics,
                d -> d.getCode().equals(PbsHostAdmissionErrors.E_HOST_MISSING_REQUIRED_CAPABILITY.name()));
        assertStableDiagnosticIdentity(
                hostAdmission,
                PbsHostAdmissionErrors.E_HOST_MISSING_REQUIRED_CAPABILITY.name(),
                DiagnosticPhase.HOST_ADMISSION);
    }

    @Test
    void gateU_shouldAcceptLowAssetsInterfaceSurfaceAndExposeCanonicalAssetBindings() {
        final var source = """
                declare host LowAssets {
                  [Host(module = "asset", name = "load", version = 1)]
                  [Capability(name = "asset")]
                  [AssetLowering(param = 0)]
                  fn load(addressable: Addressable, slot: int) -> (status: int, loading_handle: int);

                  [Host(module = "asset", name = "status", version = 1)]
                  [Capability(name = "asset")]
                  fn status(loading_handle: int) -> int;

                  [Host(module = "asset", name = "commit", version = 1)]
                  [Capability(name = "asset")]
                  fn commit(loading_handle: int) -> int;

                  [Host(module = "asset", name = "cancel", version = 1)]
                  [Capability(name = "asset")]
                  fn cancel(loading_handle: int) -> int;
                }
                """;

        final var diagnostics = DiagnosticSink.empty();
        final var backend = new PbsFrontendCompiler().compileFile(
                new FileId(43),
                source,
                diagnostics,
                SourceKind.SDK_INTERFACE);

        assertFalse(diagnostics.hasErrors(), diagnostics.stream().map(Object::toString).toList().toString());
        assertEquals(4, backend.reservedMetadata().hostMethodBindings().size());
        assertEquals(1, backend.reservedMetadata().requiredCapabilities().size());
        assertEquals("asset", backend.reservedMetadata().requiredCapabilities().getFirst());
        assertTrue(backend.reservedMetadata().hostMethodBindings().stream()
                .anyMatch(h -> h.ownerName().equals("LowAssets")
                        && java.util.Objects.equals(h.assetLoweringParam(), 0)
                        && h.abiModule().equals("asset")
                        && h.abiMethod().equals("load")
                        && h.abiVersion() == 1
                        && h.retSlots() == 2));
    }

    private WorkspaceCompileResult compileWorkspaceModule(
            final Path projectRoot,
            final String sourceContent,
            final String barrelContent,
            final int stdlibVersion,
            final Predicate<Diagnostic> awaitedDiagnostic) throws IOException {
        final var sourceRoot = projectRoot.resolve("src");
        final var modulePath = sourceRoot.resolve("app");
        Files.createDirectories(modulePath);

        final var sourceFile = modulePath.resolve("source.pbs");
        final var barrelFile = modulePath.resolve("mod.barrel");
        Files.writeString(sourceFile, sourceContent);
        Files.writeString(barrelFile, barrelContent);

        final var projectTable = new ProjectTable();
        final var fileTable = new FileTable(1);
        final var projectId = projectTable.register(ProjectDescriptor.builder()
                .rootPath(projectRoot)
                .name("app")
                .version("1.0.0")
                .sourceRoots(ReadOnlyList.wrap(List.of(sourceRoot)))
                .build());

        registerFile(projectId, projectRoot, sourceFile, fileTable);
        registerFile(projectId, projectRoot, barrelFile, fileTable);

        final var context = new FrontendPhaseContext(
                projectTable,
                fileTable,
                new BuildStack(ReadOnlyList.wrap(List.of(projectId))),
                stdlibVersion);
        final var diagnostics = DiagnosticSink.empty();

        final var irBackend = new PBSFrontendPhaseService().compile(
                context,
                diagnostics,
                LogAggregator.empty(),
                BuildingIssueSink.empty());

        if (awaitedDiagnostic != null) {
            assertTrue(diagnostics.stream().anyMatch(awaitedDiagnostic));
        }

        return new WorkspaceCompileResult(irBackend, diagnostics);
    }

    private void registerFile(
            final ProjectId projectId,
            final Path projectRoot,
            final Path file,
            final FileTable fileTable) throws IOException {
        final BasicFileAttributes attributes = Files.readAttributes(file, BasicFileAttributes.class);
        fileTable.register(new SourceHandle(
                projectId,
                projectRoot.relativize(file),
                file,
                attributes.size(),
                attributes.lastModifiedTime().toMillis(),
                SourceProviderFactory.filesystem()));
    }

    private Diagnostic firstDiagnostic(
            final DiagnosticSink diagnostics,
            final Predicate<Diagnostic> filter) {
        final Optional<Diagnostic> match = diagnostics.stream().filter(filter).findFirst();
        assertTrue(match.isPresent());
        return match.get();
    }

    private void assertStableDiagnosticIdentity(
            final Diagnostic diagnostic,
            final String code,
            final DiagnosticPhase phase) {
        assertNotNull(diagnostic);
        assertEquals(code, diagnostic.getCode());
        assertEquals(phase, diagnostic.getPhase());
        assertEquals(code, diagnostic.getTemplateId());
        assertNotNull(diagnostic.getSpan());
        assertNotNull(diagnostic.getSpan().getFileId());
    }

    private record WorkspaceCompileResult(
            IRBackend irBackend,
            DiagnosticSink diagnostics) {
    }
}
