/*
 * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
 * or more contributor license agreements. Licensed under the "Elastic License
 * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
 * Public License v 1"; you may not use this file except in compliance with, at
 * your election, the "Elastic License 2.0", the "GNU Affero General Public
 * License v3.0 only", or the "Server Side Public License, v 1".
 */

package org.elasticsearch.painless;

import org.elasticsearch.painless.spi.Whitelist;
import org.elasticsearch.script.ScriptContext;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static java.util.Collections.emptyMap;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.startsWith;

/**
 * Tests for Painless implementing different interfaces.
 */
public class BaseClassTests extends ScriptTestCase {

    protected Map<ScriptContext<?>, List<Whitelist>> scriptContexts() {
        Map<ScriptContext<?>, List<Whitelist>> contexts = new HashMap<>();
        contexts.put(Gets.CONTEXT, PAINLESS_BASE_WHITELIST);
        contexts.put(NoArgs.CONTEXT, PAINLESS_BASE_WHITELIST);
        contexts.put(OneArg.CONTEXT, PAINLESS_BASE_WHITELIST);
        contexts.put(ArrayArg.CONTEXT, PAINLESS_BASE_WHITELIST);
        contexts.put(PrimitiveArrayArg.CONTEXT, PAINLESS_BASE_WHITELIST);
        contexts.put(DefArrayArg.CONTEXT, PAINLESS_BASE_WHITELIST);
        contexts.put(ManyArgs.CONTEXT, PAINLESS_BASE_WHITELIST);
        contexts.put(VarArgs.CONTEXT, PAINLESS_BASE_WHITELIST);
        contexts.put(DefaultMethods.CONTEXT, PAINLESS_BASE_WHITELIST);
        contexts.put(ReturnsVoid.CONTEXT, PAINLESS_BASE_WHITELIST);
        contexts.put(ReturnsPrimitiveBoolean.CONTEXT, PAINLESS_BASE_WHITELIST);
        contexts.put(ReturnsPrimitiveInt.CONTEXT, PAINLESS_BASE_WHITELIST);
        contexts.put(ReturnsPrimitiveFloat.CONTEXT, PAINLESS_BASE_WHITELIST);
        contexts.put(ReturnsPrimitiveDouble.CONTEXT, PAINLESS_BASE_WHITELIST);
        contexts.put(NoArgsConstant.CONTEXT, PAINLESS_BASE_WHITELIST);
        contexts.put(WrongArgsConstant.CONTEXT, PAINLESS_BASE_WHITELIST);
        contexts.put(WrongLengthOfArgConstant.CONTEXT, PAINLESS_BASE_WHITELIST);
        contexts.put(UnknownArgType.CONTEXT, PAINLESS_BASE_WHITELIST);
        contexts.put(UnknownReturnType.CONTEXT, PAINLESS_BASE_WHITELIST);
        contexts.put(UnknownArgTypeInArray.CONTEXT, PAINLESS_BASE_WHITELIST);
        contexts.put(TwoExecuteMethods.CONTEXT, PAINLESS_BASE_WHITELIST);
        return contexts;
    }

    public abstract static class Gets {

        public interface Factory {
            Gets newInstance(String testString, int testInt, Map<String, Object> params);
        }

        public static final ScriptContext<Factory> CONTEXT = new ScriptContext<>("gets", Factory.class);

        private final String testString;
        private final int testInt;
        private final Map<String, Object> testMap;

        public Gets(String testString, int testInt, Map<String, Object> testMap) {
            this.testString = testString;
            this.testInt = testInt;
            this.testMap = testMap;
        }

        public static final String[] PARAMETERS = new String[] {};

        public abstract Object execute();

        public String getTestString() {
            return testString;
        }

        public int getTestInt() {
            return Math.abs(testInt);
        }

        public Map<String, Object> getTestMap() {
            return testMap == null ? new HashMap<>() : testMap;
        }
    }

    public void testGets() throws Exception {
        Map<String, Object> map = new HashMap<>();
        map.put("s", 1);

        assertEquals(1, scriptEngine.compile("testGets0", "testInt", Gets.CONTEXT, emptyMap()).newInstance("s", -1, null).execute());
        assertEquals(
            Collections.emptyMap(),
            scriptEngine.compile("testGets1", "testMap", Gets.CONTEXT, emptyMap()).newInstance("s", -1, null).execute()
        );
        assertEquals(
            Collections.singletonMap("1", "1"),
            scriptEngine.compile("testGets2", "testMap", Gets.CONTEXT, emptyMap())
                .newInstance("s", -1, Collections.singletonMap("1", "1"))
                .execute()
        );
        assertEquals("s", scriptEngine.compile("testGets3", "testString", Gets.CONTEXT, emptyMap()).newInstance("s", -1, null).execute());
        assertEquals(
            map,
            scriptEngine.compile("testGets4", "testMap.put(testString, testInt); testMap", Gets.CONTEXT, emptyMap())
                .newInstance("s", -1, null)
                .execute()
        );
    }

