package p.studio.compiler.pbs;

import org.junit.jupiter.api.Test;
import p.studio.compiler.pbs.lexer.LexErrors;
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.PbsBarrelParser;
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.diagnostics.Severity;
import p.studio.compiler.source.identifiers.FileId;
import p.studio.utilities.structures.ReadOnlyList;

import java.util.List;

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

class PbsDiagnosticsContractTest {

    @Test
    void shouldTagLexerDiagnosticsWithSyntaxPhase() {
        final var diagnostics = DiagnosticSink.empty();

        PbsLexer.lex("$", new FileId(0), diagnostics);

        final var diagnostic = diagnostics.stream()
                .filter(d -> d.getCode().equals(LexErrors.E_LEX_INVALID_CHAR.name()))
                .findFirst()
                .orElseThrow();
        assertEquals(DiagnosticPhase.SYNTAX, diagnostic.getPhase());
        assertEquals(diagnostic.getCode(), diagnostic.getTemplateId());
        assertTrue(diagnostic.getPlaceholders().isEmpty());
    }

    @Test
    void shouldTagSemanticsDiagnosticsWithStaticSemanticsPhase() {
        final var source = """
                fn sum(a: int) {}
                fn sum(a: int) {}
                """;
        final var diagnostics = DiagnosticSink.empty();

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

        final var diagnostic = diagnostics.stream()
                .filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_CALLABLE_SHAPE.name()))
                .findFirst()
                .orElseThrow();
        assertEquals(DiagnosticPhase.STATIC_SEMANTICS, diagnostic.getPhase());
    }

    @Test
    void shouldTagIgnoredValueWarningsWithStaticSemanticsPhase() {
        final var source = """
                fn pure() -> int { return 1; }
                fn frame() -> void {
                  pure();
                  return;
                }
                """;
        final var diagnostics = DiagnosticSink.empty();

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

        final var diagnostic = diagnostics.stream()
                .filter(d -> d.getCode().equals(PbsSemanticsErrors.W_SEM_IGNORED_VALUE.name()))
                .findFirst()
                .orElseThrow();
        assertEquals(DiagnosticPhase.STATIC_SEMANTICS, diagnostic.getPhase());
        assertEquals(Severity.Warning, diagnostic.getSeverity());
        assertEquals(diagnostic.getCode(), diagnostic.getTemplateId());
    }

    @Test
    void shouldTagLinkingDiagnosticsAndExposeRelatedSpanForDuplicateBarrelEntry() {
        final var diagnostics = DiagnosticSink.empty();
        final var sourceFileId = new FileId(1);
        final var barrelFileId = new FileId(2);
        final var source = """
                fn run() {}
                """;
        final var barrel = """
                pub fn run();
                pub fn run();
                """;
        final var sourceAst = PbsParser.parse(PbsLexer.lex(source, sourceFileId, diagnostics), sourceFileId, diagnostics);
        final var barrelAst = PbsBarrelParser.parse(PbsLexer.lex(barrel, barrelFileId, diagnostics), barrelFileId, diagnostics);
        final var module = new PbsModuleVisibilityValidator.ModuleUnit(
                new PbsModuleVisibilityValidator.ModuleCoordinates("app", ReadOnlyList.wrap(List.of("core"))),
                ReadOnlyList.wrap(List.of(new PbsModuleVisibilityValidator.SourceFile(sourceFileId, sourceAst))),
                ReadOnlyList.wrap(List.of(new PbsModuleVisibilityValidator.BarrelFile(barrelFileId, barrelAst))));

        new PbsModuleVisibilityValidator().validate(ReadOnlyList.wrap(List.of(module)), diagnostics);

        final var diagnostic = diagnostics.stream()
                .filter(d -> d.getCode().equals(PbsLinkErrors.E_LINK_DUPLICATE_BARREL_ENTRY.name()))
                .findFirst()
                .orElseThrow();
        assertEquals(DiagnosticPhase.LINKING, diagnostic.getPhase());
        assertEquals(1, diagnostic.getRelated().size());
    }

    @Test
    void shouldTagBuiltinLayoutDiagnosticsWithStaticSemanticsPhase() {
        final var source = """
                declare struct S(value: int);
                [BuiltinType(name = "Color", version = 1)]
                declare builtin type Color(
                  pub raw: S
                ) {
                }
                """;
        final var diagnostics = DiagnosticSink.empty();

        new PbsFrontendCompiler().compileFile(new FileId(9), source, diagnostics, p.studio.compiler.models.SourceKind.SDK_INTERFACE);

        final var diagnostic = diagnostics.stream()
                .filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_BUILTIN_LAYOUT_FIELD_TYPE_NOT_ADMISSIBLE.name()))
                .findFirst()
                .orElseThrow();
        assertEquals(DiagnosticPhase.STATIC_SEMANTICS, diagnostic.getPhase());
        assertEquals(diagnostic.getCode(), diagnostic.getTemplateId());
        assertEquals(9, diagnostic.getSpan().getFileId().getId());
    }

    @Test
    void shouldTagHostAdmissionDiagnosticsWithHostAdmissionPhase() {
        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(10),
                source,
                diagnostics,
                p.studio.compiler.models.SourceKind.SDK_INTERFACE);

        final var diagnostic = diagnostics.stream()
                .filter(d -> d.getCode().equals(PbsHostAdmissionErrors.E_HOST_MISSING_REQUIRED_CAPABILITY.name()))
                .findFirst()
                .orElseThrow();
        assertEquals(DiagnosticPhase.HOST_ADMISSION, diagnostic.getPhase());
        assertEquals(diagnostic.getCode(), diagnostic.getTemplateId());
        assertEquals(10, diagnostic.getSpan().getFileId().getId());
    }
}
