/*
 * 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.test;

import static jdk.dynalink.StandardNamespace.PROPERTY;
import static jdk.dynalink.StandardOperation.GET;

import java.lang.invoke.CallSite;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.List;
import java.util.ServiceConfigurationError;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import jdk.dynalink.CallSiteDescriptor;
import jdk.dynalink.DynamicLinker;
import jdk.dynalink.DynamicLinkerFactory;
import jdk.dynalink.NoSuchDynamicMethodException;
import jdk.dynalink.Operation;
import jdk.dynalink.StandardNamespace;
import jdk.dynalink.StandardOperation;
import jdk.dynalink.beans.StaticClass;
import jdk.dynalink.linker.GuardedInvocation;
import jdk.dynalink.linker.GuardingDynamicLinker;
import jdk.dynalink.linker.LinkRequest;
import jdk.dynalink.linker.LinkerServices;
import jdk.dynalink.support.SimpleRelinkableCallSite;
import jdk.nashorn.api.scripting.AbstractJSObject;
import org.testng.Assert;
import org.testng.annotations.Test;

@SuppressWarnings("javadoc")
public class DynamicLinkerFactoryTest {

    private static final Operation GET_PROPERTY = GET.withNamespace(PROPERTY);

    private static DynamicLinkerFactory newDynamicLinkerFactory(final boolean resetClassLoader) {
        final DynamicLinkerFactory factory = new DynamicLinkerFactory();
        if (resetClassLoader) {
            factory.setClassLoader(null);
        }
        return factory;
    }

    @Test
    public void callSiteCreationTest() {
        final DynamicLinkerFactory factory = newDynamicLinkerFactory(true);
        final DynamicLinker linker = factory.createLinker();
        final StandardOperation[] operations = StandardOperation.values();
        final MethodType mt = MethodType.methodType(Object.class, Object.class);
        for (final Operation op : operations) {
            final CallSite cs = linker.link(new SimpleRelinkableCallSite(new CallSiteDescriptor(
                    MethodHandles.publicLookup(), op, mt)));
            Assert.assertNotNull(cs);
            Assert.assertEquals(cs.type(), mt);
            Assert.assertNotNull(cs.getTarget());
        }
    }

    @Test
    public void fallbackLinkerTest() {
        final DynamicLinkerFactory factory = newDynamicLinkerFactory(true);
        final Operation myOperation = new Operation() {
        };
        final boolean[] reachedFallback = { false };
        factory.setFallbackLinkers((GuardingDynamicLinker) (final LinkRequest linkRequest, final LinkerServices linkerServices) -> {
            Assert.assertEquals(linkRequest.getCallSiteDescriptor().getOperation(), myOperation);
            reachedFallback[0] = true;
            return null;
        });

        final DynamicLinker linker = factory.createLinker();
        final MethodType mt = MethodType.methodType(Object.class);
        final CallSite cs = linker.link(new SimpleRelinkableCallSite(new CallSiteDescriptor(
                MethodHandles.publicLookup(), myOperation, mt)));

        // linking the call site initially does not invoke the linkers!
        Assert.assertFalse(reachedFallback[0]);
        try {
            cs.getTarget().invoke();
        } catch (final NoSuchDynamicMethodException nsdm) {
            // we do expect NoSuchDynamicMethod!
            // because our dummy fallback linker returns null!
        } catch (final Throwable th) {
            throw new RuntimeException("should not reach here with: " + th);
        }

        // check that the control reached fallback linker!
        Assert.assertTrue(reachedFallback[0]);
    }

    @Test
    public void priorityLinkerTest() {
        final DynamicLinkerFactory factory = newDynamicLinkerFactory(true);
        final Operation myOperation = new Operation() {
        };
        final boolean[] reachedProrityLinker = { false };
        factory.setPrioritizedLinker((GuardingDynamicLinker) (final LinkRequest linkRequest, final LinkerServices linkerServices) -> {
            Assert.assertEquals(linkRequest.getCallSiteDescriptor().getOperation(), myOperation);
            reachedProrityLinker[0] = true;
            return null;
        });

        final DynamicLinker linker = factory.createLinker();
        final MethodType mt = MethodType.methodType(Object.class);
        final CallSite cs = linker.link(new SimpleRelinkableCallSite(new CallSiteDescriptor(
                MethodHandles.publicLookup(), myOperation, mt)));

        // linking the call site initially does not invoke the linkers!
        Assert.assertFalse(reachedProrityLinker[0]);
        try {
            cs.getTarget().invoke();
        } catch (final NoSuchDynamicMethodException nsdm) {
            // we do expect NoSuchDynamicMethod!
            // because our dummy priority linker returns null!
        } catch (final Throwable th) {
            throw new RuntimeException("should not reach here with: " + th);
        }

        // check that the control reached fallback linker!
        Assert.assertTrue(reachedProrityLinker[0]);
    }

    @Test
    public void priorityAndFallbackLinkerTest() {
        final DynamicLinkerFactory factory = newDynamicLinkerFactory(true);
        final Operation myOperation = new Operation() {
        };
        final int[] linkerReachCounter = { 0 };
        factory.setPrioritizedLinker((GuardingDynamicLinker) (final LinkRequest linkRequest, final LinkerServices linkerServices) -> {
            Assert.assertEquals(linkRequest.getCallSiteDescriptor().getOperation(), myOperation);
            linkerReachCounter[0]++;
            return null;
        });
        factory.setFallbackLinkers((GuardingDynamicLinker) (final LinkRequest linkRequest, final LinkerServices linkerServices) -> {
            Assert.assertEquals(linkRequest.getCallSiteDescriptor().getOperation(), myOperation);
            Assert.assertEquals(linkerReachCounter[0], 1);
            linkerReachCounter[0]++;
            return null;
        });

        final DynamicLinker linker = factory.createLinker();
        final MethodType mt = MethodType.methodType(Object.class);
        final CallSite cs = linker.link(new SimpleRelinkableCallSite(new CallSiteDescriptor(
                MethodHandles.publicLookup(), myOperation, mt)));

        // linking the call site initially does not invoke the linkers!
        Assert.assertEquals(linkerReachCounter[0], 0);

        try {
            cs.getTarget().invoke();
        } catch (final NoSuchDynamicMethodException nsdm) {
            // we do expect NoSuchDynamicMethod!
        } catch (final Throwable th) {
            throw new RuntimeException("should not reach here with: " + th);
        }

        Assert.assertEquals(linkerReachCounter[0], 2);
    }

    @Test
    public void prelinkTransformerTest() throws Throwable {
        final DynamicLinkerFactory factory = newDynamicLinkerFactory(true);
        final boolean[] reachedPrelinkTransformer = { false };

        factory.setPrelinkTransformer((final GuardedInvocation inv, final LinkRequest linkRequest, final LinkerServices linkerServices) -> {
            reachedPrelinkTransformer[0] = true;
            // just identity transformer!
            return inv;
        });

        final MethodType mt = MethodType.methodType(Object.class, Object.class, String.class);
        final DynamicLinker linker = factory.createLinker();
        final CallSite cs = linker.link(new SimpleRelinkableCallSite(new CallSiteDescriptor(
                MethodHandles.publicLookup(), GET_PROPERTY, mt)));
        Assert.assertFalse(reachedPrelinkTransformer[0]);
        Assert.assertEquals(cs.getTarget().invoke(new Object(), "class"), Object.class);
        Assert.assertTrue(reachedPrelinkTransformer[0]);
    }

    @Test
    public void internalObjectsFilterTest() throws Throwable {
        final DynamicLinkerFactory factory = newDynamicLinkerFactory(true);
        final boolean[] reachedInternalObjectsFilter = { false };

        factory.setInternalObjectsFilter((final MethodHandle mh) -> {
            reachedInternalObjectsFilter[0] = true;
            return mh;
        });

        final MethodType mt = MethodType.methodType(Object.class, Object.class, String.class);
        final DynamicLinker linker = factory.createLinker();
        final CallSite cs = linker.link(new SimpleRelinkableCallSite(new CallSiteDescriptor(
                MethodHandles.publicLookup(), GET_PROPERTY, mt)));
        Assert.assertFalse(reachedInternalObjectsFilter[0]);
        Assert.assertEquals(cs.getTarget().invoke(new Object(), "class"), Object.class);
        Assert.assertTrue(reachedInternalObjectsFilter[0]);
    }

    private static void checkOneAutoLoadingError(final DynamicLinkerFactory factory) {
        // expect one error as we have one untrusted linker exporter in META-INF/services
        final List<ServiceConfigurationError> autoLoadingErrors = factory.getAutoLoadingErrors();
        // single error ...
        Assert.assertFalse(autoLoadingErrors.isEmpty());
        final Throwable cause = autoLoadingErrors.get(0).getCause();
        // ..  due to permission check..
        Assert.assertTrue(cause.toString().contains("dynalink.exportLinkersAutomatically"));
    }

    @Test
    public void autoLoadedLinkerNegativeTest() {
        // enable auto loaded linkers
        final DynamicLinkerFactory factory = newDynamicLinkerFactory(false);
        factory.createLinker();
        checkOneAutoLoadingError(factory);
    }

    @Test
    public void autoLoadedLinkerTest() {
        testAutoLoadedLinkerInvoked(new Object(), "toString");
    }

    @Test
    public void autoLoadedLinkerSeesStaticMethod() {
        testAutoLoadedLinkerInvoked(StaticClass.forClass(System.class), "currentTimeMillis");
    }

    private static void testAutoLoadedLinkerInvoked(final Object target, final String methodName) {
        final DynamicLinkerFactory factory = newDynamicLinkerFactory(false);
        final DynamicLinker linker = factory.createLinker();

        // we should still get one error due to untrusted dynamic linker exporter!
        checkOneAutoLoadingError(factory);

        final MethodType mt = MethodType.methodType(Object.class, Object.class);
        final CallSiteDescriptor testDescriptor = new CallSiteDescriptor(MethodHandles.publicLookup(),
                GET.withNamespace(StandardNamespace.METHOD).named(methodName), mt);
        final CallSite cs = linker.link(new SimpleRelinkableCallSite(testDescriptor));

        TrustedGuardingDynamicLinkerExporter.enable();
        try {
            cs.getTarget().invoke(target);
            // The linker was loaded and it observed our invocation
            Assert.assertTrue(TrustedGuardingDynamicLinkerExporter.isLastCallSiteDescriptor(testDescriptor));
        } catch (final Throwable th) {
            throw new RuntimeException(th);
        } finally {
            TrustedGuardingDynamicLinkerExporter.disable();
        }

    }

    @Test
    public void nashornExportedLinkerJSObjectTest() {
        final DynamicLinkerFactory factory = newDynamicLinkerFactory(false);
        final DynamicLinker linker = factory.createLinker();

        final MethodType mt = MethodType.methodType(Object.class, Object.class);
        final Operation op = GET_PROPERTY.named("foo");
        final CallSite cs = linker.link(new SimpleRelinkableCallSite(new CallSiteDescriptor(
                MethodHandles.publicLookup(), op, mt)));
        final boolean[] reachedGetMember = new boolean[1];
        // check that the nashorn exported linker can be used for user defined JSObject
        final Object obj = new AbstractJSObject() {
                @Override
                public Object getMember(final String name) {
                    reachedGetMember[0] = true;
                    return name.equals("foo")? "bar" : "<unknown>";
                }
            };

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

        Assert.assertTrue(reachedGetMember[0]);
        Assert.assertEquals(value, "bar");
    }

    @Test
    public void nashornExportedLinkerScriptObjectMirrorTest() {
        final DynamicLinkerFactory factory = newDynamicLinkerFactory(false);
        final DynamicLinker linker = factory.createLinker();

        // check that the nashorn exported linker can be used for ScriptObjectMirror
        final ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
        final MethodType mt = MethodType.methodType(Object.class, Object.class);
        final Operation op = GET_PROPERTY.named("foo");
        final CallSite cs = linker.link(new SimpleRelinkableCallSite(new CallSiteDescriptor(
                MethodHandles.publicLookup(), op, mt)));
        Object value = null;
        try {
            final Object obj = engine.eval("({ foo: 'hello' })");
            value = cs.getTarget().invoke(obj);
        } catch (final Throwable th) {
            throw new RuntimeException(th);
        }
        Assert.assertEquals(value, "hello");
    }
}
