package p.studio.compiler.pbs.semantics;

import org.junit.jupiter.api.Test;
import p.studio.compiler.models.SourceKind;
import p.studio.compiler.pbs.PbsFrontendCompiler;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.identifiers.FileId;

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

class PbsInterfaceModuleSemanticsTest {

    @Test
    void shouldAcceptValidInterfaceModuleReservedMetadataShapes() {
        final var source = """
                [BuiltinType(name = "color", version = 1)]
                declare builtin type Color(
                  pub raw: int
                ) {
                  [IntrinsicCall(name = "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, c: Color) -> void;
                }

                [BuiltinConst(target = "color", name = "white", version = 1)]
                declare const WHITE: Color;
                """;
        final var diagnostics = DiagnosticSink.empty();

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

        assertFalse(diagnostics.stream().anyMatch(d ->
                d.getCode().equals(PbsSemanticsErrors.E_SEM_MISSING_REQUIRED_RESERVED_ATTRIBUTE.name())));
        assertFalse(diagnostics.stream().anyMatch(d ->
                d.getCode().equals(PbsSemanticsErrors.E_SEM_INVALID_RESERVED_ATTRIBUTE_TARGET.name())));
        assertFalse(diagnostics.stream().anyMatch(d ->
                d.getCode().equals(PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name())));
    }

    @Test
    void shouldRejectInvalidReservedAttributeTargetsAndShapesInInterfaceModule() {
        final var source = """
                declare host Gfx {
                  fn draw_pixel(x: int, y: int) -> void;
                }

                declare contract Api {
                  [Host(module = "gfx", name = "draw_pixel", version = 1)]
                  [Capability(name = "gfx")]
                  fn run() -> void;
                }

                declare builtin type Color(pub raw: int) {
                  [IntrinsicCall(version = 1)]
                  fn pack() -> int;
                }

                [BuiltinConst(target = "", name = "white", version = 0)]
                declare const WHITE: int = 1;
                """;
        final var diagnostics = DiagnosticSink.empty();

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

        assertTrue(diagnostics.stream().anyMatch(d ->
                d.getCode().equals(PbsSemanticsErrors.E_SEM_MISSING_REQUIRED_RESERVED_ATTRIBUTE.name())));
        assertTrue(diagnostics.stream().anyMatch(d ->
                d.getCode().equals(PbsSemanticsErrors.E_SEM_INVALID_RESERVED_ATTRIBUTE_TARGET.name())));
        assertTrue(diagnostics.stream().anyMatch(d ->
                d.getCode().equals(PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name())));
    }

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

                declare contract C { fn run() -> int; }
                declare struct S(v: int);
                implements C for S using s {
                  fn run() -> int { return 1; }
                }
                """;
        final var diagnostics = DiagnosticSink.empty();

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

        final var nonDeclarativeCount = diagnostics.stream()
                .filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_INTERFACE_NON_DECLARATIVE_DECLARATION.name()))
                .count();
        assertTrue(nonDeclarativeCount >= 2);
    }

    @Test
    void shouldAllowDeclarativeServiceFacadesInInterfaceModule() {
        final var source = """
                declare host LowLog {
                  [Host(module = "log", name = "write", version = 1)]
                  [Capability(name = "log")]
                  fn write(level: int, message: str) -> void;
                }

                declare service Log {
                  fn trace(message: str) -> void {
                    LowLog.write(0, message);
                  }
                }
                """;
        final var diagnostics = DiagnosticSink.empty();

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

        assertFalse(diagnostics.stream().anyMatch(d ->
                d.getCode().equals(PbsSemanticsErrors.E_SEM_INTERFACE_NON_DECLARATIVE_DECLARATION.name())));
    }

    @Test
    void shouldAcceptLowAssetsHostSurfaceInInterfaceModule() {
        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;
                }
                """;
        final var diagnostics = DiagnosticSink.empty();

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

        assertTrue(diagnostics.isEmpty(), diagnostics.stream().map(d -> d.getCode() + ":" + d.getMessage()).toList().toString());
    }

    @Test
    void shouldRejectMalformedAssetLoweringOnHostSurface() {
        final var source = """
                declare host LowAssets {
                  [Host(module = "asset", name = "load", version = 1)]
                  [Capability(name = "asset")]
                  [AssetLowering(param = 1)]
                  fn load(addressable: Addressable) -> int;
                }
                """;
        final var diagnostics = DiagnosticSink.empty();

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

        assertTrue(diagnostics.stream().anyMatch(d ->
                d.getCode().equals(PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name())));
    }

    @Test
    void shouldRejectBuiltinFieldWithNonAdmissibleLayoutType() {
        final var source = """
                declare struct RGBA(raw: int);

                [BuiltinType(name = "Color", version = 1)]
                declare builtin type Color(
                  pub raw: RGBA
                ) {
                }
                """;
        final var diagnostics = DiagnosticSink.empty();

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

        assertTrue(diagnostics.stream().anyMatch(d ->
                d.getCode().equals(PbsSemanticsErrors.E_SEM_BUILTIN_LAYOUT_FIELD_TYPE_NOT_ADMISSIBLE.name())));
    }

    @Test
    void shouldRejectCyclicBuiltinLayoutComposition() {
        final var source = """
                [BuiltinType(name = "A", version = 1)]
                declare builtin type A(
                  pub b: B
                ) {
                }

                [BuiltinType(name = "B", version = 1)]
                declare builtin type B(
                  pub a: A
                ) {
                }
                """;
        final var diagnostics = DiagnosticSink.empty();

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

        assertTrue(diagnostics.stream().anyMatch(d ->
                d.getCode().equals(PbsSemanticsErrors.E_SEM_BUILTIN_LAYOUT_CYCLIC_COMPOSITION.name())));
    }

    @Test
    void shouldRejectBuiltinFieldWhenNestedBuiltinWidthIsUnresolvable() {
        final var source = """
                declare struct RGBA(raw: int);

                [BuiltinType(name = "Bad", version = 1)]
                declare builtin type Bad(
                  pub raw: RGBA
                ) {
                }

                [BuiltinType(name = "Pixel", version = 1)]
                declare builtin type Pixel(
                  pub bad: Bad
                ) {
                }
                """;
        final var diagnostics = DiagnosticSink.empty();

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

        assertTrue(diagnostics.stream().anyMatch(d ->
                d.getCode().equals(PbsSemanticsErrors.E_SEM_BUILTIN_LAYOUT_UNRESOLVABLE_WIDTH.name())));
    }
}