    public abstract static class NoArgs {
        public interface Factory {
            NoArgs newInstance();
        }

        public static final ScriptContext<Factory> CONTEXT = new ScriptContext<>("noargs", Factory.class);

        public static final String[] PARAMETERS = new String[] {};

        public abstract Object execute();
    }

    public void testNoArgs() throws Exception {
        assertEquals(1, scriptEngine.compile("testNoArgs0", "1", NoArgs.CONTEXT, emptyMap()).newInstance().execute());
        assertEquals("foo", scriptEngine.compile("testNoArgs1", "'foo'", NoArgs.CONTEXT, emptyMap()).newInstance().execute());

        Exception e = expectScriptThrows(
            IllegalArgumentException.class,
            () -> scriptEngine.compile("testNoArgs2", "doc", NoArgs.CONTEXT, emptyMap())
        );
        assertEquals("cannot resolve symbol [doc]", e.getMessage());
        e = expectScriptThrows(
            IllegalArgumentException.class,
            () -> scriptEngine.compile("testNoArgs3", "_score", NoArgs.CONTEXT, emptyMap())
        );
        assertEquals("cannot resolve symbol [_score]", e.getMessage());

        String debug = Debugger.toString(NoArgs.class, "int i = 0", new CompilerSettings(), PAINLESS_BASE_WHITELIST);
        assertThat(debug, containsString("ACONST_NULL"));
        assertThat(debug, containsString("ARETURN"));
    }

    public abstract static class OneArg {
        public interface Factory {
            OneArg newInstance();
        }

        public static final ScriptContext<Factory> CONTEXT = new ScriptContext<>("onearg", Factory.class);

        public static final String[] PARAMETERS = new String[] { "arg" };

        public abstract Object execute(Object arg);
    }

    public void testOneArg() throws Exception {
        Object rando = randomInt();
        assertEquals(rando, scriptEngine.compile("testOneArg0", "arg", OneArg.CONTEXT, emptyMap()).newInstance().execute(rando));
        rando = randomAlphaOfLength(5);
        assertEquals(rando, scriptEngine.compile("testOneArg1", "arg", OneArg.CONTEXT, emptyMap()).newInstance().execute(rando));
    }

    public abstract static class ArrayArg {
        public interface Factory {
            ArrayArg newInstance();
        }

        public static final ScriptContext<Factory> CONTEXT = new ScriptContext<>("arrayarg", Factory.class);

        public static final String[] PARAMETERS = new String[] { "arg" };

        public abstract Object execute(String[] arg);
    }

    public void testArrayArg() throws Exception {
        String rando = randomAlphaOfLength(5);
        assertEquals(
            rando,
            scriptEngine.compile("testArrayArg0", "arg[0]", ArrayArg.CONTEXT, emptyMap())
                .newInstance()
                .execute(new String[] { rando, "foo" })
        );
    }

    public abstract static class PrimitiveArrayArg {
        public interface Factory {
            PrimitiveArrayArg newInstance();
        }

        public static final ScriptContext<Factory> CONTEXT = new ScriptContext<>("primitivearrayarg", Factory.class);

        public static final String[] PARAMETERS = new String[] { "arg" };

        public abstract Object execute(int[] arg);
    }

    public void testPrimitiveArrayArg() throws Exception {
        int rando = randomInt();
        assertEquals(
            rando,
            scriptEngine.compile("PrimitiveArrayArg0", "arg[0]", PrimitiveArrayArg.CONTEXT, emptyMap())
                .newInstance()
                .execute(new int[] { rando, 10 })
        );
    }

    public abstract static class DefArrayArg {
        public interface Factory {
            DefArrayArg newInstance();
        }

        public static final ScriptContext<Factory> CONTEXT = new ScriptContext<>("defarrayarg", Factory.class);

        public static final String[] PARAMETERS = new String[] { "arg" };

        public abstract Object execute(Object[] arg);
    }

    public void testDefArrayArg() throws Exception {
        Object rando = randomInt();
        assertEquals(
            rando,
            scriptEngine.compile("testDefArray0", "arg[0]", DefArrayArg.CONTEXT, emptyMap())
                .newInstance()
                .execute(new Object[] { rando, 10 })
        );
        rando = randomAlphaOfLength(5);
        assertEquals(
            rando,
            scriptEngine.compile("testDefArray1", "arg[0]", DefArrayArg.CONTEXT, emptyMap())
                .newInstance()
                .execute(new Object[] { rando, 10 })
        );
        assertEquals(
            5,
            scriptEngine.compile("testDefArray2", "arg[0].length()", DefArrayArg.CONTEXT, emptyMap())
                .newInstance()
                .execute(new Object[] { rando, 10 })
        );
    }

