/*
 * 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.geode.modules.session.junit;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.internal.runners.statements.Fail;
import org.junit.internal.runners.statements.RunAfters;
import org.junit.internal.runners.statements.RunBefores;
import org.junit.rules.MethodRule;
import org.junit.rules.TestRule;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;
import org.junit.runners.model.TestClass;

public class PerTestClassLoaderRunner extends NamedRunner {
  // Some data related to the class from the custom class loader.
  private TestClass testClassFromClassLoader;
  private Object beforeFromClassLoader;
  private Object afterFromClassLoader;
  private Object ruleFromClassLoader;
  private Object testRuleFromClassLoader;
  private Object methodRuleFromClassLoader;

  /**
   * Instantiates a new test per class loader runner.
   *
   * @param klass the klass
   * @throws InitializationError the initialization error
   */
  public PerTestClassLoaderRunner(Class<?> klass) throws InitializationError {
    super(klass);
  }

  @Override
  protected Object createTest() throws Exception {
    // Need an instance now from the class loaded by the custom loader.
    return testClassFromClassLoader.getJavaClass().newInstance();
  }

  /**
   * Load classes (TestCase, @Before and @After with custom class loader.
   *
   * @throws ClassNotFoundException the class not found exception
   */
  private void loadClassesWithCustomClassLoader() throws ClassNotFoundException {
    String classPath = System.getProperty("java.class.path");
    StringTokenizer st = new StringTokenizer(classPath, ":");
    List<URL> urls = new ArrayList<>();
    while (st.hasMoreTokens()) {
      String u = st.nextToken();
      // Because of LOG4J2-2266 and LOG4J-2152 ignore all log4j related jars and let them be picked
      // up from the system classloader. Although it's supposed to be fixed in log4j 2.10, it does
      // not seem to be.
      if (u.contains("log4j")) {
        continue;
      }
      try {
        if (!u.endsWith(".jar")) {
          u += "/";
        }
        URL url = new URL("file://" + u);
        urls.add(url);
      } catch (MalformedURLException e) {
        e.printStackTrace();
      }
    }

    ClassLoader classLoader = new ChildFirstClassLoader(urls.toArray(new URL[] {}),
        Thread.currentThread().getContextClassLoader());

    Thread.currentThread().setContextClassLoader(classLoader);

    testClassFromClassLoader =
        new TestClass(classLoader.loadClass(getTestClass().getJavaClass().getName()));
    // See withAfters and withBefores for the reason.
    beforeFromClassLoader = classLoader.loadClass(Before.class.getName());
    afterFromClassLoader = classLoader.loadClass(After.class.getName());
    ruleFromClassLoader = classLoader.loadClass(Rule.class.getName());
    testRuleFromClassLoader = classLoader.loadClass(TestRule.class.getName());
    methodRuleFromClassLoader = classLoader.loadClass(MethodRule.class.getName());
  }

  @Override
  protected Statement methodBlock(FrameworkMethod method) {
    FrameworkMethod newMethod;
    try {
      // Need the class from the custom loader now, so lets load the class.
      loadClassesWithCustomClassLoader();
      // The method as parameter is from the original class and thus not found in our
      // class loaded by the custom name (reflection is class loader sensitive)
      // So find the same method but now in the class from the class Loader.
      Method methodFromNewlyLoadedClass =
          testClassFromClassLoader.getJavaClass().getMethod(method.getName());
      newMethod = new FrameworkMethod(methodFromNewlyLoadedClass);
    } catch (ClassNotFoundException | NoSuchMethodException | SecurityException e) {
      // Show any problem nicely as a JUnit Test failure.
      return new Fail(e);
    }

    // We can carry out the normal JUnit functionality with our newly discovered method now.
    return super.methodBlock(newMethod);
  }

  @SuppressWarnings("unchecked")
  @Override
  protected Statement withAfters(FrameworkMethod method, Object target, Statement statement) {
    // We now to need to search in the class from the custom loader.
    // We also need to search with the annotation loaded by the custom
    // class loader or otherwise we don't find any method.
    List<FrameworkMethod> afters = testClassFromClassLoader
        .getAnnotatedMethods((Class<? extends Annotation>) afterFromClassLoader);
    return new RunAfters(statement, afters, target);
  }

  @SuppressWarnings("unchecked")
  @Override
  protected Statement withBefores(FrameworkMethod method, Object target, Statement statement) {
    // We now to need to search in the class from the custom loader.
    // We also need to search with the annotation loaded by the custom
    // class loader or otherwise we don't find any method.
    List<FrameworkMethod> befores = testClassFromClassLoader
        .getAnnotatedMethods((Class<? extends Annotation>) beforeFromClassLoader);
    return new RunBefores(statement, befores, target);
  }

  @SuppressWarnings("unchecked")
  @Override
  protected List<MethodRule> rules(Object target) {
    List<MethodRule> result = testClassFromClassLoader.getAnnotatedMethodValues(target,
        (Class<? extends Annotation>) ruleFromClassLoader, (Class) methodRuleFromClassLoader);

    result.addAll(testClassFromClassLoader.getAnnotatedFieldValues(target,
        (Class<? extends Annotation>) ruleFromClassLoader, (Class) methodRuleFromClassLoader));

    return result;
  }

  @SuppressWarnings("unchecked")
  @Override
  protected List<TestRule> getTestRules(Object target) {
    List<TestRule> result = testClassFromClassLoader.getAnnotatedMethodValues(target,
        (Class<? extends Annotation>) ruleFromClassLoader, (Class) testRuleFromClassLoader);

    result.addAll(testClassFromClassLoader.getAnnotatedFieldValues(target,
        (Class<? extends Annotation>) ruleFromClassLoader, (Class) testRuleFromClassLoader));

    return result;
  }
}
