package p.studio.compiler.pbs.semantics;

import org.junit.jupiter.api.Test;
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.*;

class PbsSemanticsDeclarationsTest {

    @Test
    void shouldAllowFunctionOverloadsWithDifferentShapes() {
        final var source = """
                fn run(x: int) -> int { return x; }
                fn run(x: float) -> int { return 0; }
                """;
        final var diagnostics = DiagnosticSink.empty();

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

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

    @Test
    void shouldRejectDuplicateCallableShapeIgnoringLabels() {
        final var source = """
                fn run(a: int) -> (x: int, y: int) { return 0; }
                fn run(b: int) -> (left: int, right: int) { return 1; }
                """;
        final var diagnostics = DiagnosticSink.empty();

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

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

    @Test
    void shouldRejectDuplicateParametersAndReturnLabels() {
        final var source = """
                fn dup(a: int, a: int) -> (x: int, x: int) { return 0; }
                """;
        final var diagnostics = DiagnosticSink.empty();

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

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

    @Test
    void shouldRejectDuplicateEnumAndErrorCases() {
        final var source = """
                declare error Err { Fail; Fail; }
                declare enum Mode(Idle = 0, Idle = 1, Run = 1);
                """;
        final var diagnostics = DiagnosticSink.empty();

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

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

    @Test
    void shouldRejectInvalidReturnSurfaces() {
        final var source = """
                declare error Err { Fail; }
                fn badResult() -> result<Err> optional int { return ok(1); }
                fn badOptional(v: optional void) -> int { return 0; }
                """;
        final var diagnostics = DiagnosticSink.empty();

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

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

    @Test
    void shouldRejectConstWithoutInitializer() {
        final var source = "declare const LIMIT: int;";
        final var diagnostics = DiagnosticSink.empty();

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

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

    @Test
    void shouldRejectInvalidCtorServiceAndCallbackShapes() {
        final var source = """
                declare struct S(a: int) {
                  ctor make(x: int) { return; }
                  ctor make(y: int) { }
                }

                declare service Game {
                  fn run(a: int) -> int { return a; }
                  fn run(b: int) -> int { return b; }
                }

                declare callback Tick(a: int, a: int) -> int;
                """;
        final var diagnostics = DiagnosticSink.empty();

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

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

    @Test
    void shouldRejectDuplicateDeclarationNamesByNamespace() {
        final var source = """
                declare struct State();
                declare enum State(Idle);

                declare const Tick: int = 1;
                declare service Tick {
                  fn run() -> void { return; }
                }
                """;
        final var diagnostics = DiagnosticSink.empty();

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

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

    @Test
    void shouldRejectVisibleTopLevelCollisionsBetweenFunctionServiceGlobalAndConst() {
        final var source = """
                fn Tick() -> int { return 1; }
                declare service Tick {
                  fn run() -> void { return; }
                }
                declare global Score: int = 0;
                declare const Score: int = 1;
                """;
        final var diagnostics = DiagnosticSink.empty();

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

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

    @Test
    void shouldAcceptSupportedGlobalInitializerForms() {
        final var source = """
                declare struct Box(value: int);
                declare global BASE: int = 1;
                declare global CLONE: int = BASE + 1;
                declare global DEFAULT_BOX: Box = new Box(BASE);

                fn read() -> int {
                  return CLONE;
                }
                """;
        final var diagnostics = DiagnosticSink.empty();

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

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

    @Test
    void shouldRejectUnsupportedGlobalInitializerForms() {
        final var source = """
                fn inc(v: int) -> int { return v + 1; }
                declare global A: int = inc(1);
                declare global B: int = if true { 1 } else { 2 };
                declare global C: optional int = some(1);
                declare global D: optional int = none;
                """;
        final var diagnostics = DiagnosticSink.empty();

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

        final var invalidCount = diagnostics.stream()
                .filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_GLOBAL_UNSUPPORTED_INITIALIZER.name()))
                .count();
        assertEquals(4, invalidCount);
    }

    @Test
    void shouldRejectInvalidLifecycleSignaturesAndMultipleFileInitMarkers() {
        final var source = """
                [Init]
                fn boot(v: int) -> void { return; }

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

                [Frame]
                fn frame() { return; }
                """;
        final var diagnostics = DiagnosticSink.empty();

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

        final var lifecycleSignatureCount = diagnostics.stream()
                .filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_INVALID_LIFECYCLE_SIGNATURE.name()))
                .count();
        final var duplicateInitCount = diagnostics.stream()
                .filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_FILE_INIT.name()))
                .count();

        assertEquals(2, lifecycleSignatureCount);
        assertEquals(1, duplicateInitCount);
    }

    @Test
    void shouldRejectInitAllowedOutsideHostSignatures() {
        final var source = """
                [InitAllowed]
                declare const LIMIT: int = 1;
                """;
        final var diagnostics = DiagnosticSink.empty();

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

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

    @Test
    void shouldAllowSelfInStructServiceMethodsAndCtors() {
        final var source = """
                declare struct Box(v: int) {
                  fn merge(other: Self) -> Self { return this; }
                  ctor make(copy: Self) { this.v = copy.v; }
                }

                declare service Game {
                  fn mirror(current: Self) -> int { return 0; }
                }
                """;
        final var diagnostics = DiagnosticSink.empty();

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

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

    @Test
    void shouldRejectSelfOutsideAllowedDeclarationContexts() {
        final var source = """
                fn bad(v: Self) -> int { return 0; }
                declare const GLOBAL: Self = 1;
                """;
        final var diagnostics = DiagnosticSink.empty();

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

        final var invalidSelfCount = diagnostics.stream()
                .filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_INVALID_SELF_CONTEXT.name()))
                .count();
        assertEquals(2, invalidSelfCount);
    }
}