    public abstract static class ManyArgs {
        public interface Factory {
            ManyArgs newInstance();
        }

        public static final ScriptContext<Factory> CONTEXT = new ScriptContext<>("manyargs", Factory.class);

        public static final String[] PARAMETERS = new String[] { "a", "b", "c", "d" };

        public abstract Object execute(int a, int b, int c, int d);

        public abstract boolean needsA();

        public abstract boolean needsB();

        public abstract boolean needsC();

        public abstract boolean needsD();
    }

    public void testManyArgs() throws Exception {
        int rando = randomInt();
        assertEquals(rando, scriptEngine.compile("testManyArgs0", "a", ManyArgs.CONTEXT, emptyMap()).newInstance().execute(rando, 0, 0, 0));
        assertEquals(
            10,
            scriptEngine.compile("testManyArgs1", "a + b + c + d", ManyArgs.CONTEXT, emptyMap()).newInstance().execute(1, 2, 3, 4)
        );

        // While we're here we can verify that painless correctly finds used variables
        ManyArgs script = scriptEngine.compile("testManyArgs2", "a", ManyArgs.CONTEXT, emptyMap()).newInstance();
        assertTrue(script.needsA());
        assertFalse(script.needsB());
        assertFalse(script.needsC());
        assertFalse(script.needsD());
        script = scriptEngine.compile("testManyArgs3", "a + b + c", ManyArgs.CONTEXT, emptyMap()).newInstance();
        assertTrue(script.needsA());
        assertTrue(script.needsB());
        assertTrue(script.needsC());
        assertFalse(script.needsD());
        script = scriptEngine.compile("testManyArgs4", "a + b + c + d", ManyArgs.CONTEXT, emptyMap()).newInstance();
        assertTrue(script.needsA());
        assertTrue(script.needsB());
        assertTrue(script.needsC());
        assertTrue(script.needsD());
    }

    public abstract static class VarArgs {
        public interface Factory {
            VarArgs newInstance();
        }

        public static final ScriptContext<Factory> CONTEXT = new ScriptContext<>("varargs", Factory.class);

        public static final String[] PARAMETERS = new String[] { "arg" };

        public abstract Object execute(String... arg);
    }

    public void testVarArgs() throws Exception {
        assertEquals(
            "foo bar baz",
            scriptEngine.compile("testVarArgs0", "String.join(' ', Arrays.asList(arg))", VarArgs.CONTEXT, emptyMap())
                .newInstance()
                .execute("foo", "bar", "baz")
        );
    }

    public abstract static class DefaultMethods {
        public interface Factory {
            DefaultMethods newInstance();
        }

        public static final ScriptContext<Factory> CONTEXT = new ScriptContext<>("defaultmethods", Factory.class);

        public static final String[] PARAMETERS = new String[] { "a", "b", "c", "d" };

        public abstract Object execute(int a, int b, int c, int d);

        public Object executeWithOne() {
            return execute(1, 1, 1, 1);
        }

        public Object executeWithASingleOne(int a, int b, int c) {
            return execute(a, b, c, 1);
        }
    }

    public void testDefaultMethods() throws Exception {
        int rando = randomInt();
        assertEquals(
            rando,
            scriptEngine.compile("testDefaultMethods0", "a", DefaultMethods.CONTEXT, emptyMap()).newInstance().execute(rando, 0, 0, 0)
        );
        assertEquals(
            rando,
            scriptEngine.compile("testDefaultMethods1", "a", DefaultMethods.CONTEXT, emptyMap())
                .newInstance()
                .executeWithASingleOne(rando, 0, 0)
        );
        assertEquals(
            10,
            scriptEngine.compile("testDefaultMethods2", "a + b + c + d", DefaultMethods.CONTEXT, emptyMap())
                .newInstance()
                .execute(1, 2, 3, 4)
        );
        assertEquals(
            4,
            scriptEngine.compile("testDefaultMethods3", "a + b + c + d", DefaultMethods.CONTEXT, emptyMap()).newInstance().executeWithOne()
        );
        assertEquals(
            7,
            scriptEngine.compile("testDefaultMethods4", "a + b + c + d", DefaultMethods.CONTEXT, emptyMap())
                .newInstance()
                .executeWithASingleOne(1, 2, 3)
        );
    }

    public abstract static class ReturnsVoid {
        public interface Factory {
            ReturnsVoid newInstance();
        }

        public static final ScriptContext<Factory> CONTEXT = new ScriptContext<>("returnsvoid", Factory.class);

        public static final String[] PARAMETERS = new String[] { "map" };

