/*
 * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package jdk.dynalink.beans.test;

import static jdk.dynalink.StandardNamespace.ELEMENT;
import static jdk.dynalink.StandardNamespace.METHOD;
import static jdk.dynalink.StandardNamespace.PROPERTY;
import static jdk.dynalink.StandardOperation.CALL;
import static jdk.dynalink.StandardOperation.GET;
import static jdk.dynalink.StandardOperation.NEW;
import static jdk.dynalink.StandardOperation.REMOVE;
import static jdk.dynalink.StandardOperation.SET;

import java.lang.invoke.CallSite;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.security.AccessControlException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import jdk.dynalink.CallSiteDescriptor;
import jdk.dynalink.DynamicLinker;
import jdk.dynalink.DynamicLinkerFactory;
import jdk.dynalink.NamedOperation;
import jdk.dynalink.NoSuchDynamicMethodException;
import jdk.dynalink.Operation;
import jdk.dynalink.beans.BeansLinker;
import jdk.dynalink.beans.StaticClass;
import jdk.dynalink.support.SimpleRelinkableCallSite;
import org.testng.Assert;
import org.testng.annotations.AfterTest;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

public class BeanLinkerTest {

    private DynamicLinker linker;
    private static final MethodHandles.Lookup MY_LOOKUP = MethodHandles.lookup();

    @SuppressWarnings("unused")
    @DataProvider
    private static Object[][] flags() {
        return new Object[][]{
            {Boolean.FALSE},
            {Boolean.TRUE}
        };
    }

    // helpers to create callsite objects
    private CallSite createCallSite(final boolean publicLookup, final Operation op, final MethodType mt) {
        return linker.link(new SimpleRelinkableCallSite(new CallSiteDescriptor(
                publicLookup ? MethodHandles.publicLookup() : MY_LOOKUP, op, mt)));
    }

    private CallSite createCallSite(final boolean publicLookup, final Operation op, final Object name, final MethodType mt) {
        return createCallSite(publicLookup, op.named(name), mt);
    }

    private CallSite createGetMethodCallSite(final boolean publicLookup, final String name) {
        return createCallSite(publicLookup, GET_METHOD, name, MethodType.methodType(Object.class, Object.class));
    }

    private static final MethodHandle throwArrayIndexOutOfBounds = findThrower("throwArrayIndexOutOfBounds");
    private static final MethodHandle throwIndexOutOfBounds = findThrower("throwIndexOutOfBounds");

    private static final Operation GET_PROPERTY = GET.withNamespace(PROPERTY);
    private static final Operation GET_ELEMENT = GET.withNamespace(ELEMENT);
    private static final Operation GET_METHOD = GET.withNamespace(METHOD);
    private static final Operation SET_ELEMENT = SET.withNamespace(ELEMENT);
    private static final Operation REMOVE_ELEMENT = REMOVE.withNamespace(ELEMENT);

    private static final MethodHandle findThrower(final String name) {
        try {
            return MethodHandles.lookup().findStatic(BeanLinkerTest.class, name,
                    MethodType.methodType(Object.class, Object.class, Object.class));
        } catch (NoSuchMethodException | IllegalAccessException e) {
            Assert.fail("Unexpected exception", e);
            return null;
        }
    }

    private static Object throwArrayIndexOutOfBounds(final Object receiver, final Object index) {
        throw new ArrayIndexOutOfBoundsException(String.valueOf(index));
    }

    private static Object throwIndexOutOfBounds(final Object receiver, final Object index) {
        throw new IndexOutOfBoundsException(String.valueOf(index));
    }

    @BeforeTest
    public void initLinker() {
        final DynamicLinkerFactory factory = new DynamicLinkerFactory();
        factory.setFallbackLinkers(new BeansLinker((req, services) -> {
            // This is a MissingMemberHandlerFactory that creates a missing
            // member handler for element getters and setters that throw an
            // ArrayIndexOutOfBoundsException when applied to an array and an
            // IndexOutOfBoundsException when applied to a list.

            final CallSiteDescriptor desc = req.getCallSiteDescriptor();
            final Operation op = desc.getOperation();
            final Operation baseOp = NamedOperation.getBaseOperation(op);
            if (baseOp != GET_ELEMENT && baseOp != SET_ELEMENT && baseOp != REMOVE_ELEMENT) {
                // We only handle GET_ELEMENT, SET_ELEMENT and REMOVE_ELEMENT.
                return null;
            }

            final Object receiver = req.getReceiver();
            Assert.assertNotNull(receiver);

            final Class<?> clazz = receiver.getClass();
            final MethodHandle throwerHandle;
            if (clazz.isArray()) {
                throwerHandle = throwArrayIndexOutOfBounds;
            } else if (List.class.isAssignableFrom(clazz)) {
                throwerHandle = throwIndexOutOfBounds;
            } else {
                Assert.fail("Unexpected receiver type " + clazz.getName());
                return null;
            }

            final Object name = NamedOperation.getName(op);
            final MethodHandle nameBoundHandle;
            if (name == null) {
                nameBoundHandle = throwerHandle;
            } else {
                // If the operation is for a fixed index, bind it
                nameBoundHandle = MethodHandles.insertArguments(throwerHandle, 1, name);
            }

            final MethodType callSiteType = desc.getMethodType();
            final MethodHandle arityMatchedHandle;
            if (baseOp == SET_ELEMENT) {
                // Drop "value" parameter for a setter
                final int handleArity = nameBoundHandle.type().parameterCount();
                arityMatchedHandle = MethodHandles.dropArguments(nameBoundHandle,
                        handleArity, callSiteType.parameterType(handleArity));
            } else {
                arityMatchedHandle = nameBoundHandle;
            }

            return arityMatchedHandle.asType(callSiteType);
        }));
        this.linker = factory.createLinker();
    }

    @AfterTest
    public void afterTest() {
        this.linker = null;
    }

    @Test(dataProvider = "flags")
    public void getPropertyTest(final boolean publicLookup) throws Throwable {
        final MethodType mt = MethodType.methodType(Object.class, Object.class, String.class);
        final CallSite cs = createCallSite(publicLookup, GET_PROPERTY, mt);
        Assert.assertEquals(cs.getTarget().invoke(new Object(), "class"), Object.class);
        Assert.assertEquals(cs.getTarget().invoke(new Date(), "class"), Date.class);
    }

    @Test(dataProvider = "flags")
    public void getPropertyNegativeTest(final boolean publicLookup) throws Throwable {
        final MethodType mt = MethodType.methodType(Object.class, Object.class, String.class);
        final CallSite cs = createCallSite(publicLookup, GET_PROPERTY, mt);
        Assert.assertNull(cs.getTarget().invoke(new Object(), "DOES_NOT_EXIST"));
    }

    @Test(dataProvider = "flags")
    public void getPropertyTest2(final boolean publicLookup) throws Throwable {
        final MethodType mt = MethodType.methodType(Object.class, Object.class);
        final CallSite cs = createCallSite(publicLookup, GET_PROPERTY, "class", mt);
        Assert.assertEquals(cs.getTarget().invoke(new Object()), Object.class);
        Assert.assertEquals(cs.getTarget().invoke(new Date()), Date.class);
    }

    @Test(dataProvider = "flags")
    public void getPropertyNegativeTest2(final boolean publicLookup) throws Throwable {
        final MethodType mt = MethodType.methodType(Object.class, Object.class);
        final CallSite cs = createCallSite(publicLookup, GET_PROPERTY, "DOES_NOT_EXIST", mt);

        try {
            cs.getTarget().invoke(new Object());
            throw new RuntimeException("Expected NoSuchDynamicMethodException");
        } catch (final Throwable th) {
            Assert.assertTrue(th instanceof NoSuchDynamicMethodException);
        }
    }

    @Test(dataProvider = "flags")
    public void getLengthPropertyTest(final boolean publicLookup) throws Throwable {
        final MethodType mt = MethodType.methodType(int.class, Object.class, String.class);
        final CallSite cs = createCallSite(publicLookup, GET_PROPERTY, mt);

        Assert.assertEquals((int) cs.getTarget().invoke(new int[10], "length"), 10);
        Assert.assertEquals((int) cs.getTarget().invoke(new String[33], "length"), 33);
    }

    @Test(dataProvider = "flags")
    public void getElementTest(final boolean publicLookup) throws Throwable {
        final MethodType mt = MethodType.methodType(int.class, Object.class, int.class);
        final CallSite cs = createCallSite(publicLookup, GET_ELEMENT, mt);

        final int[] arr = {23, 42};
        Assert.assertEquals((int) cs.getTarget().invoke(arr, 0), 23);
        Assert.assertEquals((int) cs.getTarget().invoke(arr, 1), 42);
        try {
            final int x = (int) cs.getTarget().invoke(arr, -1);
            throw new RuntimeException("expected ArrayIndexOutOfBoundsException");
        } catch (final ArrayIndexOutOfBoundsException ex) {
        }

        try {
            final int x = (int) cs.getTarget().invoke(arr, arr.length);
            throw new RuntimeException("expected ArrayIndexOutOfBoundsException");
        } catch (final ArrayIndexOutOfBoundsException ex) {
        }

        final List<Integer> list = new ArrayList<>();
        list.add(23);
        list.add(430);
        list.add(-4354);
        Assert.assertEquals((int) cs.getTarget().invoke(list, 0), (int) list.get(0));
        Assert.assertEquals((int) cs.getTarget().invoke(list, 1), (int) list.get(1));
        Assert.assertEquals((int) cs.getTarget().invoke(list, 2), (int) list.get(2));
        try {
            cs.getTarget().invoke(list, -1);
            throw new RuntimeException("expected IndexOutOfBoundsException");
        } catch (final IndexOutOfBoundsException ex) {
        }

        try {
            cs.getTarget().invoke(list, list.size());
            throw new RuntimeException("expected IndexOutOfBoundsException");
        } catch (final IndexOutOfBoundsException ex) {
        }
    }

    private Object invokeWithFixedKey(boolean publicLookup, Operation op, Object name, MethodType mt, Object... args) throws Throwable {
        return createCallSite(publicLookup, op.named(name), mt).getTarget().invokeWithArguments(args);
    }

    @Test(dataProvider = "flags")
    public void getElementWithFixedKeyTest(final boolean publicLookup) throws Throwable {
        final MethodType mt = MethodType.methodType(int.class, Object.class);

        final int[] arr = {23, 42};
        Assert.assertEquals((int) invokeWithFixedKey(publicLookup, GET_ELEMENT, 0, mt, arr), 23);
        Assert.assertEquals((int) invokeWithFixedKey(publicLookup, GET_ELEMENT, 1, mt, arr), 42);
        try {
            invokeWithFixedKey(publicLookup, GET_ELEMENT, -1, mt, arr);
            throw new RuntimeException("expected ArrayIndexOutOfBoundsException");
        } catch (final ArrayIndexOutOfBoundsException ex) {
        }

        try {
            invokeWithFixedKey(publicLookup, GET_ELEMENT, arr.length, mt, arr);
            throw new RuntimeException("expected ArrayIndexOutOfBoundsException");
        } catch (final ArrayIndexOutOfBoundsException ex) {
        }

        final List<Integer> list = new ArrayList<>();
        list.add(23);
        list.add(430);
        list.add(-4354);
        for (int i = 0; i < 3; ++i) {
            Assert.assertEquals((int) invokeWithFixedKey(publicLookup, GET_ELEMENT, i, mt, list), (int) list.get(i));
        }
        try {
            invokeWithFixedKey(publicLookup, GET_ELEMENT, -1, mt, list);
            throw new RuntimeException("expected IndexOutOfBoundsException");
        } catch (final IndexOutOfBoundsException ex) {
        }

        try {
            invokeWithFixedKey(publicLookup, GET_ELEMENT, list.size(), mt, list);
            throw new RuntimeException("expected IndexOutOfBoundsException");
        } catch (final IndexOutOfBoundsException ex) {
        }
    }

    @Test(dataProvider = "flags")
    public void setElementTest(final boolean publicLookup) throws Throwable {
        final MethodType mt = MethodType.methodType(void.class, Object.class, int.class, int.class);
        final CallSite cs = createCallSite(publicLookup, SET_ELEMENT, mt);

        final int[] arr = {23, 42};
        cs.getTarget().invoke(arr, 0, 0);
        Assert.assertEquals(arr[0], 0);
        cs.getTarget().invoke(arr, 1, -5);
        Assert.assertEquals(arr[1], -5);

        try {
            cs.getTarget().invoke(arr, -1, 12);
            throw new RuntimeException("expected ArrayIndexOutOfBoundsException");
        } catch (final ArrayIndexOutOfBoundsException ex) {
        }

        try {
            cs.getTarget().invoke(arr, arr.length, 20);
            throw new RuntimeException("expected ArrayIndexOutOfBoundsException");
        } catch (final ArrayIndexOutOfBoundsException ex) {
        }

        final List<Integer> list = new ArrayList<>();
        list.add(23);
        list.add(430);
        list.add(-4354);

        cs.getTarget().invoke(list, 0, -list.get(0));
        Assert.assertEquals((int) list.get(0), -23);
        cs.getTarget().invoke(list, 1, -430);
        Assert.assertEquals((int) list.get(1), -430);
        cs.getTarget().invoke(list, 2, 4354);
        Assert.assertEquals((int) list.get(2), 4354);
        try {
            cs.getTarget().invoke(list, -1, 343);
            throw new RuntimeException("expected IndexOutOfBoundsException");
        } catch (final IndexOutOfBoundsException ex) {
        }

        try {
            cs.getTarget().invoke(list, list.size(), 43543);
            throw new RuntimeException("expected IndexOutOfBoundsException");
        } catch (final IndexOutOfBoundsException ex) {
        }
    }

    @Test(dataProvider = "flags")
    public void setElementWithFixedKeyTest(final boolean publicLookup) throws Throwable {
        final MethodType mt = MethodType.methodType(void.class, Object.class, int.class);

        final int[] arr = {23, 42};
        invokeWithFixedKey(publicLookup, SET_ELEMENT, 0, mt, arr, 0);
        Assert.assertEquals(arr[0], 0);
        invokeWithFixedKey(publicLookup, SET_ELEMENT, 1, mt, arr, -5);
        Assert.assertEquals(arr[1], -5);

        try {
            invokeWithFixedKey(publicLookup, SET_ELEMENT, -1, mt, arr, 12);
            throw new RuntimeException("expected ArrayIndexOutOfBoundsException");
        } catch (final ArrayIndexOutOfBoundsException ex) {
        }

        try {
            invokeWithFixedKey(publicLookup, SET_ELEMENT, arr.length, mt, arr, 20);
            throw new RuntimeException("expected ArrayIndexOutOfBoundsException");
        } catch (final ArrayIndexOutOfBoundsException ex) {
        }

        final List<Integer> list = new ArrayList<>();
        list.add(23);
        list.add(430);
        list.add(-4354);

        invokeWithFixedKey(publicLookup, SET_ELEMENT, 0, mt, list, -list.get(0));
        Assert.assertEquals((int) list.get(0), -23);
        invokeWithFixedKey(publicLookup, SET_ELEMENT, 1, mt, list, -430);
        Assert.assertEquals((int) list.get(1), -430);
        invokeWithFixedKey(publicLookup, SET_ELEMENT, 2, mt, list, 4354);
        Assert.assertEquals((int) list.get(2), 4354);
        try {
            invokeWithFixedKey(publicLookup, SET_ELEMENT, -1, mt, list, 343);
            throw new RuntimeException("expected IndexOutOfBoundsException");
        } catch (final IndexOutOfBoundsException ex) {
        }

        try {
            invokeWithFixedKey(publicLookup, SET_ELEMENT, list.size(), mt, list, 43543);
            throw new RuntimeException("expected IndexOutOfBoundsException");
        } catch (final IndexOutOfBoundsException ex) {
        }
    }

    @Test(dataProvider = "flags")
    public void newObjectTest(final boolean publicLookup) {
        final MethodType mt = MethodType.methodType(Object.class, Object.class);
        final CallSite cs = createCallSite(publicLookup, NEW, mt);

        Object obj = null;
        try {
            obj = cs.getTarget().invoke(StaticClass.forClass(Date.class));
        } catch (final Throwable th) {
            throw new RuntimeException(th);
        }

        Assert.assertTrue(obj instanceof Date);
    }

    @Test(dataProvider = "flags")
    public void staticPropertyTest(final boolean publicLookup) {
        final MethodType mt = MethodType.methodType(Object.class, Class.class);
        final CallSite cs = createCallSite(publicLookup, GET_PROPERTY, "static", mt);

        Object obj = null;
        try {
            obj = cs.getTarget().invoke(Object.class);
        } catch (final Throwable th) {
            throw new RuntimeException(th);
        }

        Assert.assertTrue(obj instanceof StaticClass);
        Assert.assertEquals(((StaticClass) obj).getRepresentedClass(), Object.class);

        try {
            obj = cs.getTarget().invoke(Date.class);
        } catch (final Throwable th) {
            throw new RuntimeException(th);
        }

        Assert.assertTrue(obj instanceof StaticClass);
        Assert.assertEquals(((StaticClass) obj).getRepresentedClass(), Date.class);

        try {
            obj = cs.getTarget().invoke(Object[].class);
        } catch (final Throwable th) {
            throw new RuntimeException(th);
        }

        Assert.assertTrue(obj instanceof StaticClass);
        Assert.assertEquals(((StaticClass) obj).getRepresentedClass(), Object[].class);
    }

    @Test(dataProvider = "flags")
    public void instanceMethodCallTest(final boolean publicLookup) {
        final CallSite cs = createGetMethodCallSite(publicLookup, "getClass");
        final MethodType mt2 = MethodType.methodType(Class.class, Object.class, Object.class);
        final CallSite cs2 = createCallSite(publicLookup, CALL, mt2);

        Object method = null;
        try {
            method = cs.getTarget().invoke(new Date());
        } catch (final Throwable th) {
            throw new RuntimeException(th);
        }

        Assert.assertNotNull(method);
        Assert.assertTrue(BeansLinker.isDynamicMethod(method));
        Class clz = null;
        try {
            clz = (Class) cs2.getTarget().invoke(method, new Date());
        } catch (final Throwable th) {
            throw new RuntimeException(th);
        }

        Assert.assertEquals(clz, Date.class);
    }

    @Test(dataProvider = "flags")
    public void staticMethodCallTest(final boolean publicLookup) {
        final CallSite cs = createGetMethodCallSite(publicLookup, "getProperty");
        final MethodType mt2 = MethodType.methodType(String.class, Object.class, Object.class, String.class);
        final CallSite cs2 = createCallSite(publicLookup, CALL, mt2);

        Object method = null;
        try {
            method = cs.getTarget().invoke(StaticClass.forClass(System.class));
        } catch (final Throwable th) {
            throw new RuntimeException(th);
        }

        Assert.assertNotNull(method);
        Assert.assertTrue(BeansLinker.isDynamicMethod(method));

        String str = null;
        try {
            str = (String) cs2.getTarget().invoke(method, null, "os.name");
        } catch (final Throwable th) {
            throw new RuntimeException(th);
        }
        Assert.assertEquals(str, System.getProperty("os.name"));
    }

    // try calling System.getenv and expect security exception
    @Test(dataProvider = "flags")
    public void systemGetenvTest(final boolean publicLookup) {
        final CallSite cs1 = createGetMethodCallSite(publicLookup, "getenv");
        final CallSite cs2 = createCallSite(publicLookup, CALL, MethodType.methodType(Object.class, Object.class, Object.class));

        try {
            final Object method = cs1.getTarget().invoke(StaticClass.forClass(System.class));
            cs2.getTarget().invoke(method, StaticClass.forClass(System.class));
            throw new RuntimeException("should not reach here in any case!");
        } catch (final Throwable th) {
            Assert.assertTrue(th instanceof SecurityException);
        }
    }

    // try getting a specific sensitive System property and expect security exception
    @Test(dataProvider = "flags")
    public void systemGetPropertyTest(final boolean publicLookup) {
        final CallSite cs1 = createGetMethodCallSite(publicLookup, "getProperty");
        final CallSite cs2 = createCallSite(publicLookup, CALL, MethodType.methodType(String.class, Object.class, Object.class, String.class));

        try {
            final Object method = cs1.getTarget().invoke(StaticClass.forClass(System.class));
            cs2.getTarget().invoke(method, StaticClass.forClass(System.class), "java.home");
            throw new RuntimeException("should not reach here in any case!");
        } catch (final Throwable th) {
            Assert.assertTrue(th instanceof SecurityException);
        }
    }

    // check a @CallerSensitive API and expect appropriate access check exception
    @Test(dataProvider = "flags")
    public void systemLoadLibraryTest(final boolean publicLookup) {
        final CallSite cs1 = createGetMethodCallSite(publicLookup, "loadLibrary");
        final CallSite cs2 = createCallSite(publicLookup, CALL, MethodType.methodType(void.class, Object.class, Object.class, String.class));

        try {
            final Object method = cs1.getTarget().invoke(StaticClass.forClass(System.class));
            cs2.getTarget().invoke(method, StaticClass.forClass(System.class), "foo");
            throw new RuntimeException("should not reach here in any case!");
        } catch (final Throwable th) {
            if (publicLookup) {
                Assert.assertTrue(th instanceof IllegalAccessError);
            } else {
                Assert.assertTrue(th instanceof AccessControlException);
            }
        }
    }

    @Test(dataProvider = "flags")
    public void removeElementFromListTest(final boolean publicLookup) throws Throwable {
        final MethodType mt = MethodType.methodType(void.class, Object.class, int.class);
        final CallSite cs = createCallSite(publicLookup, REMOVE_ELEMENT, mt);

        final List<Integer> list = new ArrayList<>(List.of(23, 430, -4354));

        cs.getTarget().invoke(list, 1);
        Assert.assertEquals(list, List.of(23, -4354));
        cs.getTarget().invoke(list, 1);
        Assert.assertEquals(list, List.of(23));
        cs.getTarget().invoke(list, 0);
        Assert.assertEquals(list, List.of());
        try {
            cs.getTarget().invoke(list, -1);
            throw new RuntimeException("expected IndexOutOfBoundsException");
        } catch (final IndexOutOfBoundsException ex) {
        }

        try {
            cs.getTarget().invoke(list, list.size());
            throw new RuntimeException("expected IndexOutOfBoundsException");
        } catch (final IndexOutOfBoundsException ex) {
        }
    }

    @Test(dataProvider = "flags")
    public void removeElementFromListWithFixedKeyTest(final boolean publicLookup) throws Throwable {
        final MethodType mt = MethodType.methodType(void.class, Object.class);

        final List<Integer> list = new ArrayList<>(List.of(23, 430, -4354));

        createCallSite(publicLookup, REMOVE_ELEMENT.named(1), mt).getTarget().invoke(list);
        Assert.assertEquals(list, List.of(23, -4354));
        createCallSite(publicLookup, REMOVE_ELEMENT.named(1), mt).getTarget().invoke(list);
        Assert.assertEquals(list, List.of(23));
        createCallSite(publicLookup, REMOVE_ELEMENT.named(0), mt).getTarget().invoke(list);
        Assert.assertEquals(list, List.of());
        try {
            createCallSite(publicLookup, REMOVE_ELEMENT.named(-1), mt).getTarget().invoke(list);
            throw new RuntimeException("expected IndexOutOfBoundsException");
        } catch (final IndexOutOfBoundsException ex) {
        }

        try {
            createCallSite(publicLookup, REMOVE_ELEMENT.named(list.size()), mt).getTarget().invoke(list);
            throw new RuntimeException("expected IndexOutOfBoundsException");
        } catch (final IndexOutOfBoundsException ex) {
        }
    }

    @Test(dataProvider = "flags")
    public void removeElementFromMapTest(final boolean publicLookup) throws Throwable {
        final MethodType mt = MethodType.methodType(void.class, Object.class, Object.class);
        final CallSite cs = createCallSite(publicLookup, REMOVE_ELEMENT, mt);

        final Map<String, String> map = new HashMap<>(Map.of("k1", "v1", "k2", "v2", "k3", "v3"));

        cs.getTarget().invoke(map, "k2");
        Assert.assertEquals(map, Map.of("k1", "v1", "k3", "v3"));
        cs.getTarget().invoke(map, "k4");
        Assert.assertEquals(map, Map.of("k1", "v1", "k3", "v3"));
        cs.getTarget().invoke(map, "k1");
        Assert.assertEquals(map, Map.of("k3", "v3"));
    }


    @Test(dataProvider = "flags")
    public void removeElementFromMapWithFixedKeyTest(final boolean publicLookup) throws Throwable {
        final MethodType mt = MethodType.methodType(void.class, Object.class);

        final Map<String, String> map = new HashMap<>(Map.of("k1", "v1", "k2", "v2", "k3", "v3"));

        createCallSite(publicLookup, REMOVE_ELEMENT.named("k2"), mt).getTarget().invoke(map);
        Assert.assertEquals(map, Map.of("k1", "v1", "k3", "v3"));
        createCallSite(publicLookup, REMOVE_ELEMENT.named("k4"), mt).getTarget().invoke(map);
        Assert.assertEquals(map, Map.of("k1", "v1", "k3", "v3"));
        createCallSite(publicLookup, REMOVE_ELEMENT.named("k1"), mt).getTarget().invoke(map);
        Assert.assertEquals(map, Map.of("k3", "v3"));
    }
}
