package p.studio.compiler.pbs;

import org.junit.jupiter.api.Test;
import p.studio.compiler.messages.Addressable;
import p.studio.compiler.messages.FESurfaceContext;
import p.studio.compiler.messages.HostAdmissionContext;
import p.studio.compiler.models.IRHiddenGlobalKind;
import p.studio.compiler.models.IRGlobalVisibility;
import p.studio.compiler.models.IRSyntheticCallableKind;
import p.studio.compiler.models.SourceKind;
import p.studio.compiler.pbs.lexer.LexErrors;
import p.studio.compiler.pbs.lexer.PbsLexer;
import p.studio.compiler.pbs.parser.PbsParser;
import p.studio.compiler.pbs.semantics.PbsSemanticsErrors;
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.ModuleId;
import p.studio.compiler.source.tables.NameTable;
import p.studio.utilities.structures.ReadOnlyList;

import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

class PbsFrontendCompilerTest {

    @Test
    void shouldLowerFunctionsToIr() {
        final var source = """
                fn a() -> int {
                  return 1;
                }

                fn b(x: int) -> int {
                  return x;
                }
                """;

        final var diagnostics = DiagnosticSink.empty();
        final var compiler = new PbsFrontendCompiler();
        final var fileBackend = compiler.compileFile(new FileId(0), source, diagnostics);
        final var functions = fileBackend.functions();

        assertTrue(diagnostics.isEmpty(), "Valid program should not report diagnostics");
        assertEquals(2, functions.size());
        assertEquals("a", functions.get(0).name());
        assertEquals(0, functions.get(0).parameterCount());
        assertEquals("b", functions.get(1).name());
        assertEquals(1, functions.get(1).parameterCount());
    }

    @Test
    void shouldExposeGlobalsAndSyntheticLifecycleArtifactsInBackendFile() {
        final var source = """
                declare global SCORE: int = 0;

                [Init]
                fn boot() -> void { return; }

                [Frame]
                fn frame() -> void { return; }
                """;

        final var diagnostics = DiagnosticSink.empty();
        final var compiler = new PbsFrontendCompiler();
        final var fileBackend = compiler.compileFile(new FileId(1), source, diagnostics);

        assertTrue(diagnostics.isEmpty(), diagnostics.stream().map(d -> d.getCode() + ":" + d.getMessage()).toList().toString());
        assertEquals(1, fileBackend.globals().size());
        assertEquals("SCORE", fileBackend.globals().getFirst().name());
        assertEquals(0, fileBackend.globals().getFirst().slot());
        assertEquals(IRGlobalVisibility.MODULE, fileBackend.globals().getFirst().visibility());
        assertFalse(fileBackend.globals().getFirst().isHidden());
        assertEquals(IRHiddenGlobalKind.NONE, fileBackend.globals().getFirst().hiddenKind());
        assertEquals("SCORE", fileBackend.globals().getFirst().origin().derivedFromUserSymbol());
        assertEquals(4, fileBackend.syntheticFunctions().size());
        assertTrue(fileBackend.syntheticFunctions().stream().anyMatch(fn -> fn.kind() == IRSyntheticCallableKind.FILE_INIT_FRAGMENT));
        assertTrue(fileBackend.syntheticFunctions().stream().anyMatch(fn -> fn.kind() == IRSyntheticCallableKind.MODULE_INIT));
        assertTrue(fileBackend.syntheticFunctions().stream().anyMatch(fn -> fn.kind() == IRSyntheticCallableKind.PROJECT_INIT));
        assertTrue(fileBackend.syntheticFunctions().stream().anyMatch(fn -> fn.kind() == IRSyntheticCallableKind.PUBLISHED_FRAME_WRAPPER));
        assertTrue(fileBackend.syntheticFunctions().stream().allMatch(fn -> !fn.origin().anchorCallableName().isBlank()));
    }