        public abstract void execute(Map<String, Object> map);
    }

    public void testReturnsVoid() throws Exception {
        Map<String, Object> map = new HashMap<>();
        scriptEngine.compile("testReturnsVoid0", "map.a = 'foo'", ReturnsVoid.CONTEXT, emptyMap()).newInstance().execute(map);
        assertEquals(Collections.singletonMap("a", "foo"), map);
        scriptEngine.compile("testReturnsVoid1", "map.remove('a')", ReturnsVoid.CONTEXT, emptyMap()).newInstance().execute(map);
        assertEquals(emptyMap(), map);

        String debug = Debugger.toString(ReturnsVoid.class, "int i = 0", new CompilerSettings(), PAINLESS_BASE_WHITELIST);
        // The important thing is that this contains the opcode for returning void
        assertThat(debug, containsString(" RETURN"));
        // We shouldn't contain any weird "default to null" logic
        assertThat(debug, not(containsString("ACONST_NULL")));
    }

    public abstract static class ReturnsPrimitiveBoolean {
        public interface Factory {
            ReturnsPrimitiveBoolean newInstance();
        }

        public static final ScriptContext<Factory> CONTEXT = new ScriptContext<>("returnsprimitiveboolean", Factory.class);

        public static final String[] PARAMETERS = new String[] {};

        public abstract boolean execute();
    }

    public void testReturnsPrimitiveBoolean() throws Exception {
        assertTrue(
            scriptEngine.compile("testReturnsPrimitiveBoolean0", "true", ReturnsPrimitiveBoolean.CONTEXT, emptyMap())
                .newInstance()
                .execute()
        );
        assertFalse(
            scriptEngine.compile("testReturnsPrimitiveBoolean1", "false", ReturnsPrimitiveBoolean.CONTEXT, emptyMap())
                .newInstance()
                .execute()
        );
        assertTrue(
            scriptEngine.compile("testReturnsPrimitiveBoolean2", "Boolean.TRUE", ReturnsPrimitiveBoolean.CONTEXT, emptyMap())
                .newInstance()
                .execute()
        );
        assertFalse(
            scriptEngine.compile("testReturnsPrimitiveBoolean3", "Boolean.FALSE", ReturnsPrimitiveBoolean.CONTEXT, emptyMap())
                .newInstance()
                .execute()
        );

        assertTrue(
            scriptEngine.compile("testReturnsPrimitiveBoolean4", "def i = true; i", ReturnsPrimitiveBoolean.CONTEXT, emptyMap())
                .newInstance()
                .execute()
        );
        assertTrue(
            scriptEngine.compile("testReturnsPrimitiveBoolean5", "def i = Boolean.TRUE; i", ReturnsPrimitiveBoolean.CONTEXT, emptyMap())
                .newInstance()
                .execute()
        );
        assertTrue(
            scriptEngine.compile("testReturnsPrimitiveBoolean6", "true || false", ReturnsPrimitiveBoolean.CONTEXT, emptyMap())
                .newInstance()
                .execute()
        );

        String debug = Debugger.toString(ReturnsPrimitiveBoolean.class, "false", new CompilerSettings(), PAINLESS_BASE_WHITELIST);
        assertThat(debug, containsString("ICONST_0"));
        // The important thing here is that we have the bytecode for returning an integer instead of an object. booleans are integers.
        assertThat(debug, containsString("IRETURN"));

        Exception e = expectScriptThrows(
            ClassCastException.class,
            () -> scriptEngine.compile("testReturnsPrimitiveBoolean7", "1L", ReturnsPrimitiveBoolean.CONTEXT, emptyMap())
                .newInstance()
                .execute()
        );
        assertEquals("Cannot cast from [long] to [boolean].", e.getMessage());
        e = expectScriptThrows(
            ClassCastException.class,
            () -> scriptEngine.compile("testReturnsPrimitiveBoolean8", "1.1f", ReturnsPrimitiveBoolean.CONTEXT, emptyMap())
                .newInstance()
                .execute()
        );
        assertEquals("Cannot cast from [float] to [boolean].", e.getMessage());
        e = expectScriptThrows(
            ClassCastException.class,
            () -> scriptEngine.compile("testReturnsPrimitiveBoolean9", "1.1d", ReturnsPrimitiveBoolean.CONTEXT, emptyMap())
                .newInstance()
                .execute()
        );
        assertEquals("Cannot cast from [double] to [boolean].", e.getMessage());
        expectScriptThrows(
            ClassCastException.class,
            () -> scriptEngine.compile("testReturnsPrimitiveBoolean10", "def i = 1L; i", ReturnsPrimitiveBoolean.CONTEXT, emptyMap())
                .newInstance()
                .execute()
        );
        expectScriptThrows(
            ClassCastException.class,
            () -> scriptEngine.compile("testReturnsPrimitiveBoolean11", "def i = 1.1f; i", ReturnsPrimitiveBoolean.CONTEXT, emptyMap())
                .newInstance()
                .execute()
        );
        expectScriptThrows(
            ClassCastException.class,
            () -> scriptEngine.compile("testReturnsPrimitiveBoolean12", "def i = 1.1d; i", ReturnsPrimitiveBoolean.CONTEXT, emptyMap())
                .newInstance()
                .execute()
        );

        assertFalse(
            scriptEngine.compile("testReturnsPrimitiveBoolean13", "int i = 0", ReturnsPrimitiveBoolean.CONTEXT, emptyMap())
                .newInstance()
                .execute()
        );
    }

