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 BytecodePreloadVerifierServiceTest {

    private final BytecodePreloadVerifierService service = new BytecodePreloadVerifierService();

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

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

    @Test
    void verifyMustRejectRawSyscallInPreload() {
        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 thrown = assertThrows(BytecodeMarshalingException.class, () -> service.verify(module));
        assertEquals(BytecodeMarshalingErrorCode.MARSHAL_LINKAGE_RAW_SYSCALL_IN_PRELOAD, thrown.code());
    }

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

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

    @Test
    void verifyMustRejectUnknownOpcode() {
        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.verify(module));
        assertEquals(BytecodeMarshalingErrorCode.MARSHAL_VERIFY_PRECHECK_UNKNOWN_OPCODE, thrown.code());
    }

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

    private byte[] codeRawSyscall(final int index) {
        final var out = ByteBuffer.allocate(6).order(ByteOrder.LITTLE_ENDIAN);
        out.putShort((short) 0x70);
        out.putInt(index);
        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();
    }
}