    @Test
    void shouldReportDuplicateFunctionNames() {
        final var source = """
                fn a() -> int { return 1; }
                fn a() -> int { return 2; }
                """;

        final var diagnostics = DiagnosticSink.empty();
        final var compiler = new PbsFrontendCompiler();
        final var fileBackend = compiler.compileFile(new FileId(0), source, diagnostics);

        assertTrue(diagnostics.stream().anyMatch(d ->
                d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_CALLABLE_SHAPE.name())));
        assertEquals(0, fileBackend.functions().size());
    }

    @Test
    void shouldAllowOverloadedFunctionNamesWhenShapeDiffers() {
        final var source = """
                fn a(x: int) -> int { return x; }
                fn a(x: float) -> int { return 0; }
                """;

        final var diagnostics = DiagnosticSink.empty();
        final var compiler = new PbsFrontendCompiler();
        final var fileBackend = compiler.compileFile(new FileId(0), source, diagnostics);

        assertTrue(diagnostics.stream().noneMatch(d ->
                d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_CALLABLE_SHAPE.name())));
        assertEquals(2, fileBackend.functions().size());
    }

    @Test
    void shouldLowerExecutableFunctionsWithCallsiteCategories() {
        final var source = """
                fn b(x: int) -> int {
                  return x;
                }

                fn a() -> int {
                  return b(1);
                }
                """;

        final var diagnostics = DiagnosticSink.empty();
        final var compiler = new PbsFrontendCompiler();
        final var fileBackend = compiler.compileFile(new FileId(42), source, diagnostics);

        assertTrue(diagnostics.isEmpty(), "Valid program should not report diagnostics");
        assertEquals(2, fileBackend.executableFunctions().size());
        final var executableA = fileBackend.executableFunctions().stream()
                .filter(fn -> fn.callableName().equals("a"))
                .findFirst()
                .orElseThrow();
        assertTrue(executableA.instructions().stream().anyMatch(i ->
                i.kind() == p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.CALL_FUNC));
    }

    @Test
    void shouldLowerGlobalReadsAndWritesInExecutableBodies() {
        final var source = """
                declare global tile_id: int = 0;

                fn tick() -> int {
                  tile_id = tile_id + 1;
                  if (tile_id > 7) {
                    tile_id = 0;
                  }
                  return tile_id;
                }
                """;

        final var diagnostics = DiagnosticSink.empty();
        final var compiler = new PbsFrontendCompiler();
        final var fileBackend = compiler.compileFile(new FileId(46), source, diagnostics);

        assertTrue(diagnostics.isEmpty(), diagnostics.stream().map(d -> d.getCode() + ":" + d.getMessage()).toList().toString());
        final var executableTick = fileBackend.executableFunctions().stream()
                .filter(fn -> fn.callableName().equals("tick"))
                .findFirst()
                .orElseThrow();
        assertTrue(executableTick.instructions().stream().anyMatch(i ->
                i.kind() == p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.GET_GLOBAL));
        assertTrue(executableTick.instructions().stream().anyMatch(i ->
                i.kind() == p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.SET_GLOBAL));
    }

    @Test
    void shouldLowerUserConstIdentifiersInExecutableBodies() {
        final var source = """
                declare const MAX_FRAMES: int = 10;

                fn tick(frame_counter: int) -> bool {
                  return frame_counter > MAX_FRAMES;
                }
                """;

        final var diagnostics = DiagnosticSink.empty();
        final var compiler = new PbsFrontendCompiler();
        final var fileBackend = compiler.compileFile(new FileId(47), source, diagnostics);

        assertTrue(diagnostics.isEmpty(), diagnostics.stream().map(d -> d.getCode() + ":" + d.getMessage()).toList().toString());
        final var executableTick = fileBackend.executableFunctions().stream()
                .filter(fn -> fn.callableName().equals("tick"))
                .findFirst()
                .orElseThrow();
        assertTrue(executableTick.instructions().stream().anyMatch(i ->
                i.kind() == p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.PUSH_I32
                        && "10".equals(i.label())));
    }

    @Test
    void shouldClassifyCallableCallEvenWhenNameLooksHostLike() {
        final var source = """
                fn draw_pixel(x: int, y: int) -> int {
                  return x;
                }

                fn main() -> int {
                  return draw_pixel(1, 2);
                }
                """;

        final var diagnostics = DiagnosticSink.empty();
        final var compiler = new PbsFrontendCompiler();
        final var fileBackend = compiler.compileFile(new FileId(43), source, diagnostics);

        assertTrue(diagnostics.isEmpty(), "Valid program should not report diagnostics");
        final var executableMain = fileBackend.executableFunctions().stream()
                .filter(fn -> fn.callableName().equals("main"))
                .findFirst()
                .orElseThrow();
        final var callInstruction = executableMain.instructions().stream()
                .filter(i -> i.kind() == p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.CALL_FUNC)
                .findFirst()
                .orElseThrow();
        assertEquals("draw_pixel", callInstruction.calleeCallableName());
    }

    @Test
    void shouldNotInferCallableCategoryFromMemberCallText() {
        final var source = """
                fn draw_pixel(x: int, y: int) -> int {
                  return x;
                }

                fn main() -> int {
                  return gfx.draw_pixel(1, 2);
                }
                """;

        final var diagnostics = DiagnosticSink.empty();
        final var compiler = new PbsFrontendCompiler();
        final var fileBackend = compiler.compileFile(new FileId(44), source, diagnostics);

        assertTrue(fileBackend.executableFunctions().stream()
                .filter(fn -> fn.callableName().equals("main"))
                .findFirst()
                .map(fn -> fn.instructions().stream().noneMatch(i ->
                        i.kind() == p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.CALL_FUNC
                                || i.kind() == p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.CALL_HOST
                                || i.kind() == p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.CALL_INTRINSIC))
                .orElse(true));
    }

    @Test
    void shouldRejectAmbiguousCallableIdentityInsteadOfGuessingByText() {
        final var source = """
                fn draw_pixel(x: int) -> int {
                  return x;
                }

                fn draw_pixel(x: float) -> int {
                  return 0;
                }

                fn main() -> int {
                  return draw_pixel(1);
                }
                """;

        final var diagnostics = DiagnosticSink.empty();
        final var compiler = new PbsFrontendCompiler();
        final var fileBackend = compiler.compileFile(new FileId(45), source, diagnostics);

        final var loweringDiagnostic = diagnostics.stream()
                .filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_EXEC_LOWERING_UNRESOLVED_CALLEE.name()))
                .findFirst()
                .orElseThrow();
        assertEquals(DiagnosticPhase.STATIC_SEMANTICS, loweringDiagnostic.getPhase());
        assertTrue(loweringDiagnostic.getMessage().contains("ambiguous callable identity"));
        assertEquals(0, fileBackend.executableFunctions().size());
    }

    @Test
    void shouldLowerExecutableCallsitesInDeterministicSourceOrder() {
        final var source = """
                fn b() -> int { return 1; }
                fn c() -> int { return 2; }
                fn a() -> int {
                  b();
                  c();
                  return 0;
                }
                """;

        final var diagnostics = DiagnosticSink.empty();
        final var compiler = new PbsFrontendCompiler();
        final var fileBackend = compiler.compileFile(new FileId(99), source, diagnostics);

        assertEquals(0, diagnostics.errorCount(), "Valid program should not report errors");
        assertEquals(2, diagnostics.warningCount());
        final var executableA = fileBackend.executableFunctions().stream()
                .filter(fn -> fn.callableName().equals("a"))
                .findFirst()
                .orElseThrow();
        final var callNames = executableA.instructions().stream()
                .filter(i -> i.kind() == p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.CALL_FUNC)
                .map(p.studio.compiler.models.IRBackendExecutableFunction.Instruction::calleeCallableName)
                .toList();
        assertEquals(java.util.List.of("b", "c"), callNames);
    }

    @Test
    void shouldStopLoweringReachablePathAfterReturn() {
        final var source = """
                fn a() -> int { return 1; }
                fn b() -> int { return 2; }
                fn main() -> int {
                  return a();
                  b();
                }
                """;

        final var diagnostics = DiagnosticSink.empty();
        final var compiler = new PbsFrontendCompiler();
        final var fileBackend = compiler.compileFile(new FileId(100), source, diagnostics);

        assertEquals(0, diagnostics.errorCount(), "Valid program should not report errors");
        assertEquals(1, diagnostics.warningCount());
        final var executableMain = fileBackend.executableFunctions().stream()
                .filter(fn -> fn.callableName().equals("main"))
                .findFirst()
                .orElseThrow();
        assertEquals(2, executableMain.instructions().size());
        assertEquals("a", executableMain.instructions().get(0).calleeCallableName());
        assertEquals(p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.RET, executableMain.instructions().get(1).kind());
    }

    @Test
    void shouldEmitControlFlowInstructionsForIfAndWhile() {
        final var source = """
                fn cond() -> bool { return true; }
                fn t() -> int { return 1; }
                fn f() -> int { return 2; }
                fn loopBody() -> void { return; }
                fn main() -> int {
                  while cond() {
                    break;
                  }
                  if cond() {
                    return t();
                  } else {
                    return f();
                  }
                }
                """;

        final var diagnostics = DiagnosticSink.empty();
        final var compiler = new PbsFrontendCompiler();
        final var fileBackend = compiler.compileFile(new FileId(101), source, diagnostics);

        assertTrue(diagnostics.isEmpty(), "Valid program should not report diagnostics");
        final var executableMain = fileBackend.executableFunctions().stream()
                .filter(fn -> fn.callableName().equals("main"))
                .findFirst()
                .orElseThrow();
        assertTrue(executableMain.instructions().stream().anyMatch(i ->
                i.kind() == p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.LABEL));
        assertTrue(executableMain.instructions().stream().anyMatch(i ->
                i.kind() == p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.JMP));
        assertTrue(executableMain.instructions().stream().anyMatch(i ->
                i.kind() == p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.JMP_IF_FALSE));
    }

    @Test
    void shouldLowerForLoopIteratorBoundsAndStep() {
        final var source = """
                fn main() -> int {
                  let total: int = 0;
                  for i: int from 0 until 10 step 2 {
                    total += i;
                  }
                  return total;
                }
                """;

        final var diagnostics = DiagnosticSink.empty();
        final var compiler = new PbsFrontendCompiler();
        final var fileBackend = compiler.compileFile(new FileId(102), source, diagnostics);

        assertTrue(diagnostics.isEmpty(), diagnostics.stream().map(d -> d.getCode() + ":" + d.getMessage()).toList().toString());
        final var executableMain = fileBackend.executableFunctions().stream()
                .filter(fn -> fn.callableName().equals("main"))
                .findFirst()
                .orElseThrow();
        assertTrue(executableMain.instructions().stream().anyMatch(i ->
                i.kind() == p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.LT));
        assertTrue(executableMain.instructions().stream().anyMatch(i ->
                i.kind() == p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.ADD));
        assertTrue(executableMain.instructions().stream().anyMatch(i ->
                i.kind() == p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.SET_LOCAL));
        assertTrue(executableMain.instructions().stream().anyMatch(i ->
                i.kind() == p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.GET_LOCAL));
    }

    @Test
    void shouldNotLowerWhenSyntaxErrorsExist() {
        final var source = """
                $
                fn run() -> int { return 1; }
                """;

        final var diagnostics = DiagnosticSink.empty();
        final var compiler = new PbsFrontendCompiler();
        final var fileBackend = compiler.compileFile(new FileId(0), source, diagnostics);

        assertTrue(diagnostics.stream().anyMatch(d ->
                d.getCode().equals(LexErrors.E_LEX_INVALID_CHAR.name())));
        assertEquals(0, fileBackend.functions().size());
    }

    @Test
    void shouldNotLowerWhenStaticSemanticsErrorsExist() {
        final var source = """
                fn run(x: int) -> int { return x; }
                fn run(y: int) -> int { return y; }
                """;

        final var diagnostics = DiagnosticSink.empty();
        final var compiler = new PbsFrontendCompiler();
        final var fileBackend = compiler.compileFile(new FileId(0), source, diagnostics);

        assertTrue(diagnostics.stream().anyMatch(d ->
                d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_CALLABLE_SHAPE.name())));
        assertEquals(0, fileBackend.functions().size());
    }

    @Test
    void shouldExtractReservedMetadataForInterfaceModuleSurfaces() {
        final var source = """
                [BuiltinType(name = "Color", version = 1)]
                declare builtin type Color(
                  pub raw: 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 diagnostics = DiagnosticSink.empty();
        final var compiler = new PbsFrontendCompiler();
        final var fileBackend = compiler.compileFile(new FileId(0), source, diagnostics, SourceKind.SDK_INTERFACE);

        assertTrue(diagnostics.isEmpty());
        assertEquals(0, fileBackend.functions().size());
        assertEquals(1, fileBackend.reservedMetadata().hostMethodBindings().size());
        assertEquals(1, fileBackend.reservedMetadata().builtinTypeSurfaces().size());
        assertEquals(1, fileBackend.reservedMetadata().builtinConstSurfaces().size());
        assertEquals(1, fileBackend.reservedMetadata().requiredCapabilities().size());
        assertEquals("gfx", fileBackend.reservedMetadata().requiredCapabilities().getFirst());
        assertEquals("Color", fileBackend.reservedMetadata().builtinTypeSurfaces().getFirst().canonicalTypeName());
        assertEquals(1, fileBackend.reservedMetadata().builtinTypeSurfaces().getFirst().fields().size());
    }

    @Test
    void shouldExtractLowAssetsReservedMetadataForInterfaceModuleSurface() {
        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 compiler = new PbsFrontendCompiler();
        final var fileBackend = compiler.compileFile(new FileId(11), source, diagnostics, SourceKind.SDK_INTERFACE);

        assertTrue(diagnostics.isEmpty(), diagnostics.stream().map(d -> d.getCode() + ":" + d.getMessage()).toList().toString());
        assertEquals(0, fileBackend.functions().size());
        assertEquals(4, fileBackend.reservedMetadata().hostMethodBindings().size());
        assertEquals(1, fileBackend.reservedMetadata().requiredCapabilities().size());
        assertEquals("asset", fileBackend.reservedMetadata().requiredCapabilities().getFirst());
        assertTrue(fileBackend.reservedMetadata().hostMethodBindings().stream()
                .anyMatch(h -> h.ownerName().equals("LowAssets")
                        && h.abiModule().equals("asset")
                        && h.abiMethod().equals("load")
                        && h.abiVersion() == 1
                        && h.retSlots() == 2
                        && java.util.Objects.equals(h.assetLoweringParam(), 0)));
    }

    @Test
    void shouldLowerBackendOwnedAssetReferenceToAssetId() {
        final var source = """
                fn boot() -> Addressable {
                  return assets.ui.panel;
                }
                """;

        final var diagnostics = DiagnosticSink.empty();
        final var compiler = new PbsFrontendCompiler();
        final var fileBackend = compiler.compileFile(
                new FileId(12),
                source,
                diagnostics,
                SourceKind.PROJECT,
                HostAdmissionContext.permissiveDefault(),
                new FESurfaceContext(ReadOnlyList.from(new Addressable("assets.ui.panel", 37))));

        assertTrue(diagnostics.isEmpty(), diagnostics.stream().map(d -> d.getCode() + ":" + d.getMessage()).toList().toString());
        final var executableBoot = fileBackend.executableFunctions().stream()
                .filter(fn -> fn.callableName().equals("boot"))
                .findFirst()
                .orElseThrow();
        assertTrue(executableBoot.instructions().stream().anyMatch(i ->
                i.kind() == p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.PUSH_I32
                        && "37".equals(i.label())));
    }

    @Test
    void shouldRewriteAssetLoweringHostParameterToAssetId() {
        final var interfaceSource = """
                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);
                }
                """;
        final var source = """
                fn boot() -> (status: int, loading_handle: int) {
                  return LowAssets.load(assets.ui.panel, 2);
                }
                """;

        final var diagnostics = DiagnosticSink.empty();
        final var compiler = new PbsFrontendCompiler();
        final var interfaceBackend = compiler.compileFile(new FileId(16), interfaceSource, diagnostics, SourceKind.SDK_INTERFACE);
        final var sourceFileId = new FileId(17);
        final var sourceTokens = PbsLexer.lex(source, sourceFileId, diagnostics);
        final var sourceAst = PbsParser.parse(sourceTokens, sourceFileId, diagnostics);
        final var interfaceTokens = PbsLexer.lex(interfaceSource, new FileId(18), diagnostics);
        final var interfaceAst = PbsParser.parse(interfaceTokens, new FileId(18), diagnostics, PbsParser.ParseMode.INTERFACE_MODULE);
        final var fileBackend = compiler.compileParsedFile(
                sourceFileId,
                sourceAst,
                diagnostics,
                SourceKind.PROJECT,
                ModuleId.none(),
                ReadOnlyList.empty(),
                HostAdmissionContext.permissiveDefault(),
                new FESurfaceContext(ReadOnlyList.from(new Addressable("assets.ui.panel", 55))),
                new NameTable(),
                interfaceAst.topDecls(),
                ReadOnlyList.empty(),
                ReadOnlyList.empty(),
                interfaceBackend.reservedMetadata(),
                Map.of());

        assertEquals(0, diagnostics.errorCount(), diagnostics.stream().map(d -> d.getCode() + ":" + d.getMessage()).toList().toString());
        final var executableBoot = fileBackend.executableFunctions().stream()
                .filter(fn -> fn.callableName().equals("boot"))
                .findFirst()
                .orElseThrow();
        assertTrue(executableBoot.instructions().stream().anyMatch(i ->
                i.kind() == p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.PUSH_I32
                        && "55".equals(i.label())));
        assertTrue(executableBoot.instructions().stream().anyMatch(i ->
                i.kind() == p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.CALL_HOST
                        && i.hostCall() != null
                        && "asset".equals(i.hostCall().module())
                        && "load".equals(i.hostCall().name())));
    }

    @Test
    void shouldRejectUnresolvedBackendOwnedAssetReference() {
        final var source = """
                fn boot() -> Addressable {
                  return assets.ui.missing;
                }
                """;

        final var diagnostics = DiagnosticSink.empty();
        final var compiler = new PbsFrontendCompiler();
        final var fileBackend = compiler.compileFile(
                new FileId(13),
                source,
                diagnostics,
                SourceKind.PROJECT,
                HostAdmissionContext.permissiveDefault(),
                new FESurfaceContext(ReadOnlyList.from(new Addressable("assets.ui.panel", 37))));

        assertTrue(diagnostics.stream().anyMatch(d ->
                d.getCode().equals(PbsSemanticsErrors.E_SEM_INVALID_MEMBER_ACCESS.name())
                        && d.getMessage().contains("assets.ui.missing")));
        assertEquals(0, fileBackend.executableFunctions().size());
    }

    @Test
    void shouldWarnAndDiscardIgnoredMaterializedExpressionStatements() {
        final var source = """
                fn pure() -> int { return 1; }

                fn frame() -> void {
                  pure();
                  1 + 2;
                  return;
                }
                """;

        final var diagnostics = DiagnosticSink.empty();
        final var compiler = new PbsFrontendCompiler();
        final var fileBackend = compiler.compileFile(new FileId(14), source, diagnostics);

        assertEquals(2, diagnostics.warningCount());
        assertTrue(diagnostics.stream().anyMatch(d ->
                d.getCode().equals(PbsSemanticsErrors.W_SEM_IGNORED_VALUE.name())));
        final var executableFrame = fileBackend.executableFunctions().stream()
                .filter(fn -> fn.callableName().equals("frame"))
                .findFirst()
                .orElseThrow();
        assertTrue(executableFrame.instructions().stream().anyMatch(i ->
                i.kind() == p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.CALL_FUNC));
        assertEquals(2, executableFrame.instructions().stream()
                .filter(i -> i.kind() == p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.POP)
                .count());
    }

    @Test
    void shouldNotWarnOrDiscardUnitExpressionStatements() {
        final var source = """
                fn log() -> void { return; }

                fn frame() -> void {
                  log();
                  return;
                }
                """;

        final var diagnostics = DiagnosticSink.empty();
        final var compiler = new PbsFrontendCompiler();
        final var fileBackend = compiler.compileFile(new FileId(15), source, diagnostics);

        assertEquals(0, diagnostics.warningCount());
        final var executableFrame = fileBackend.executableFunctions().stream()
                .filter(fn -> fn.callableName().equals("frame"))
                .findFirst()
                .orElseThrow();
        assertEquals(0, executableFrame.instructions().stream()
                .filter(i -> i.kind() == p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.POP)
                .count());
    }

    @Test
    void shouldRejectHostBindingWithoutCapabilityAtHostAdmission() {
        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();
        final var compiler = new PbsFrontendCompiler();
        final var fileBackend = compiler.compileFile(new FileId(7), source, diagnostics, SourceKind.SDK_INTERFACE);

        final var hostDiagnostic = diagnostics.stream()
                .filter(d -> d.getCode().equals(PbsHostAdmissionErrors.E_HOST_MISSING_REQUIRED_CAPABILITY.name()))
                .findFirst()
                .orElseThrow();
        assertEquals(DiagnosticPhase.HOST_ADMISSION, hostDiagnostic.getPhase());
        assertEquals(hostDiagnostic.getCode(), hostDiagnostic.getTemplateId());
        assertEquals(0, fileBackend.functions().size());
    }

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

        final var diagnostics = DiagnosticSink.empty();
        final var compiler = new PbsFrontendCompiler();
        compiler.compileFile(
                new FileId(8),
                source,
                diagnostics,
                SourceKind.SDK_INTERFACE,
                HostAdmissionContext.strictWithDeclaredCapabilities(ReadOnlyList.wrap(java.util.List.of("input"))));

        final var hostDiagnostic = diagnostics.stream()
                .filter(d -> d.getCode().equals(PbsHostAdmissionErrors.E_HOST_CAPABILITY_NOT_DECLARED.name()))
                .findFirst()
                .orElseThrow();
        assertEquals(DiagnosticPhase.HOST_ADMISSION, hostDiagnostic.getPhase());
    }

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

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

        final var hostDiagnostic = diagnostics.stream()
                .filter(d -> d.getCode().equals(PbsHostAdmissionErrors.E_HOST_UNKNOWN_CAPABILITY.name()))
                .findFirst()
                .orElseThrow();
        assertEquals(DiagnosticPhase.HOST_ADMISSION, hostDiagnostic.getPhase());
    }

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

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

        final var hostDiagnostic = diagnostics.stream()
                .filter(d -> d.getCode().equals(PbsHostAdmissionErrors.E_HOST_MALFORMED_CAPABILITY_METADATA.name()))
                .findFirst()
                .orElseThrow();
        assertEquals(DiagnosticPhase.HOST_ADMISSION, hostDiagnostic.getPhase());
    }

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

        final var diagnostics = DiagnosticSink.empty();
        final var compiler = new PbsFrontendCompiler();
        final var fileBackend = compiler.compileFile(new FileId(3), source, diagnostics, SourceKind.SDK_INTERFACE);

        assertTrue(diagnostics.isEmpty());
        final var fields = fileBackend.reservedMetadata().builtinTypeSurfaces().getFirst().fields();
        assertEquals(2, fields.size());
        assertEquals(0L, fields.get(0).slotStart());
        assertEquals(1L, fields.get(1).slotStart());
        assertEquals(0, fileBackend.functions().size());
    }

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

                [BuiltinType(name = "Pixel", version = 1)]
                declare builtin type Pixel(
                  pub x: int,
                  pub y: int,
                  pub color: Color
                ) {
                }
                """;

        final var diagnostics = DiagnosticSink.empty();
        final var compiler = new PbsFrontendCompiler();
        final var fileBackend = compiler.compileFile(new FileId(5), source, diagnostics, SourceKind.SDK_INTERFACE);

        assertTrue(diagnostics.isEmpty());
        final var pixel = fileBackend.reservedMetadata().builtinTypeSurfaces().stream()
                .filter(t -> t.sourceTypeName().equals("Pixel"))
                .findFirst()
                .orElseThrow();
        assertEquals(3, pixel.fields().size());
        assertEquals(0L, pixel.fields().get(0).slotStart());
        assertEquals(1L, pixel.fields().get(1).slotStart());
        assertEquals(2L, pixel.fields().get(2).slotStart());
    }
}