    public abstract static class ReturnsPrimitiveInt {
        public interface Factory {
            ReturnsPrimitiveInt newInstance();
        }

        public static final ScriptContext<Factory> CONTEXT = new ScriptContext<>("returnsprimitiveint", Factory.class);

        public static final String[] PARAMETERS = new String[] {};

        public abstract int execute();
    }

    public void testReturnsPrimitiveInt() throws Exception {
        assertEquals(
            1,
            scriptEngine.compile("testReturnsPrimitiveInt0", "1", ReturnsPrimitiveInt.CONTEXT, emptyMap()).newInstance().execute()
        );
        assertEquals(
            1,
            scriptEngine.compile("testReturnsPrimitiveInt1", "(int) 1L", ReturnsPrimitiveInt.CONTEXT, emptyMap()).newInstance().execute()
        );
        assertEquals(
            1,
            scriptEngine.compile("testReturnsPrimitiveInt2", "(int) 1.1d", ReturnsPrimitiveInt.CONTEXT, emptyMap()).newInstance().execute()
        );
        assertEquals(
            1,
            scriptEngine.compile("testReturnsPrimitiveInt3", "(int) 1.1f", ReturnsPrimitiveInt.CONTEXT, emptyMap()).newInstance().execute()
        );
        assertEquals(
            1,
            scriptEngine.compile("testReturnsPrimitiveInt4", "Integer.valueOf(1)", ReturnsPrimitiveInt.CONTEXT, emptyMap())
                .newInstance()
                .execute()
        );

        assertEquals(
            1,
            scriptEngine.compile("testReturnsPrimitiveInt5", "def i = 1; i", ReturnsPrimitiveInt.CONTEXT, emptyMap())
                .newInstance()
                .execute()
        );
        assertEquals(
            1,
            scriptEngine.compile("testReturnsPrimitiveInt6", "def i = Integer.valueOf(1); i", ReturnsPrimitiveInt.CONTEXT, emptyMap())
                .newInstance()
                .execute()
        );

        assertEquals(
            2,
            scriptEngine.compile("testReturnsPrimitiveInt7", "1 + 1", ReturnsPrimitiveInt.CONTEXT, emptyMap()).newInstance().execute()
        );

        String debug = Debugger.toString(ReturnsPrimitiveInt.class, "1", new CompilerSettings(), PAINLESS_BASE_WHITELIST);
        assertThat(debug, containsString("ICONST_1"));
        // The important thing here is that we have the bytecode for returning an integer instead of an object
        assertThat(debug, containsString("IRETURN"));

        Exception e = expectScriptThrows(
            ClassCastException.class,
            () -> scriptEngine.compile("testReturnsPrimitiveInt8", "1L", ReturnsPrimitiveInt.CONTEXT, emptyMap()).newInstance().execute()
        );
        assertEquals("Cannot cast from [long] to [int].", e.getMessage());
        e = expectScriptThrows(
            ClassCastException.class,
            () -> scriptEngine.compile("testReturnsPrimitiveInt9", "1.1f", ReturnsPrimitiveInt.CONTEXT, emptyMap()).newInstance().execute()
        );
        assertEquals("Cannot cast from [float] to [int].", e.getMessage());
        e = expectScriptThrows(
            ClassCastException.class,
            () -> scriptEngine.compile("testReturnsPrimitiveInt10", "1.1d", ReturnsPrimitiveInt.CONTEXT, emptyMap()).newInstance().execute()
        );
        assertEquals("Cannot cast from [double] to [int].", e.getMessage());
        expectScriptThrows(
            ClassCastException.class,
            () -> scriptEngine.compile("testReturnsPrimitiveInt11", "def i = 1L; i", ReturnsPrimitiveInt.CONTEXT, emptyMap())
                .newInstance()
                .execute()
        );
        expectScriptThrows(
            ClassCastException.class,
            () -> scriptEngine.compile("testReturnsPrimitiveInt12", "def i = 1.1f; i", ReturnsPrimitiveInt.CONTEXT, emptyMap())
                .newInstance()
                .execute()
        );
        expectScriptThrows(
            ClassCastException.class,
            () -> scriptEngine.compile("testReturnsPrimitiveInt13", "def i = 1.1d; i", ReturnsPrimitiveInt.CONTEXT, emptyMap())
                .newInstance()
                .execute()
        );

        assertEquals(
            0,
            scriptEngine.compile("testReturnsPrimitiveInt14", "int i = 0", ReturnsPrimitiveInt.CONTEXT, emptyMap()).newInstance().execute()
        );
    }

