package p.studio.compiler.backend.bytecode;

import org.junit.jupiter.api.Test;
import p.studio.utilities.structures.ReadOnlyList;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;

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

class BytecodeLinkPrecheckServiceTest {

    private final BytecodeLinkPrecheckService service = new BytecodeLinkPrecheckService();

    @Test
    void validateMustAcceptWellFormedModule() {
        final var module = new BytecodeEmitter().emit(new BytecodeEmitter.EmissionPlan(
                0,
                ReadOnlyList.empty(),
                ReadOnlyList.from(new BytecodeModule.Export("main", 0)),
                ReadOnlyList.from(new BytecodeEmitter.FunctionPlan(
                        "main",
                        0,
                        0,
                        0,
                        1,
                        ReadOnlyList.from(BytecodeEmitter.Operation.ret())))));

        assertDoesNotThrow(() -> service.validate(module));
    }

    @Test
    void validateMustRejectInvalidExportFunctionIndex() {
        final var module = new BytecodeModule(
                0,
                ReadOnlyList.empty(),
                ReadOnlyList.from(new BytecodeModule.FunctionMeta(0, 2, 0, 0, 0, 1)),
                codeRet(),
                null,
                ReadOnlyList.from(new BytecodeModule.Export("main", 2)),
                ReadOnlyList.empty());

        final var thrown = assertThrows(BytecodeMarshalingException.class, () -> service.validate(module));
        assertEquals(BytecodeMarshalingErrorCode.MARSHAL_VERIFY_PRECHECK_EXPORT_FUNC_INDEX_INVALID, thrown.code());
    }

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

        final var thrown = assertThrows(BytecodeMarshalingException.class, () -> service.validate(module));
        assertEquals(BytecodeMarshalingErrorCode.MARSHAL_VERIFY_PRECHECK_HOSTCALL_INDEX_INVALID, thrown.code());
    }

    @Test
    void validateMustRejectUnknownOpcode() {
        final var module = new BytecodeModule(
                0,
                ReadOnlyList.empty(),
                ReadOnlyList.from(new BytecodeModule.FunctionMeta(0, 4, 0, 0, 0, 1)),
                codeUnknownThenRet(),
                null,
                ReadOnlyList.empty(),
                ReadOnlyList.empty());

        final var thrown = assertThrows(BytecodeMarshalingException.class, () -> service.validate(module));
        assertEquals(BytecodeMarshalingErrorCode.MARSHAL_VERIFY_PRECHECK_UNKNOWN_OPCODE, thrown.code());
    }

    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[] codeRet() {
        final var out = ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN);
        out.putShort((short) 0x51);
        return out.array();
    }

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