package p.studio.compiler.integration;

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import p.studio.compiler.backend.bytecode.BytecodeEmitter;
import p.studio.compiler.backend.bytecode.BytecodeMarshalingErrorCode;
import p.studio.compiler.backend.bytecode.BytecodeMarshalingException;
import p.studio.compiler.backend.bytecode.BytecodeModule;
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.compiler.source.identifiers.IntrinsicId;
import p.studio.compiler.source.tables.IntrinsicReference;
import p.studio.compiler.workspaces.stages.EmitBytecodePipelineStage;
import p.studio.compiler.workspaces.stages.LowerToIRVMPipelineStage;
import p.studio.compiler.workspaces.stages.OptimizeIRVMPipelineStage;
import p.studio.utilities.logs.LogAggregator;
import p.studio.utilities.structures.ReadOnlyList;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.HashMap;
import java.util.Set;

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

@Disabled
class BackendGateIIntegrationTest {
    private final RuntimeCompatibilityAdapter compatibilityAdapter = RuntimeCompatibilityAdapters.createDefault();

    @Test
    void gateI_syscSectionPresentAndEmpty() {
        final var module = emitFromBackend(backendWithSingleFunction(
                fn("main", ReadOnlyList.from(ret()))));

        assertTrue(module.syscalls().isEmpty());
        assertEquals(CompatibilityError.NONE, compatibilityAdapter.check(module).error());
    }

    @Test
    void gateI_validHostcallPath() {
        final var module = emitFromBackend(backendWithSingleFunction(
                fn("main", ReadOnlyList.from(
                        callHost("gfx", "draw_pixel", 1, 0, 0),
                        ret()))));

        final var check = compatibilityAdapter.check(module);
        assertEquals(CompatibilityError.NONE, check.error());
        assertTrue(check.hostcallCount() > 0);
        assertTrue(Set.of("runtime-backed", "local-fallback", "local").contains(check.adapterMode()));
        assertTrue(!check.runtimeLine().isBlank());
        assertTrue(!check.reason().isBlank());
    }

    @Test
    void gateI_rejectHostcallOutOfBounds() {
        final var module = new BytecodeModule(
                0,
                ReadOnlyList.empty(),
                ReadOnlyList.from(new BytecodeModule.FunctionMeta(0, 8, 0, 0, 0, 1)),
                codeHostcall(1),
                null,
                ReadOnlyList.empty(),
                ReadOnlyList.from(new BytecodeModule.SyscallDecl("gfx", "draw_pixel", 1, 1, 0)));

        final var check = compatibilityAdapter.check(module);
        assertEquals(CompatibilityError.HOSTCALL_INDEX_OUT_OF_BOUNDS, check.error());
    }

    @Test
    void gateI_rejectUnusedSyscallDeclaration() {
        final var module = new BytecodeModule(
                0,
                ReadOnlyList.empty(),
                ReadOnlyList.from(new BytecodeModule.FunctionMeta(0, 2, 0, 0, 0, 1)),
                codeRet(),
                null,
                ReadOnlyList.empty(),
                ReadOnlyList.from(new BytecodeModule.SyscallDecl("gfx", "draw_pixel", 1, 1, 0)));

        final var check = compatibilityAdapter.check(module);
        assertEquals(CompatibilityError.UNUSED_SYSC_DECL, check.error());
    }

    @Test
    void gateI_rejectRawSyscallInPreloadArtifact() {
        final var module = new BytecodeModule(
                0,
                ReadOnlyList.empty(),
                ReadOnlyList.from(new BytecodeModule.FunctionMeta(0, 6, 0, 0, 0, 1)),
                codeRawSyscall(0x1001),
                null,
                ReadOnlyList.empty(),
                ReadOnlyList.empty());

        final var check = compatibilityAdapter.check(module);
        assertEquals(CompatibilityError.RAW_SYSCALL_IN_PRELOAD, check.error());
    }

    @Test
    void gateI_rejectHostAbiMismatchAtEmitTime() {
        final var emitter = new BytecodeEmitter();
        final var thrown = assertThrows(BytecodeMarshalingException.class, () -> emitter.emit(new BytecodeEmitter.EmissionPlan(
                0,
                ReadOnlyList.empty(),
                ReadOnlyList.empty(),
                ReadOnlyList.from(new BytecodeEmitter.FunctionPlan(
                        "main",
                        0,
                        0,
                        0,
                        1,
                        ReadOnlyList.from(
                                BytecodeEmitter.Operation.hostcall(
                                        new BytecodeModule.SyscallDecl("gfx", "draw_pixel", 1, 2, 0),
                                        1,
                                        0),
                                BytecodeEmitter.Operation.ret()))))));

        assertEquals(BytecodeMarshalingErrorCode.MARSHAL_LINKAGE_HOST_ABI_MISMATCH, thrown.code());
    }

