/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF 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.apache.commons.jxpath.util;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;

import org.apache.commons.io.IOUtils;
import org.apache.commons.jxpath.JXPathContext;
import org.apache.commons.jxpath.JXPathException;
import org.apache.commons.lang3.ArrayUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/**
 * Tests org.apache.commons.jxpath.util.ClassLoaderUtil.
 */
public class ClassLoaderUtilTest {

    /**
     * A simple class loader which delegates all class loading to its parent with two exceptions. First, attempts to load the class
     * {@code org.apache.commons.jxpath.util.ClassLoaderUtilTest} will always result in a ClassNotFoundException. Second, loading the class
     * {@code org.apache.commons.jxpath.util.ClassLoadingExampleClass} will result in the class being loaded by this class loader, regardless of whether the
     * parent can/has loaded it.
     *
     */
    private static final class TestClassLoader extends ClassLoader {

        private Class<?> testCaseClass = null;

        public TestClassLoader(final ClassLoader classLoader) {
            super(classLoader);
        }

        @Override
        public synchronized Class<?> loadClass(final String name, final boolean resolved) throws ClassNotFoundException {
            if (EXAMPLE_CLASS_NAME.equals(name)) {
                throw new ClassNotFoundException();
            }
            if (TEST_CASE_CLASS_NAME.equals(name)) {
                if (testCaseClass == null) {
                    final URL classUrl = getParent().getResource("org/apache/commons/jxpath/util/ClassLoaderUtilTest.class");
                    byte[] clazzBytes;
                    try {
                        clazzBytes = IOUtils.toByteArray(classUrl);
                    } catch (final IOException e) {
                        throw new ClassNotFoundException(classUrl.toString(), e);
                    }
                    this.testCaseClass = this.defineClass(TEST_CASE_CLASS_NAME, clazzBytes, 0, clazzBytes.length);
                }
                return this.testCaseClass;
            }
            return getParent().loadClass(name);
        }
    }

    // These must be string literals, and not populated by calling getName() on
    // the respective classes, since the tests below will load this class in a
    // special class loader which may be unable to load those classes.
    private static final String TEST_CASE_CLASS_NAME = "org.apache.commons.jxpath.util.ClassLoaderUtilTest";
    private static final String EXAMPLE_CLASS_NAME = "org.apache.commons.jxpath.util.ClassLoadingExampleClass";

    /**
     * Performs a basic query that requires a class be loaded dynamically by JXPath and asserts the dynamic class load fails.
     */
    public static void callExampleMessageMethodAndAssertClassNotFoundJXPathException() {
        final JXPathContext context = JXPathContext.newContext(new Object());
        assertThrows(JXPathException.class, () -> context.selectSingleNode(EXAMPLE_CLASS_NAME + ".getMessage()"),
                "We should not be able to load " + EXAMPLE_CLASS_NAME + ".");
    }

    /**
     * Performs a basic query that requires a class be loaded dynamically by JXPath and asserts the dynamic class load succeeds.
     */
    public static void callExampleMessageMethodAndAssertSuccess() {
        final JXPathContext context = JXPathContext.newContext(new Object());
        assertEquals("an example class", context.selectSingleNode(EXAMPLE_CLASS_NAME + ".getMessage()"));
    }

    private ClassLoader orginalContextClassLoader;

    /**
     * Loads this class through the given class loader and then invokes the indicated no argument static method of the class.
     *
     * @param cl         the class loader under which to invoke the method.
     * @param methodName the name of the static no argument method on this class to invoke.
     * @throws ReflectiveOperationException on test failures.
     */
    private void executeTestMethodUnderClassLoader(final ClassLoader cl, final String methodName) throws ReflectiveOperationException {
        final Class<?> testClass = cl.loadClass(TEST_CASE_CLASS_NAME);
        final Method testMethod = testClass.getMethod(methodName, ArrayUtils.EMPTY_CLASS_ARRAY);
        try {
            testMethod.invoke(null, (Object[]) null);
        } catch (final InvocationTargetException e) {
            if (e.getCause() instanceof RuntimeException) {
                // Allow the runtime exception to propagate up.
                throw (RuntimeException) e.getCause();
            }
        }
    }

    /**
     * Setup for the tests.
     */
    @BeforeEach
    public void setUp() {
        this.orginalContextClassLoader = Thread.currentThread().getContextClassLoader();
    }

    /**
     * Cleanup for the tests.
     */
    @AfterEach
    public void tearDown() {
        Thread.currentThread().setContextClassLoader(this.orginalContextClassLoader);
    }

    /**
     * Tests that JXPath cannot dynamically load a class, which is not visible to its class loader, when the context class loader is null.
     *
     * @throws ReflectiveOperationException on test failures.
     */
    @Test
    public void testClassLoadFailWithoutContextClassLoader() throws ReflectiveOperationException {
        Thread.currentThread().setContextClassLoader(null);
        final ClassLoader cl = new TestClassLoader(getClass().getClassLoader());
        executeTestMethodUnderClassLoader(cl, "callExampleMessageMethodAndAssertClassNotFoundJXPathException");
    }

    /**
     * Tests that JXPath can dynamically load a class, which is not visible to its class loader, when the context class loader is set and can load the class.
     *
     * @throws ReflectiveOperationException on test failures.
     */
    @Test
    public void testClassLoadSuccessWithContextClassLoader() throws ReflectiveOperationException {
        Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
        final ClassLoader cl = new TestClassLoader(getClass().getClassLoader());
        executeTestMethodUnderClassLoader(cl, "callExampleMessageMethodAndAssertSuccess");
    }

    /**
     * Tests that JXPath can dynamically load a class, which is visible to its class loader, when there is no context class loader set.
     */
    @Test
    public void testClassLoadSuccessWithoutContextClassLoader() {
        Thread.currentThread().setContextClassLoader(null);
        callExampleMessageMethodAndAssertSuccess();
    }

    /**
     * Tests that JXPath will use its class loader to dynamically load a requested class when the context class loader is set but unable to load the class.
     */
    @Test
    public void testCurrentClassLoaderFallback() {
        final ClassLoader cl = new TestClassLoader(getClass().getClassLoader());
        Thread.currentThread().setContextClassLoader(cl);
        callExampleMessageMethodAndAssertSuccess();
    }
}