    public abstract static class ReturnsPrimitiveFloat {
        public interface Factory {
            ReturnsPrimitiveFloat newInstance();
        }

        public static final ScriptContext<Factory> CONTEXT = new ScriptContext<>("returnsprimitivefloat", Factory.class);

        public static final String[] PARAMETERS = new String[] {};

        public abstract float execute();
    }

    public void testReturnsPrimitiveFloat() throws Exception {
        assertEquals(
            1.1f,
            scriptEngine.compile("testReturnsPrimitiveFloat0", "1.1f", ReturnsPrimitiveFloat.CONTEXT, emptyMap()).newInstance().execute(),
            0
        );
        assertEquals(
            1.1f,
            scriptEngine.compile("testReturnsPrimitiveFloat1", "(float) 1.1d", ReturnsPrimitiveFloat.CONTEXT, emptyMap())
                .newInstance()
                .execute(),
            0
        );
        assertEquals(
            1.1f,
            scriptEngine.compile("testReturnsPrimitiveFloat2", "def d = 1.1f; d", ReturnsPrimitiveFloat.CONTEXT, emptyMap())
                .newInstance()
                .execute(),
            0
        );
        assertEquals(
            1.1f,
            scriptEngine.compile("testReturnsPrimitiveFloat3", "def d = Float.valueOf(1.1f); d", ReturnsPrimitiveFloat.CONTEXT, emptyMap())
                .newInstance()
                .execute(),
            0
        );

        assertEquals(
            1.1f + 6.7f,
            scriptEngine.compile("testReturnsPrimitiveFloat4", "1.1f + 6.7f", ReturnsPrimitiveFloat.CONTEXT, emptyMap())
                .newInstance()
                .execute(),
            0
        );

        Exception e = expectScriptThrows(
            ClassCastException.class,
            () -> scriptEngine.compile("testReturnsPrimitiveFloat5", "1.1d", ReturnsPrimitiveFloat.CONTEXT, emptyMap())
                .newInstance()
                .execute()
        );
        assertEquals("Cannot cast from [double] to [float].", e.getMessage());
        e = expectScriptThrows(
            ClassCastException.class,
            () -> scriptEngine.compile("testReturnsPrimitiveFloat6", "def d = 1.1d; d", ReturnsPrimitiveFloat.CONTEXT, emptyMap())
                .newInstance()
                .execute()
        );
        e = expectScriptThrows(
            ClassCastException.class,
            () -> scriptEngine.compile(
                "testReturnsPrimitiveFloat7",
                "def d = Double.valueOf(1.1); d",
                ReturnsPrimitiveFloat.CONTEXT,
                emptyMap()
            ).newInstance().execute()
        );

        String debug = Debugger.toString(ReturnsPrimitiveFloat.class, "1f", new CompilerSettings(), PAINLESS_BASE_WHITELIST);
        assertThat(debug, containsString("FCONST_1"));
        // The important thing here is that we have the bytecode for returning a float instead of an object
        assertThat(debug, containsString("FRETURN"));

        assertEquals(
            0.0f,
            scriptEngine.compile("testReturnsPrimitiveFloat8", "int i = 0", ReturnsPrimitiveFloat.CONTEXT, emptyMap())
                .newInstance()
                .execute(),
            0
        );
    }

    public abstract static class ReturnsPrimitiveDouble {
        public interface Factory {
            ReturnsPrimitiveDouble newInstance();
        }

        public static final ScriptContext<Factory> CONTEXT = new ScriptContext<>("returnsprimitivedouble", Factory.class);

        public static final String[] PARAMETERS = new String[] {};

        public abstract double execute();
    }

