package p.studio.compiler.workspaces.stages;

import org.junit.jupiter.api.Test;
import p.studio.compiler.messages.BuilderPipelineConfig;
import p.studio.compiler.models.BuilderPipelineContext;
import p.studio.compiler.models.IRBackend;
import p.studio.compiler.models.IRBackendExecutableFunction;
import p.studio.compiler.source.Span;
import p.studio.compiler.source.identifiers.CallableId;
import p.studio.compiler.source.identifiers.FileId;
import p.studio.utilities.logs.LogAggregator;
import p.studio.utilities.structures.ReadOnlyList;

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

class LowerToIRVMPipelineStageTest {

    @Test
    void runMustFailWhenIrBackendIsMissing() {
        final var ctx = BuilderPipelineContext.fromConfig(new BuilderPipelineConfig(false, "."));
        final var stage = new LowerToIRVMPipelineStage();

        final var issues = stage.run(ctx, LogAggregator.empty());
        final var firstIssue = issues.asCollection().iterator().next();

        assertTrue(issues.hasErrors());
        assertEquals(1, issues.size());
        assertEquals("BACKEND_LOWER_TO_IRVM", firstIssue.getPhase());
        assertEquals("MARSHAL_FORMAT_STAGE_INPUT_MISSING", firstIssue.getCode());
    }

    @Test
    void runMustLowerExecutableFunctionsToIrvm() {
        final var ctx = BuilderPipelineContext.fromConfig(new BuilderPipelineConfig(false, "."));
        ctx.irBackend = IRBackend.builder()
                .entryPointModuleId(new p.studio.compiler.source.identifiers.ModuleId(0))
                .executableFunctions(ReadOnlyList.from(new IRBackendExecutableFunction(
                        new FileId(0),
                        new p.studio.compiler.source.identifiers.ModuleId(0),
                        "main",
                        new CallableId(1),
                        0,
                        10,
                        0,
                        0,
                        0,
                        1,
                        ReadOnlyList.from(new IRBackendExecutableFunction.Instruction(
                                IRBackendExecutableFunction.InstructionKind.RET,
                                "",
                                null,
                                null,
                                Span.none())),
                        Span.none())))
                .build();
        final var stage = new LowerToIRVMPipelineStage();

        final var issues = stage.run(ctx, LogAggregator.empty());

        assertTrue(!issues.hasErrors());
        assertNotNull(ctx.irvm);
        assertEquals(1, ctx.irvm.module().functions().size());
    }

    @Test
    void runMustPropagateConfiguredVmProfileToLowering() {
        final var ctx = BuilderPipelineContext.fromConfig(new BuilderPipelineConfig(false, ".", "experimental-v1"));
        ctx.irBackend = IRBackend.builder()
                .entryPointModuleId(new p.studio.compiler.source.identifiers.ModuleId(0))
                .executableFunctions(ReadOnlyList.from(new IRBackendExecutableFunction(
                        new FileId(0),
                        new p.studio.compiler.source.identifiers.ModuleId(0),
                        "main",
                        new CallableId(1),
                        0,
                        10,
                        0,
                        0,
                        0,
                        1,
                        ReadOnlyList.from(new IRBackendExecutableFunction.Instruction(
                                IRBackendExecutableFunction.InstructionKind.RET,
                                "",
                                null,
                                null,
                                Span.none())),
                        Span.none())))
                .build();

        final var issues = new LowerToIRVMPipelineStage().run(ctx, LogAggregator.empty());

        assertFalse(issues.hasErrors());
        assertNotNull(ctx.irvm);
        assertEquals("experimental-v1", ctx.irvm.module().vmProfile());
    }

    @Test
    void runMustAttachSourceAttributionForLoweringFailure() {
        final var callSpan = new Span(new FileId(9), 12, 24);
        final var ctx = BuilderPipelineContext.fromConfig(new BuilderPipelineConfig(false, "."));
        ctx.irBackend = IRBackend.builder()
                .entryPointModuleId(new p.studio.compiler.source.identifiers.ModuleId(0))
                .executableFunctions(ReadOnlyList.from(new IRBackendExecutableFunction(
                        new FileId(9),
                        new p.studio.compiler.source.identifiers.ModuleId(0),
                        "main",
                        new CallableId(1),
                        10,
                        30,
                        0,
                        0,
                        0,
                        1,
                        ReadOnlyList.from(
                                new IRBackendExecutableFunction.Instruction(
                                        IRBackendExecutableFunction.InstructionKind.CALL_FUNC,
                                        p.studio.compiler.source.identifiers.ModuleId.none(),
                                        "missing",
                                        new CallableId(99),
                                        null,
                                        null,
                                        callSpan),
                                new IRBackendExecutableFunction.Instruction(
                                        IRBackendExecutableFunction.InstructionKind.RET,
                                        "",
                                        null,
                                        null,
                                        Span.none())),
                        new Span(new FileId(9), 10, 30))))
                .build();

        final var issues = new LowerToIRVMPipelineStage().run(ctx, LogAggregator.empty());
        final var firstIssue = issues.asCollection().iterator().next();

        assertTrue(issues.hasErrors());
        assertEquals("LOWER_IRVM_MISSING_CALLEE", firstIssue.getCode());
        assertEquals(9, firstIssue.getFileId());
        assertEquals(12, firstIssue.getStart());
        assertEquals(24, firstIssue.getEnd());
    }

    @Test
    void runMustRejectCallWhenStackDoesNotProvideDeclaredArgs() {
        final var ctx = BuilderPipelineContext.fromConfig(new BuilderPipelineConfig(false, "."));
        ctx.irBackend = IRBackend.builder()
                .entryPointModuleId(new p.studio.compiler.source.identifiers.ModuleId(0))
                .executableFunctions(ReadOnlyList.from(
                        new IRBackendExecutableFunction(
                                new FileId(0),
                                new p.studio.compiler.source.identifiers.ModuleId(0),
                                "callee",
                                new CallableId(2),
                                0,
                                10,
                                1,
                                0,
                                0,
                                1,
                                ReadOnlyList.from(new IRBackendExecutableFunction.Instruction(
                                        IRBackendExecutableFunction.InstructionKind.RET,
                                        "",
                                        null,
                                        null,
                                        Span.none())),
                                Span.none()),
                        new IRBackendExecutableFunction(
                                new FileId(0),
                                new p.studio.compiler.source.identifiers.ModuleId(0),
                                "main",
                                new CallableId(1),
                                0,
                                10,
                                0,
                                0,
                                0,
                                1,
                                ReadOnlyList.from(
                                        new IRBackendExecutableFunction.Instruction(
                                                IRBackendExecutableFunction.InstructionKind.CALL_FUNC,
                                                p.studio.compiler.source.identifiers.ModuleId.none(),
                                                "callee",
                                                new CallableId(2),
                                                null,
                                                null,
                                                1,
                                                0,
                                                Span.none()),
                                        new IRBackendExecutableFunction.Instruction(
                                                IRBackendExecutableFunction.InstructionKind.RET,
                                                "",
                                                null,
                                                null,
                                                Span.none())),
                                Span.none())))
                .build();

        final var issues = new LowerToIRVMPipelineStage().run(ctx, LogAggregator.empty());
        final var firstIssue = issues.asCollection().iterator().next();

        assertTrue(issues.hasErrors());
        assertEquals("MARSHAL_VERIFY_PRECHECK_STACK_UNDERFLOW", firstIssue.getCode());
    }
}
