package p.studio.compiler.backend.irvm;

import org.junit.jupiter.api.Test;
import p.studio.compiler.backend.bytecode.BytecodeEmitter;
import p.studio.compiler.backend.bytecode.BytecodeModule;
import p.studio.utilities.structures.ReadOnlyList;

import java.util.Map;
import java.util.Set;

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

class IRVMValidatorTest {
    private final IRVMValidator validator = new IRVMValidator();

    @Test
    void validateMustRejectInvalidJumpTarget() {
        final var module = new IRVMModule(
                "core-v1",
                ReadOnlyList.from(
                        new IRVMFunction(
                                "main",
                                0,
                                0,
                                0,
                                1,
                                ReadOnlyList.from(
                                        new IRVMInstruction(IRVMOp.JMP, 999)))));

        final var thrown = assertThrows(IRVMValidationException.class, () -> validator.validate(module, false));
        assertEquals(IRVMValidationErrorCode.MARSHAL_VERIFY_PRECHECK_INVALID_JUMP_TARGET, thrown.code());
    }

    @Test
    void validateMustRejectStackMismatchJoin() {
        final var module = new IRVMModule(
                "core-v1",
                ReadOnlyList.from(
                        new IRVMFunction(
                                "main",
                                0,
                                0,
                                0,
                                2,
                                ReadOnlyList.from(
                                        new IRVMInstruction(IRVMOp.PUSH_I32, 1),
                                        new IRVMInstruction(IRVMOp.JMP_IF_TRUE, 18),
                                        new IRVMInstruction(IRVMOp.PUSH_I32, 2),
                                        new IRVMInstruction(IRVMOp.RET, null)))));

        final var thrown = assertThrows(IRVMValidationException.class, () -> validator.validate(module, false));
        assertEquals(IRVMValidationErrorCode.MARSHAL_VERIFY_PRECHECK_STACK_MISMATCH_JOIN, thrown.code());
    }

    @Test
    void validateMustRejectRetShapeMismatch() {
        final var module = new IRVMModule(
                "core-v1",
                ReadOnlyList.from(
                        new IRVMFunction(
                                "main",
                                0,
                                0,
                                1,
                                1,
                                ReadOnlyList.from(
                                        new IRVMInstruction(IRVMOp.RET, null)))));

        final var thrown = assertThrows(IRVMValidationException.class, () -> validator.validate(module, false));
        assertEquals(IRVMValidationErrorCode.MARSHAL_VERIFY_PRECHECK_BAD_RET_STACK_HEIGHT, thrown.code());
    }

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

        final var thrown = assertThrows(IRVMValidationException.class, () -> validator.validate(module, true));
        assertEquals(IRVMValidationErrorCode.MARSHAL_VERIFY_PRECHECK_INTERNAL_OPCODE_RESIDUAL, thrown.code());
    }

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

        final var thrown = assertThrows(IRVMValidationException.class, () -> validator.validate(module, false));
        assertEquals(IRVMValidationErrorCode.MARSHAL_VERIFY_PRECHECK_OPCODE_NOT_ALLOWED_FOR_PROFILE, thrown.code());
    }

    @Test
    void validateMayAllowProfileSpecificOpcodeWhenProfileSupportsIt() {
        final var module = new IRVMModule(
                "experimental-v1",
                ReadOnlyList.from(
                        new IRVMFunction(
                                "main",
                                0,
                                0,
                                0,
                                1,
                                ReadOnlyList.from(
                                        new IRVMInstruction(IRVMOp.INTERNAL_EXT, null),
                                        new IRVMInstruction(IRVMOp.HALT, null)))));

        assertDoesNotThrow(() -> validator.validate(module, false));
    }

    @Test
    void validateMustApplyStructuralMetadataForCustomInternalExtension() {
        final var extPush = new IRVMOp("IRVM_EXT_PUSH", 0xFFFE, 0, 0, 1, false, false, true);
        final var customGate = new IRVMProfileFeatureGate(Map.of(
                "experimental-structural-v1", Set.of(extPush, IRVMOp.HALT)));
        final var customValidator = new IRVMValidator(customGate);
        final var module = new IRVMModule(
                "experimental-structural-v1",
                ReadOnlyList.from(new IRVMFunction(
                        "main",
                        0,
                        0,
                        0,
                        1,
                        ReadOnlyList.from(
                                new IRVMInstruction(extPush, null),
                                new IRVMInstruction(IRVMOp.HALT, null)))));

        assertDoesNotThrow(() -> customValidator.validate(module, false));
    }

    @Test
    void validateMustRejectCustomInternalExtensionWhenStructuralMetadataUnderflowsStack() {
        final var extPop = new IRVMOp("IRVM_EXT_POP", 0xFFFD, 0, 1, 0, false, false, true);
        final var customGate = new IRVMProfileFeatureGate(Map.of(
                "experimental-structural-v1", Set.of(extPop, IRVMOp.HALT)));
        final var customValidator = new IRVMValidator(customGate);
        final var module = new IRVMModule(
                "experimental-structural-v1",
                ReadOnlyList.from(new IRVMFunction(
                        "main",
                        0,
                        0,
                        0,
                        1,
                        ReadOnlyList.from(
                                new IRVMInstruction(extPop, null),
                                new IRVMInstruction(IRVMOp.HALT, null)))));

        final var thrown = assertThrows(IRVMValidationException.class, () -> customValidator.validate(module, false));
        assertEquals(IRVMValidationErrorCode.MARSHAL_VERIFY_PRECHECK_STACK_UNDERFLOW, thrown.code());
    }

    @Test
    void validateProgramMustRejectIntrinsicWithoutSignatureMetadata() {
        final var module = new IRVMModule(
                "core-v1",
                ReadOnlyList.from(new IRVMFunction(
                        "main",
                        0,
                        0,
                        0,
                        1,
                        ReadOnlyList.from(
                                new IRVMInstruction(IRVMOp.INTRINSIC, 0),
                                new IRVMInstruction(IRVMOp.RET, null)))));
        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.intrinsic(0, null),
                                BytecodeEmitter.Operation.ret()))));
        final var program = new IRVMProgram(module, plan);

        final var thrown = assertThrows(IRVMValidationException.class, () -> validator.validate(program, false));
        assertEquals(IRVMValidationErrorCode.MARSHAL_VERIFY_PRECHECK_INTRINSIC_SIGNATURE_MISSING, thrown.code());
    }

    @Test
    void validateProgramMustRejectHostcallWhenDeclaredArgsAreMissingOnStack() {
        final var module = new IRVMModule(
                "core-v1",
                ReadOnlyList.from(new IRVMFunction(
                        "main",
                        0,
                        0,
                        0,
                        1,
                        ReadOnlyList.from(
                                new IRVMInstruction(IRVMOp.HOSTCALL, 0),
                                new IRVMInstruction(IRVMOp.RET, null)))));
        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.hostcall(
                                        new BytecodeModule.SyscallDecl("gfx", "draw_pixel", 1, 1, 0),
                                        1,
                                        0),
                                BytecodeEmitter.Operation.ret()))));
        final var program = new IRVMProgram(module, plan);

        final var thrown = assertThrows(IRVMValidationException.class, () -> validator.validate(program, false));
        assertEquals(IRVMValidationErrorCode.MARSHAL_VERIFY_PRECHECK_STACK_UNDERFLOW, thrown.code());
    }
}