    public void testReturnsPrimitiveDouble() throws Exception {
        assertEquals(
            1.0,
            scriptEngine.compile("testReturnsPrimitiveDouble0", "1", ReturnsPrimitiveDouble.CONTEXT, emptyMap()).newInstance().execute(),
            0
        );
        assertEquals(
            1.0,
            scriptEngine.compile("testReturnsPrimitiveDouble1", "1L", ReturnsPrimitiveDouble.CONTEXT, emptyMap()).newInstance().execute(),
            0
        );
        assertEquals(
            1.1,
            scriptEngine.compile("testReturnsPrimitiveDouble2", "1.1d", ReturnsPrimitiveDouble.CONTEXT, emptyMap()).newInstance().execute(),
            0
        );
        assertEquals(
            (double) 1.1f,
            scriptEngine.compile("testReturnsPrimitiveDouble3", "1.1f", ReturnsPrimitiveDouble.CONTEXT, emptyMap()).newInstance().execute(),
            0
        );
        assertEquals(
            1.1,
            scriptEngine.compile("testReturnsPrimitiveDouble4", "Double.valueOf(1.1)", ReturnsPrimitiveDouble.CONTEXT, emptyMap())
                .newInstance()
                .execute(),
            0
        );
        assertEquals(
            (double) 1.1f,
            scriptEngine.compile("testReturnsPrimitiveDouble5", "Float.valueOf(1.1f)", ReturnsPrimitiveDouble.CONTEXT, emptyMap())
                .newInstance()
                .execute(),
            0
        );

        assertEquals(
            1.0,
            scriptEngine.compile("testReturnsPrimitiveDouble6", "def d = 1; d", ReturnsPrimitiveDouble.CONTEXT, emptyMap())
                .newInstance()
                .execute(),
            0
        );
        assertEquals(
            1.0,
            scriptEngine.compile("testReturnsPrimitiveDouble7", "def d = 1L; d", ReturnsPrimitiveDouble.CONTEXT, emptyMap())
                .newInstance()
                .execute(),
            0
        );
        assertEquals(
            1.1,
            scriptEngine.compile("testReturnsPrimitiveDouble8", "def d = 1.1d; d", ReturnsPrimitiveDouble.CONTEXT, emptyMap())
                .newInstance()
                .execute(),
            0
        );
        assertEquals(
            (double) 1.1f,
            scriptEngine.compile("testReturnsPrimitiveDouble9", "def d = 1.1f; d", ReturnsPrimitiveDouble.CONTEXT, emptyMap())
                .newInstance()
                .execute(),
            0
        );
        assertEquals(
            1.1,
            scriptEngine.compile(
                "testReturnsPrimitiveDouble10",
                "def d = Double.valueOf(1.1); d",
                ReturnsPrimitiveDouble.CONTEXT,
                emptyMap()
            ).newInstance().execute(),
            0
        );
        assertEquals(
            (double) 1.1f,
            scriptEngine.compile(
                "testReturnsPrimitiveDouble11",
                "def d = Float.valueOf(1.1f); d",
                ReturnsPrimitiveDouble.CONTEXT,
                emptyMap()
            ).newInstance().execute(),
            0
        );

        assertEquals(
            1.1 + 6.7,
            scriptEngine.compile("testReturnsPrimitiveDouble12", "1.1 + 6.7", ReturnsPrimitiveDouble.CONTEXT, emptyMap())
                .newInstance()
                .execute(),
            0
        );

        String debug = Debugger.toString(ReturnsPrimitiveDouble.class, "1", new CompilerSettings(), PAINLESS_BASE_WHITELIST);
        // The important thing here is that we have the bytecode for returning a double instead of an object
        assertThat(debug, containsString("DRETURN"));

        assertEquals(
            0.0,
            scriptEngine.compile("testReturnsPrimitiveDouble13", "int i = 0", ReturnsPrimitiveDouble.CONTEXT, emptyMap())
                .newInstance()
                .execute(),
            0
        );
    }

    public abstract static class NoArgsConstant {
        public interface Factory {
            NoArgsConstant newInstance();
        }

        public static final ScriptContext<Factory> CONTEXT = new ScriptContext<>("noargsconstant", Factory.class);

        public abstract Object execute(String foo);
    }

    public void testNoArgsConstant() {
        Exception e = expectScriptThrows(
            IllegalArgumentException.class,
            false,
            () -> scriptEngine.compile("testNoArgsConstant0", "1", NoArgsConstant.CONTEXT, emptyMap()).newInstance().execute("constant")
        );
        assertThat(
            e.getMessage(),
            startsWith(
                "Painless needs a constant [String[] PARAMETERS] on all interfaces it implements with the "
                    + "names of the method arguments but ["
                    + NoArgsConstant.class.getName()
                    + "] doesn't have one."
            )
        );
    }

    public abstract static class WrongArgsConstant {
        public interface Factory {
            WrongArgsConstant newInstance();
        }

        public static final ScriptContext<Factory> CONTEXT = new ScriptContext<>("wrongargscontext", Factory.class);

        boolean[] PARAMETERS = new boolean[] { false };

        public abstract Object execute(String foo);
    }