    @Test
    void gateI_rejectMissingCapability() {
        final var module = emitFromBackend(backendWithSingleFunction(
                fn("main", ReadOnlyList.from(
                        callHost("gfx", "draw_pixel", 1, 0, 0),
                        ret()))));

        final var caps = Set.of("input");
        final var required = new HashMap<String, String>();
        required.put("gfx::draw_pixel::1", "gfx");
        final var check = new LocalRuntimeCompatibilityAdapter(caps, required).check(module);
        assertEquals(CompatibilityError.MISSING_CAPABILITY, check.error());
    }

    @Test
    void gateI_validIntrinsicPath() {
        final var module = emitFromBackend(backendWithSingleFunction(
                fn("main", ReadOnlyList.from(
                        callIntrinsic("input.pad", 1, 0),
                        ret())),
                ReadOnlyList.from(new IntrinsicReference("input.pad", 1))));

        final var check = compatibilityAdapter.check(module);
        assertEquals(CompatibilityError.NONE, check.error());
        assertTrue(module.syscalls().isEmpty());
    }

    private BytecodeModule emitFromBackend(final IRBackend backend) {
        final var ctx = BuilderPipelineContext.fromConfig(new BuilderPipelineConfig(false, "."));
        ctx.irBackend = backend;

        final var lowerIssues = new LowerToIRVMPipelineStage().run(ctx, LogAggregator.empty());
        assertFalse(lowerIssues.hasErrors(), "LowerToIRVM must succeed in fixture");

        final var optimizeIssues = new OptimizeIRVMPipelineStage().run(ctx, LogAggregator.empty());
        assertFalse(optimizeIssues.hasErrors(), "OptimizeIRVM must succeed in fixture");

        final var emitIssues = new EmitBytecodePipelineStage().run(ctx, LogAggregator.empty());
        assertFalse(emitIssues.hasErrors(), "EmitBytecode must succeed in fixture");
        assertNotNull(ctx.bytecodeModule);
        return ctx.bytecodeModule;
    }

    private IRBackend backendWithSingleFunction(final IRBackendExecutableFunction function) {
        return backendWithSingleFunction(function, ReadOnlyList.empty());
    }

    private IRBackend backendWithSingleFunction(
            final IRBackendExecutableFunction function,
            final ReadOnlyList<IntrinsicReference> intrinsicPool) {
        return IRBackend.builder()
                .entryPointModuleId(new p.studio.compiler.source.identifiers.ModuleId(0))
                .executableFunctions(ReadOnlyList.from(function))
                .intrinsicPool(intrinsicPool)
                .build();
    }

    private IRBackendExecutableFunction fn(
            final String name,
            final ReadOnlyList<IRBackendExecutableFunction.Instruction> instructions) {
        return new IRBackendExecutableFunction(
                new FileId(1),
                new p.studio.compiler.source.identifiers.ModuleId(0),
                name,
                new CallableId(1),
                0,
                10,
                0,
                0,
                0,
                4,
                instructions,
                Span.none());
    }

    private IRBackendExecutableFunction.Instruction ret() {
        return new IRBackendExecutableFunction.Instruction(
                IRBackendExecutableFunction.InstructionKind.RET,
                "",
                null,
                null,
                Span.none());
    }

    private IRBackendExecutableFunction.Instruction callHost(
            final String module,
            final String name,
            final long version,
            final int argSlots,
            final int retSlots) {
        return new IRBackendExecutableFunction.Instruction(
                IRBackendExecutableFunction.InstructionKind.CALL_HOST,
                "",
                new IRBackendExecutableFunction.HostCallMetadata(module, name, version, argSlots, retSlots),
                null,
                Span.none());
    }

    private IRBackendExecutableFunction.Instruction callIntrinsic(
            final String canonicalName,
            final long canonicalVersion,
            final int intrinsicId) {
        return new IRBackendExecutableFunction.Instruction(
                IRBackendExecutableFunction.InstructionKind.CALL_INTRINSIC,
                "",
                null,
                new IRBackendExecutableFunction.IntrinsicCallMetadata(canonicalName, canonicalVersion, new IntrinsicId(intrinsicId)),
                0,
                0,
                Span.none());
    }

    private byte[] codeHostcall(final int index) {
        final var out = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN);
        out.putShort((short) 0x71);
        out.putInt(index);
        out.putShort((short) 0x51);
        return out.array();
    }

    private byte[] codeRawSyscall(final int id) {
        final var out = ByteBuffer.allocate(6).order(ByteOrder.LITTLE_ENDIAN);
        out.putShort((short) 0x70);
        out.putInt(id);
        return out.array();
    }

    private byte[] codeRet() {
        final var out = ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN);
        out.putShort((short) 0x51);
        return out.array();
    }
}
