/*
 * Licensed to Elasticsearch under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch licenses this file to you under
 * the Apache License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.elasticsearch.painless;

import java.util.HashMap;
import java.util.Map;

import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;
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 ImplementInterfacesTests extends ScriptTestCase {
    public interface NoArgs {
        String[] ARGUMENTS = new String[] {};
        Object execute();
    }
    public void testNoArgs() {
        assertEquals(1, scriptEngine.compile(NoArgs.class, null, "1", emptyMap()).execute());
        assertEquals("foo", scriptEngine.compile(NoArgs.class, null, "'foo'", emptyMap()).execute());

        Exception e = expectScriptThrows(IllegalArgumentException.class, () ->
            scriptEngine.compile(NoArgs.class, null, "doc", emptyMap()));
        assertEquals("Variable [doc] is not defined.", e.getMessage());
        // _score was once embedded into painless by deep magic
        e = expectScriptThrows(IllegalArgumentException.class, () ->
            scriptEngine.compile(NoArgs.class, null, "_score", emptyMap()));
        assertEquals("Variable [_score] is not defined.", e.getMessage());

        String debug = Debugger.toString(NoArgs.class, "int i = 0", new CompilerSettings());
        /* Elasticsearch requires that scripts that return nothing return null. We hack that together by returning null from scripts that
         * return Object if they don't return anything. */
        assertThat(debug, containsString("ACONST_NULL"));
        assertThat(debug, containsString("ARETURN"));
    }

    public interface OneArg {
        String[] ARGUMENTS = new String[] {"arg"};
        Object execute(Object arg);
    }
    public void testOneArg() {
        Object rando = randomInt();
        assertEquals(rando, scriptEngine.compile(OneArg.class, null, "arg", emptyMap()).execute(rando));
        rando = randomAlphaOfLength(5);
        assertEquals(rando, scriptEngine.compile(OneArg.class, null, "arg", emptyMap()).execute(rando));

        Exception e = expectScriptThrows(IllegalArgumentException.class, () ->
            scriptEngine.compile(NoArgs.class, null, "doc", emptyMap()));
        assertEquals("Variable [doc] is not defined.", e.getMessage());
        // _score was once embedded into painless by deep magic
        e = expectScriptThrows(IllegalArgumentException.class, () ->
            scriptEngine.compile(NoArgs.class, null, "_score", emptyMap()));
        assertEquals("Variable [_score] is not defined.", e.getMessage());
    }

    public interface ArrayArg {
        String[] ARGUMENTS = new String[] {"arg"};
        Object execute(String[] arg);
    }
    public void testArrayArg() {
        String rando = randomAlphaOfLength(5);
        assertEquals(rando, scriptEngine.compile(ArrayArg.class, null, "arg[0]", emptyMap()).execute(new String[] {rando, "foo"}));
    }

    public interface PrimitiveArrayArg {
        String[] ARGUMENTS = new String[] {"arg"};
        Object execute(int[] arg);
    }
    public void testPrimitiveArrayArg() {
        int rando = randomInt();
        assertEquals(rando, scriptEngine.compile(PrimitiveArrayArg.class, null, "arg[0]", emptyMap()).execute(new int[] {rando, 10}));
    }

    public interface DefArrayArg {
        String[] ARGUMENTS = new String[] {"arg"};
        Object execute(Object[] arg);
    }
    public void testDefArrayArg() {
        Object rando = randomInt();
        assertEquals(rando, scriptEngine.compile(DefArrayArg.class, null, "arg[0]", emptyMap()).execute(new Object[] {rando, 10}));
        rando = randomAlphaOfLength(5);
        assertEquals(rando, scriptEngine.compile(DefArrayArg.class, null, "arg[0]", emptyMap()).execute(new Object[] {rando, 10}));
        assertEquals(5, scriptEngine.compile(DefArrayArg.class, null, "arg[0].length()", emptyMap()).execute(new Object[] {rando, 10}));
    }

    public interface ManyArgs {
        String[] ARGUMENTS = new String[] {"a", "b", "c", "d"};
        Object execute(int a, int b, int c, int d);
        boolean uses$a();
        boolean uses$b();
        boolean uses$c();
        boolean uses$d();
    }
    public void testManyArgs() {
        int rando = randomInt();
        assertEquals(rando, scriptEngine.compile(ManyArgs.class, null, "a", emptyMap()).execute(rando, 0, 0, 0));
        assertEquals(10, scriptEngine.compile(ManyArgs.class, null, "a + b + c + d", emptyMap()).execute(1, 2, 3, 4));

        // While we're here we can verify that painless correctly finds used variables
        ManyArgs script = scriptEngine.compile(ManyArgs.class, null, "a", emptyMap());
        assertTrue(script.uses$a());
        assertFalse(script.uses$b());
        assertFalse(script.uses$c());
        assertFalse(script.uses$d());
        script = scriptEngine.compile(ManyArgs.class, null, "a + b + c", emptyMap());
        assertTrue(script.uses$a());
        assertTrue(script.uses$b());
        assertTrue(script.uses$c());
        assertFalse(script.uses$d());
        script = scriptEngine.compile(ManyArgs.class, null, "a + b + c + d", emptyMap());
        assertTrue(script.uses$a());
        assertTrue(script.uses$b());
        assertTrue(script.uses$c());
        assertTrue(script.uses$d());
    }

    public interface VarargTest {
        String[] ARGUMENTS = new String[] {"arg"};
        Object execute(String... arg);
    }
    public void testVararg() {
        assertEquals("foo bar baz", scriptEngine.compile(VarargTest.class, null, "String.join(' ', Arrays.asList(arg))", emptyMap())
                    .execute("foo", "bar", "baz"));
    }

    public interface DefaultMethods {
        String[] ARGUMENTS = new String[] {"a", "b", "c", "d"};
        Object execute(int a, int b, int c, int d);
        default Object executeWithOne() {
            return execute(1, 1, 1, 1);
        }
        default Object executeWithASingleOne(int a, int b, int c) {
            return execute(a, b, c, 1);
        }
    }
    public void testDefaultMethods() {
        int rando = randomInt();
        assertEquals(rando, scriptEngine.compile(DefaultMethods.class, null, "a", emptyMap()).execute(rando, 0, 0, 0));
        assertEquals(rando, scriptEngine.compile(DefaultMethods.class, null, "a", emptyMap()).executeWithASingleOne(rando, 0, 0));
        assertEquals(10, scriptEngine.compile(DefaultMethods.class, null, "a + b + c + d", emptyMap()).execute(1, 2, 3, 4));
        assertEquals(4, scriptEngine.compile(DefaultMethods.class, null, "a + b + c + d", emptyMap()).executeWithOne());
        assertEquals(7, scriptEngine.compile(DefaultMethods.class, null, "a + b + c + d", emptyMap()).executeWithASingleOne(1, 2, 3));
    }

    public interface ReturnsVoid {
        String[] ARGUMENTS = new String[] {"map"};
        void execute(Map<String, Object> map);
    }
    public void testReturnsVoid() {
        Map<String, Object> map = new HashMap<>();
        scriptEngine.compile(ReturnsVoid.class, null, "map.a = 'foo'", emptyMap()).execute(map);
        assertEquals(singletonMap("a", "foo"), map);
        scriptEngine.compile(ReturnsVoid.class, null, "map.remove('a')", emptyMap()).execute(map);
        assertEquals(emptyMap(), map);

        String debug = Debugger.toString(ReturnsVoid.class, "int i = 0", new CompilerSettings());
        // 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 interface ReturnsPrimitiveBoolean {
        String[] ARGUMENTS = new String[] {};
        boolean execute();
    }
    public void testReturnsPrimitiveBoolean() {
        assertEquals(true, scriptEngine.compile(ReturnsPrimitiveBoolean.class, null, "true", emptyMap()).execute());
        assertEquals(false, scriptEngine.compile(ReturnsPrimitiveBoolean.class, null, "false", emptyMap()).execute());
        assertEquals(true, scriptEngine.compile(ReturnsPrimitiveBoolean.class, null, "Boolean.TRUE", emptyMap()).execute());
        assertEquals(false, scriptEngine.compile(ReturnsPrimitiveBoolean.class, null, "Boolean.FALSE", emptyMap()).execute());

        assertEquals(true, scriptEngine.compile(ReturnsPrimitiveBoolean.class, null, "def i = true; i", emptyMap()).execute());
        assertEquals(true, scriptEngine.compile(ReturnsPrimitiveBoolean.class, null, "def i = Boolean.TRUE; i", emptyMap()).execute());

        assertEquals(true, scriptEngine.compile(ReturnsPrimitiveBoolean.class, null, "true || false", emptyMap()).execute());

        String debug = Debugger.toString(ReturnsPrimitiveBoolean.class, "false", new CompilerSettings());
        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(ReturnsPrimitiveBoolean.class, null, "1L", emptyMap()).execute());
        assertEquals("Cannot cast from [long] to [boolean].", e.getMessage());
        e = expectScriptThrows(ClassCastException.class, () ->
                scriptEngine.compile(ReturnsPrimitiveBoolean.class, null, "1.1f", emptyMap()).execute());
        assertEquals("Cannot cast from [float] to [boolean].", e.getMessage());
        e = expectScriptThrows(ClassCastException.class, () ->
                scriptEngine.compile(ReturnsPrimitiveBoolean.class, null, "1.1d", emptyMap()).execute());
        assertEquals("Cannot cast from [double] to [boolean].", e.getMessage());
        expectScriptThrows(ClassCastException.class, () ->
                scriptEngine.compile(ReturnsPrimitiveBoolean.class, null, "def i = 1L; i", emptyMap()).execute());
        expectScriptThrows(ClassCastException.class, () ->
                scriptEngine.compile(ReturnsPrimitiveBoolean.class, null, "def i = 1.1f; i", emptyMap()).execute());
        expectScriptThrows(ClassCastException.class, () ->
                scriptEngine.compile(ReturnsPrimitiveBoolean.class, null, "def i = 1.1d; i", emptyMap()).execute());

        assertEquals(false, scriptEngine.compile(ReturnsPrimitiveBoolean.class, null, "int i = 0", emptyMap()).execute());
    }

    public interface ReturnsPrimitiveInt {
        String[] ARGUMENTS = new String[] {};
        int execute();
    }
    public void testReturnsPrimitiveInt() {
        assertEquals(1, scriptEngine.compile(ReturnsPrimitiveInt.class, null, "1", emptyMap()).execute());
        assertEquals(1, scriptEngine.compile(ReturnsPrimitiveInt.class, null, "(int) 1L", emptyMap()).execute());
        assertEquals(1, scriptEngine.compile(ReturnsPrimitiveInt.class, null, "(int) 1.1d", emptyMap()).execute());
        assertEquals(1, scriptEngine.compile(ReturnsPrimitiveInt.class, null, "(int) 1.1f", emptyMap()).execute());
        assertEquals(1, scriptEngine.compile(ReturnsPrimitiveInt.class, null, "Integer.valueOf(1)", emptyMap()).execute());

        assertEquals(1, scriptEngine.compile(ReturnsPrimitiveInt.class, null, "def i = 1; i", emptyMap()).execute());
        assertEquals(1, scriptEngine.compile(ReturnsPrimitiveInt.class, null, "def i = Integer.valueOf(1); i", emptyMap()).execute());

        assertEquals(2, scriptEngine.compile(ReturnsPrimitiveInt.class, null, "1 + 1", emptyMap()).execute());

        String debug = Debugger.toString(ReturnsPrimitiveInt.class, "1", new CompilerSettings());
        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(ReturnsPrimitiveInt.class, null, "1L", emptyMap()).execute());
        assertEquals("Cannot cast from [long] to [int].", e.getMessage());
        e = expectScriptThrows(ClassCastException.class, () ->
                scriptEngine.compile(ReturnsPrimitiveInt.class, null, "1.1f", emptyMap()).execute());
        assertEquals("Cannot cast from [float] to [int].", e.getMessage());
        e = expectScriptThrows(ClassCastException.class, () ->
                scriptEngine.compile(ReturnsPrimitiveInt.class, null, "1.1d", emptyMap()).execute());
        assertEquals("Cannot cast from [double] to [int].", e.getMessage());
        expectScriptThrows(ClassCastException.class, () ->
                scriptEngine.compile(ReturnsPrimitiveInt.class, null, "def i = 1L; i", emptyMap()).execute());
        expectScriptThrows(ClassCastException.class, () ->
                scriptEngine.compile(ReturnsPrimitiveInt.class, null, "def i = 1.1f; i", emptyMap()).execute());
        expectScriptThrows(ClassCastException.class, () ->
                scriptEngine.compile(ReturnsPrimitiveInt.class, null, "def i = 1.1d; i", emptyMap()).execute());

        assertEquals(0, scriptEngine.compile(ReturnsPrimitiveInt.class, null, "int i = 0", emptyMap()).execute());
    }

    public interface ReturnsPrimitiveFloat {
        String[] ARGUMENTS = new String[] {};
        float execute();
    }
    public void testReturnsPrimitiveFloat() {
        assertEquals(1.1f, scriptEngine.compile(ReturnsPrimitiveFloat.class, null, "1.1f", emptyMap()).execute(), 0);
        assertEquals(1.1f, scriptEngine.compile(ReturnsPrimitiveFloat.class, null, "(float) 1.1d", emptyMap()).execute(), 0);
        assertEquals(1.1f, scriptEngine.compile(ReturnsPrimitiveFloat.class, null, "def d = 1.1f; d", emptyMap()).execute(), 0);
        assertEquals(1.1f,
                scriptEngine.compile(ReturnsPrimitiveFloat.class, null, "def d = Float.valueOf(1.1f); d", emptyMap()).execute(), 0);

        assertEquals(1.1f + 6.7f, scriptEngine.compile(ReturnsPrimitiveFloat.class, null, "1.1f + 6.7f", emptyMap()).execute(), 0);

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

        String debug = Debugger.toString(ReturnsPrimitiveFloat.class, "1f", new CompilerSettings());
        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(ReturnsPrimitiveFloat.class, null, "int i = 0", emptyMap()).execute(), 0);
    }

    public interface ReturnsPrimitiveDouble {
        String[] ARGUMENTS = new String[] {};
        double execute();
    }
    public void testReturnsPrimitiveDouble() {
        assertEquals(1.0, scriptEngine.compile(ReturnsPrimitiveDouble.class, null, "1", emptyMap()).execute(), 0);
        assertEquals(1.0, scriptEngine.compile(ReturnsPrimitiveDouble.class, null, "1L", emptyMap()).execute(), 0);
        assertEquals(1.1, scriptEngine.compile(ReturnsPrimitiveDouble.class, null, "1.1d", emptyMap()).execute(), 0);
        assertEquals((double) 1.1f, scriptEngine.compile(ReturnsPrimitiveDouble.class, null, "1.1f", emptyMap()).execute(), 0);
        assertEquals(1.1, scriptEngine.compile(ReturnsPrimitiveDouble.class, null, "Double.valueOf(1.1)", emptyMap()).execute(), 0);
        assertEquals((double) 1.1f,
                    scriptEngine.compile(ReturnsPrimitiveDouble.class, null, "Float.valueOf(1.1f)", emptyMap()).execute(), 0);

        assertEquals(1.0, scriptEngine.compile(ReturnsPrimitiveDouble.class, null, "def d = 1; d", emptyMap()).execute(), 0);
        assertEquals(1.0, scriptEngine.compile(ReturnsPrimitiveDouble.class, null, "def d = 1L; d", emptyMap()).execute(), 0);
        assertEquals(1.1, scriptEngine.compile(ReturnsPrimitiveDouble.class, null, "def d = 1.1d; d", emptyMap()).execute(), 0);
        assertEquals((double) 1.1f, scriptEngine.compile(ReturnsPrimitiveDouble.class, null, "def d = 1.1f; d", emptyMap()).execute(), 0);
        assertEquals(1.1,
                scriptEngine.compile(ReturnsPrimitiveDouble.class, null, "def d = Double.valueOf(1.1); d", emptyMap()).execute(), 0);
        assertEquals((double) 1.1f,
                scriptEngine.compile(ReturnsPrimitiveDouble.class, null, "def d = Float.valueOf(1.1f); d", emptyMap()).execute(), 0);

        assertEquals(1.1 + 6.7, scriptEngine.compile(ReturnsPrimitiveDouble.class, null, "1.1 + 6.7", emptyMap()).execute(), 0);

        String debug = Debugger.toString(ReturnsPrimitiveDouble.class, "1", new CompilerSettings());
        assertThat(debug, containsString("DCONST_1"));
        // 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(ReturnsPrimitiveDouble.class, null, "int i = 0", emptyMap()).execute(), 0);
    }

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

    public interface WrongArgumentsConstant {
        boolean[] ARGUMENTS = new boolean[] {false};
        Object execute(String foo);
    }
    public void testWrongArgumentsConstant() {
        Exception e = expectScriptThrows(IllegalArgumentException.class, false, () ->
            scriptEngine.compile(WrongArgumentsConstant.class, null, "1", emptyMap()));
        assertThat(e.getMessage(), startsWith("Painless needs a constant [String[] ARGUMENTS] on all interfaces it implements with the "
                + "names of the method arguments but [" + WrongArgumentsConstant.class.getName() + "] doesn't have one."));
    }

    public interface WrongLengthOfArgumentConstant {
        String[] ARGUMENTS = new String[] {"foo", "bar"};
        Object execute(String foo);
    }
    public void testWrongLengthOfArgumentConstant() {
        Exception e = expectScriptThrows(IllegalArgumentException.class, false, () ->
            scriptEngine.compile(WrongLengthOfArgumentConstant.class, null, "1", emptyMap()));
        assertThat(e.getMessage(), startsWith("[" + WrongLengthOfArgumentConstant.class.getName() + "#ARGUMENTS] has length [2] but ["
                + WrongLengthOfArgumentConstant.class.getName() + "#execute] takes [1] argument."));
    }

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

    public interface UnknownReturnType {
        String[] ARGUMENTS = new String[] {"foo"};
        UnknownReturnType execute(String foo);
    }
    public void testUnknownReturnType() {
        Exception e = expectScriptThrows(IllegalArgumentException.class, false, () ->
            scriptEngine.compile(UnknownReturnType.class, null, "1", 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 interface UnknownArgTypeInArray {
        String[] ARGUMENTS = new String[] {"foo"};
        Object execute(UnknownArgTypeInArray[] foo);
    }
    public void testUnknownArgTypeInArray() {
        Exception e = expectScriptThrows(IllegalArgumentException.class, false, () ->
            scriptEngine.compile(UnknownArgTypeInArray.class, null, "1", emptyMap()));
        assertEquals("[foo] is of unknown type [" + UnknownArgTypeInArray.class.getName() + ". Painless interfaces can only accept "
                + "arguments that are of whitelisted types.", e.getMessage());
    }

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

    public interface BadMethod {
        Object something();
    }
    public void testBadMethod() {
        Exception e = expectScriptThrows(IllegalArgumentException.class, false, () ->
            scriptEngine.compile(BadMethod.class, null, "null", emptyMap()));
        assertEquals("Painless can only implement methods named [execute] and [uses$argName] but [" + BadMethod.class.getName()
                + "] contains a method named [something]", e.getMessage());
    }

    public interface BadUsesReturn {
        String[] ARGUMENTS = new String[] {"foo"};
        Object execute(String foo);
        Object uses$foo();
    }
    public void testBadUsesReturn() {
        Exception e = expectScriptThrows(IllegalArgumentException.class, false, () ->
            scriptEngine.compile(BadUsesReturn.class, null, "null", emptyMap()));
        assertEquals("Painless can only implement uses$ methods that return boolean but [" + BadUsesReturn.class.getName()
                + "#uses$foo] returns [java.lang.Object].", e.getMessage());
    }

    public interface BadUsesParameter {
        String[] ARGUMENTS = new String[] {"foo", "bar"};
        Object execute(String foo, String bar);
        boolean uses$bar(boolean foo);
    }
    public void testBadUsesParameter() {
        Exception e = expectScriptThrows(IllegalArgumentException.class, false, () ->
            scriptEngine.compile(BadUsesParameter.class, null, "null", emptyMap()));
        assertEquals("Painless can only implement uses$ methods that do not take parameters but [" + BadUsesParameter.class.getName()
                + "#uses$bar] does.", e.getMessage());
    }

    public interface BadUsesName {
        String[] ARGUMENTS = new String[] {"foo", "bar"};
        Object execute(String foo, String bar);
        boolean uses$baz();
    }
    public void testBadUsesName() {
        Exception e = expectScriptThrows(IllegalArgumentException.class, false, () ->
            scriptEngine.compile(BadUsesName.class, null, "null", emptyMap()));
        assertEquals("Painless can only implement uses$ methods that match a parameter name but [" + BadUsesName.class.getName()
                + "#uses$baz] doesn't match any of [foo, bar].", e.getMessage());
    }
}
