package p.studio.compiler.workspaces.stages;

import org.junit.jupiter.api.Test;
import p.studio.compiler.backend.bytecode.BytecodeEmitter;
import p.studio.compiler.backend.irvm.*;
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 BackendSafetyGateSUTest {

    @Test
    void lowerStageMustExposeDeterministicFailureCodeForSameInvalidInput() {
        final var backend = IRBackend.builder()
                .entryPointModuleId(new p.studio.compiler.source.identifiers.ModuleId(0))
                .executableFunctions(ReadOnlyList.from(new IRBackendExecutableFunction(
                        new FileId(1),
                        new p.studio.compiler.source.identifiers.ModuleId(0),
                        "main",
                        new CallableId(1),
                        0,
                        10,
                        0,
                        0,
                        0,
                        2,
                        ReadOnlyList.from(
                                new IRBackendExecutableFunction.Instruction(
                                        IRBackendExecutableFunction.InstructionKind.CALL_FUNC,
                                        p.studio.compiler.source.identifiers.ModuleId.none(),
                                        "missing",
                                        new CallableId(99),
                                        null,
                                        null,
                                        0,
                                        0,
                                        Span.none()),
                                new IRBackendExecutableFunction.Instruction(
                                        IRBackendExecutableFunction.InstructionKind.RET,
                                        "",
                                        null,
                                        null,
                                        Span.none())),
                        Span.none())))
                .build();

        final var issueA = runLower(backend).asCollection().iterator().next();
        final var issueB = runLower(backend).asCollection().iterator().next();

        assertEquals("LOWER_IRVM_MISSING_CALLEE", issueA.getCode());
        assertEquals(issueA.getCode(), issueB.getCode());
        assertEquals(issueA.getPhase(), issueB.getPhase());
    }

    @Test
    void optimizeStageMustBeDeterministicForSameInputProgram() {
        final var program = new IRVMProgram(new IRVMModule(
                "core-v1",
                ReadOnlyList.from(new IRVMFunction(
                        "main",
                        0,
                        0,
                        0,
                        1,
                        ReadOnlyList.from(new IRVMInstruction(IRVMOp.HALT, null))))));

        final var first = runOptimize(program);
        final var second = runOptimize(program);

        assertTrue(!first.hasErrors());
        assertTrue(!second.hasErrors());
        assertEquals(first.size(), second.size());
    }

    @Test
    void emitStageMustExposeMarshalingLinkageFailureDeterministically() {
        final var plan = new BytecodeEmitter.EmissionPlan(
                0,
                ReadOnlyList.empty(),
                ReadOnlyList.empty(),
                ReadOnlyList.from(new BytecodeEmitter.FunctionPlan(
                        "main",
                        0,
                        0,
                        0,
                        1,
                        ReadOnlyList.from(BytecodeEmitter.Operation.rawSyscall(0x1001)))));

        final var issueA = runEmit(new IRVMProgram(false, plan)).asCollection().iterator().next();
        final var issueB = runEmit(new IRVMProgram(false, plan)).asCollection().iterator().next();

        assertEquals("MARSHAL_VERIFY_PRECHECK_IRVM_PROGRAM_COHERENCE", issueA.getCode());
        assertEquals(issueA.getCode(), issueB.getCode());
        assertEquals(issueA.getPhase(), issueB.getPhase());
    }

    @Test
    void fullPipelineMustProduceDeterministicBytecodeForSameInput() {
        final var backend = IRBackend.builder()
                .entryPointModuleId(new p.studio.compiler.source.identifiers.ModuleId(0))
                .executableFunctions(ReadOnlyList.from(new IRBackendExecutableFunction(
                        new FileId(1),
                        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 first = emitBytes(backend);
        final var second = emitBytes(backend);

        assertArrayEquals(first, second);
    }

    private static p.studio.compiler.messages.BuildingIssueSink runLower(final IRBackend backend) {
        final var ctx = BuilderPipelineContext.fromConfig(new BuilderPipelineConfig(false, "."));
        ctx.irBackend = backend;
        return new LowerToIRVMPipelineStage().run(ctx, LogAggregator.empty());
    }

    private static p.studio.compiler.messages.BuildingIssueSink runOptimize(final IRVMProgram program) {
        final var ctx = BuilderPipelineContext.fromConfig(new BuilderPipelineConfig(false, "."));
        ctx.irvm = program;
        return new OptimizeIRVMPipelineStage().run(ctx, LogAggregator.empty());
    }

    private static p.studio.compiler.messages.BuildingIssueSink runEmit(final IRVMProgram program) {
        final var ctx = BuilderPipelineContext.fromConfig(new BuilderPipelineConfig(false, "."));
        ctx.optimizedIrvm = program;
        return new EmitBytecodePipelineStage().run(ctx, LogAggregator.empty());
    }

    private static byte[] emitBytes(final IRBackend backend) {
        final var ctx = BuilderPipelineContext.fromConfig(new BuilderPipelineConfig(false, "."));
        ctx.irBackend = backend;

        final var lowerIssues = new LowerToIRVMPipelineStage().run(ctx, LogAggregator.empty());
        final var optimizeIssues = new OptimizeIRVMPipelineStage().run(ctx, LogAggregator.empty());
        final var emitIssues = new EmitBytecodePipelineStage().run(ctx, LogAggregator.empty());

        assertTrue(!lowerIssues.hasErrors());
        assertTrue(!optimizeIssues.hasErrors());
        assertTrue(!emitIssues.hasErrors());
        return ctx.bytecodeBytes;
    }
}
