/*
 * 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.servicecomb.it.junit;

import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Stack;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.commons.lang3.reflect.MethodUtils;
import org.apache.servicecomb.core.Const;
import org.apache.servicecomb.foundation.common.utils.BeanUtils;
import org.apache.servicecomb.foundation.common.utils.JvmUtils;
import org.apache.servicecomb.it.Consumers;
import org.apache.servicecomb.it.extend.engine.GateRestTemplate;
import org.apache.servicecomb.it.extend.engine.ITSCBRestTemplate;
import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunListener;

import com.google.common.reflect.ClassPath;

public final class ITJUnitUtils {
  private static ClassLoader classLoader = JvmUtils.findClassLoader();

  private static JUnitCore jUnitCore = new JUnitCore();

  private static Stack<String> parents = new Stack<>();

  private static List<SCBFailure> failures = new ArrayList<>();

  private static AtomicInteger runCount = new AtomicInteger();

  private static String transport;

  private static String producerName;

  public static String getProducerName() {
    return producerName;
  }

  public static void setProducerName(String producerName) {
    ITJUnitUtils.producerName = producerName;
  }

  static {
    jUnitCore.addListener(new RunListener() {
      @Override
      public void testFailure(Failure failure) {
        SCBFailure scbFailure = new SCBFailure(failure.getDescription(), failure.getException());
        failures.add(scbFailure);
        System.out.println(scbFailure.toString());
      }
    });
  }

  private ITJUnitUtils() {
  }

  public static int getRunCount() {
    return runCount.get();
  }

  public static Stack<String> getParents() {
    return parents;
  }

  public static void pushTransport(String transport) {
    ITJUnitUtils.transport = transport;
    addParent(transport);
  }

  public static String getTransport() {
    return transport;
  }

  public static void popTransport() {
    ITJUnitUtils.transport = null;
    popParent();
  }

  public static void addProducer(String producerName) {
    ITJUnitUtils.setProducerName(producerName);
    parents.add(producerName);
  }

  public static void popProducer() {
    ITJUnitUtils.setProducerName(null);
    parents.pop();
  }

  public static void addParent(String name) {
    parents.add(name);
  }

  public static void popParent() {
    parents.pop();
  }

  public static List<String> cloneParents() {
    return new ArrayList<>(parents);
  }

  public static List<SCBFailure> getFailures() {
    return failures;
  }

  public static void runFromPackage(String packageName) throws Throwable {
    Class<?>[] classes = findAllClassInPackage(packageName);
    run(classes);
  }

  public static void run(Class<?>... classes) throws Throwable {
    initClasses(classes);
    Result result = jUnitCore.run(classes);
    runCount.addAndGet(result.getRunCount());
  }

  public static void initClasses(Class<?>... classes) throws Throwable {
    for (Class<?> cls : classes) {
      for (Field field : FieldUtils.getAllFieldsList(cls)) {
        if (Consumers.class.isAssignableFrom(field.getType())
            || GateRestTemplate.class.isAssignableFrom(field.getType())
            || ITSCBRestTemplate.class.isAssignableFrom(field.getType())) {
          Object target = FieldUtils.readStaticField(field, true);
          MethodUtils.invokeMethod(target, "init");
        }
      }
    }
  }

  public static Class<?>[] findAllClassInPackage(String packageName) {
    try {
      return ClassPath.from(classLoader)
          .getTopLevelClassesRecursive(packageName).stream()
          .map(clsInfo -> clsInfo.load())
          .toArray(Class[]::new);
    } catch (IOException e) {
      throw new IllegalStateException("failed to find all classes in package " + packageName, e);
    }
  }

  public static void runWithHighwayAndRest(Class<?>... classes) throws Throwable {
    runWithTransports(Arrays.asList(Const.HIGHWAY, Const.RESTFUL), classes);
  }

  public static void runWithRest(Class<?>... classes) throws Throwable {
    runWithTransports(Arrays.asList(Const.RESTFUL), classes);
  }

  public static void runWithTransports(List<String> transports, Class<?>... classes) throws Throwable {
    for (String transport : transports) {
      ITJUnitUtils.pushTransport(transport);

      ITJUnitUtils.run(classes);

      ITJUnitUtils.popTransport();
    }
  }

  /**
   * <pre>
   * make easier to debug only one test case
   * normal test case:
   *   @Test
   *   public void test() {
   *     ...
   *   }
   *
   * when need to test one this case:
   * 1.start SC/it-edge and your test target manually
   * 2. change the code to
   *   @Test
   *   public void test() {
   *     ITJUnitUtils.initForDebug("it-producer", "rest");
   *     ...
   *   }
   * 3.run the case
   * 4.after finished the debug, remove code of ITJUnitUtils.initForDebug
   * </pre>
   * @param producerName
   * @param transport
   */
  public static void initForDebug(String producerName, String transport) {
    BeanUtils.init();
    ITJUnitUtils.addProducer(producerName);
    ITJUnitUtils.pushTransport(transport);
    try {
      Class<?> testClass = Class.forName(Thread.currentThread().getStackTrace()[2].getClassName());
      ITJUnitUtils.initClasses(testClass);
    } catch (Throwable e) {
      throw new IllegalStateException(e);
    }
  }
}