    public void testWrongArgsConstant() {
        Exception e = expectScriptThrows(
            IllegalArgumentException.class,
            false,
            () -> scriptEngine.compile("testWrongArgsConstant0", "1", WrongArgsConstant.CONTEXT, emptyMap())
        );
        assertThat(
            e.getMessage(),
            startsWith(
                "Painless needs a constant [String[] PARAMETERS] on all interfaces it implements with the "
                    + "names of the method arguments but ["
                    + WrongArgsConstant.class.getName()
                    + "] doesn't have one."
            )
        );
    }

    public abstract static class WrongLengthOfArgConstant {
        public interface Factory {
            WrongLengthOfArgConstant newInstance();
        }

        public static final ScriptContext<Factory> CONTEXT = new ScriptContext<>("wronglengthofargcontext", Factory.class);

        public static final String[] PARAMETERS = new String[] { "foo", "bar" };

        public abstract Object execute(String foo);
    }

    public void testWrongLengthOfArgConstant() {
        Exception e = expectScriptThrows(
            IllegalArgumentException.class,
            false,
            () -> scriptEngine.compile("testWrongLengthOfArgConstant", "1", WrongLengthOfArgConstant.CONTEXT, emptyMap())
        );
        assertThat(
            e.getMessage(),
            startsWith(
                "["
                    + WrongLengthOfArgConstant.class.getName()
                    + "#ARGUMENTS] has length [2] but ["
                    + WrongLengthOfArgConstant.class.getName()
                    + "#execute] takes [1] argument."
            )
        );
    }

    public abstract static class UnknownArgType {
        public interface Factory {
            UnknownArgType newInstance();
        }

        public static final ScriptContext<Factory> CONTEXT = new ScriptContext<>("unknownargtype", Factory.class);

        public static final String[] PARAMETERS = new String[] { "foo" };

        public abstract Object execute(UnknownArgType foo);
    }

    public void testUnknownArgType() {
        Exception e = expectScriptThrows(
            IllegalArgumentException.class,
            false,
            () -> scriptEngine.compile("testUnknownArgType0", "1", UnknownArgType.CONTEXT, emptyMap())
        );
        assertEquals(
            "[foo] is of unknown type ["
                + UnknownArgType.class.getName()
                + ". Painless interfaces can only accept arguments "
                + "that are of whitelisted types.",
            e.getMessage()
        );
    }

    public abstract static class UnknownReturnType {
        public interface Factory {
            UnknownReturnType newInstance();
        }

        public static final ScriptContext<Factory> CONTEXT = new ScriptContext<>("unknownreturntype", Factory.class);

        public static final String[] PARAMETERS = new String[] { "foo" };

        public abstract UnknownReturnType execute(String foo);
    }

    public void testUnknownReturnType() {
        Exception e = expectScriptThrows(
            IllegalArgumentException.class,
            false,
            () -> scriptEngine.compile("testUnknownReturnType0", "1", UnknownReturnType.CONTEXT, emptyMap())
        );
        assertEquals(
            "Painless can only implement execute methods returning a whitelisted type but ["
                + UnknownReturnType.class.getName()
                + "#execute] returns ["
                + UnknownReturnType.class.getName()
                + "] which isn't whitelisted.",
            e.getMessage()
        );
    }

    public abstract static class UnknownArgTypeInArray {
        public interface Factory {
            UnknownArgTypeInArray newInstance();
        }

        public static final ScriptContext<Factory> CONTEXT = new ScriptContext<>("unknownargtypeinarray", Factory.class);

        public static final String[] PARAMETERS = new String[] { "foo" };

        public abstract Object execute(UnknownArgTypeInArray[] foo);
    }

    public void testUnknownArgTypeInArray() {
        Exception e = expectScriptThrows(
            IllegalArgumentException.class,
            false,
            () -> scriptEngine.compile("testUnknownAryTypeInArray0", "1", UnknownArgTypeInArray.CONTEXT, emptyMap())
        );
        assertEquals(
            "[foo] is of unknown type ["
                + UnknownArgTypeInArray.class.getName()
                + ". Painless interfaces can only accept "
                + "arguments that are of whitelisted types.",
            e.getMessage()
        );
    }

    public abstract static class TwoExecuteMethods {
        public interface Factory {
            TwoExecuteMethods newInstance();
        }

        public static final ScriptContext<Factory> CONTEXT = new ScriptContext<>("twoexecutemethods", Factory.class);

        public abstract Object execute();

        public abstract Object execute(boolean foo);
    }

    public void testTwoExecuteMethods() {
        Exception e = expectScriptThrows(
            IllegalArgumentException.class,
            false,
            () -> scriptEngine.compile("testTwoExecuteMethods0", "null", TwoExecuteMethods.CONTEXT, emptyMap())
        );
        assertEquals(
            "Painless can only implement interfaces that have a single method named [execute] but ["
                + TwoExecuteMethods.class.getName()
                + "] has more than one.",
            e.getMessage()
        );
    }
}
