package org.testng;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.BlockingQueue;

import org.testng.collections.ListMultiMap;
import org.testng.collections.Lists;
import org.testng.collections.Maps;
import org.testng.collections.Sets;
import org.testng.internal.AbstractParallelWorker;
import org.testng.internal.Attributes;
import org.testng.internal.ClassInfoMap;
import org.testng.internal.ConfigMethodArguments;
import org.testng.internal.ConfigMethodArguments.Builder;
import org.testng.internal.ConfigurationGroupMethods;
import org.testng.internal.DefaultListenerFactory;
import org.testng.internal.DynamicGraphHelper;
import org.testng.internal.GroupsHelper;
import org.testng.internal.IConfiguration;
import org.testng.internal.IInvoker;
import org.testng.internal.ITestResultNotifier;
import org.testng.internal.InstanceCreator;
import org.testng.internal.InvokedMethod;
import org.testng.internal.Invoker;
import org.testng.internal.TestMethodComparator;
import org.testng.internal.MethodGroupsHelper;
import org.testng.internal.MethodHelper;
import org.testng.internal.ResultMap;
import org.testng.internal.RunInfo;
import org.testng.internal.Systematiser;
import org.testng.internal.TestListenerHelper;
import org.testng.internal.TestMethodWorker;
import org.testng.internal.TestNGClassFinder;
import org.testng.internal.TestNGMethodFinder;
import org.testng.internal.Utils;
import org.testng.internal.XmlMethodSelector;
import org.testng.internal.annotations.IAnnotationFinder;
import org.testng.thread.ITestNGThreadPoolExecutor;
import org.testng.thread.IThreadWorkerFactory;
import org.testng.thread.IWorker;
import org.testng.junit.IJUnitTestRunner;
import org.testng.log4testng.Logger;
import org.testng.util.Strings;
import org.testng.xml.XmlClass;
import org.testng.xml.XmlInclude;
import org.testng.xml.XmlPackage;
import org.testng.xml.XmlTest;

import com.google.inject.Injector;
import com.google.inject.Module;

import javax.annotation.Nonnull;

import static org.testng.internal.MethodHelper.fixMethodsWithClass;

/** This class takes care of running one Test. */
public class TestRunner
    implements ITestContext, ITestResultNotifier, IThreadWorkerFactory<ITestNGMethod> {

  private static final String DEFAULT_PROP_OUTPUT_DIR = "test-output";
  private static final Logger LOGGER = Logger.getLogger(TestRunner.class);

  private final Comparator<ITestNGMethod> comparator;
  private ISuite m_suite;
  private XmlTest m_xmlTest;
  private String m_testName;

  private final GuiceHelper guiceHelper = new GuiceHelper(this);

  private List<XmlClass> m_testClassesFromXml = null;

  private IInvoker m_invoker = null;
  private IAnnotationFinder m_annotationFinder = null;

  /** ITestListeners support. */
  private List<ITestListener> m_testListeners = Lists.newArrayList();

  private Set<IConfigurationListener> m_configurationListeners = Sets.newHashSet();
  private final Set<IExecutionVisualiser> visualisers = Sets.newHashSet();

  private IConfigurationListener m_confListener = new ConfigurationListener();

  private Collection<IInvokedMethodListener> m_invokedMethodListeners = Lists.newArrayList();
  private final Map<Class<? extends IClassListener>, IClassListener> m_classListeners =
      Maps.newHashMap();
  private final Map<Class<? extends IDataProviderListener>, IDataProviderListener>
      m_dataProviderListeners;

  /**
   * All the test methods we found, associated with their respective classes. Note that these test
   * methods might belong to different classes. We pick which ones to run at runtime.
   */
  private ITestNGMethod[] m_allTestMethods = new ITestNGMethod[0];

  // Information about this test run

  private Date m_startDate = new Date();
  private Date m_endDate = null;

  /** A map to keep track of Class <-> IClass. */
  private Map<Class<?>, ITestClass> m_classMap = Maps.newLinkedHashMap();

  /** Where the reports will be created. */
  private String m_outputDirectory = DEFAULT_PROP_OUTPUT_DIR;

  // The XML method selector (groups/methods included/excluded in XML)
  private XmlMethodSelector m_xmlMethodSelector = new XmlMethodSelector();

  private static int m_verbose = 1;

  //
  // These next fields contain all the configuration methods found on this class.
  // At initialization time, they just contain all the various @Configuration methods
  // found in all the classes we are going to run.  When comes the time to run them,
  // only a subset of them are run:  those that are enabled and belong on the same class as
  // (or a parent of) the test class.
  //
  /** */
  private ITestNGMethod[] m_beforeSuiteMethods = {};

  private ITestNGMethod[] m_afterSuiteMethods = {};
  private ITestNGMethod[] m_beforeXmlTestMethods = {};
  private ITestNGMethod[] m_afterXmlTestMethods = {};
  private List<ITestNGMethod> m_excludedMethods = Lists.newArrayList();
  private ConfigurationGroupMethods m_groupMethods = null;

  // Meta groups
  private Map<String, List<String>> m_metaGroups = Maps.newHashMap();

  // All the tests that were run along with their result
  private IResultMap m_passedTests = new ResultMap();
  private IResultMap m_failedTests = new ResultMap();
  private IResultMap m_failedButWithinSuccessPercentageTests = new ResultMap();
  private IResultMap m_skippedTests = new ResultMap();

  private RunInfo m_runInfo = new RunInfo();

  // The host where this test was run, or null if run locally
  private String m_host;

  // Defined dynamically depending on <test preserve-order="true/false">
  private List<IMethodInterceptor> m_methodInterceptors;

  private ClassMethodMap m_classMethodMap;
  private TestNGClassFinder m_testClassFinder;
  private IConfiguration m_configuration;

  public enum PriorityWeight {
    groupByInstance,
    preserveOrder,
    priority,
    dependsOnGroups,
    dependsOnMethods
  }

  protected TestRunner(
      IConfiguration configuration,
      ISuite suite,
      XmlTest test,
      String outputDirectory,
      IAnnotationFinder finder,
      boolean skipFailedInvocationCounts,
      Collection<IInvokedMethodListener> invokedMethodListeners,
      List<IClassListener> classListeners,
      Comparator<ITestNGMethod> comparator,
      Map<Class<? extends IDataProviderListener>, IDataProviderListener> dataProviderListeners) {
    this.comparator = comparator;
    this.m_dataProviderListeners = Maps.newHashMap(dataProviderListeners);
    init(
        configuration,
        suite,
        test,
        outputDirectory,
        finder,
        skipFailedInvocationCounts,
        invokedMethodListeners,
        classListeners);
  }

  public TestRunner(
      IConfiguration configuration,
      ISuite suite,
      XmlTest test,
      boolean skipFailedInvocationCounts,
      Collection<IInvokedMethodListener> invokedMethodListeners,
      List<IClassListener> classListeners,
      Comparator<ITestNGMethod> comparator) {
    this.comparator = comparator;
    this.m_dataProviderListeners = Collections.emptyMap();
    init(
        configuration,
        suite,
        test,
        suite.getOutputDirectory(),
        suite.getAnnotationFinder(),
        skipFailedInvocationCounts,
        invokedMethodListeners,
        classListeners);
  }

  /**
   * This constructor is used by testng-remote, any changes related to it please contact with
   * testng-team.
   */
  public TestRunner(
      IConfiguration configuration,
      ISuite suite,
      XmlTest test,
      boolean skipFailedInvocationCounts,
      Collection<IInvokedMethodListener> invokedMethodListeners,
      List<IClassListener> classListeners) {
    this.comparator = Systematiser.getComparator();
    this.m_dataProviderListeners = Collections.emptyMap();
    init(
        configuration,
        suite,
        test,
        suite.getOutputDirectory(),
        suite.getAnnotationFinder(),
        skipFailedInvocationCounts,
        invokedMethodListeners,
        classListeners);
  }

  private void init(
      IConfiguration configuration,
      ISuite suite,
      XmlTest test,
      String outputDirectory,
      IAnnotationFinder annotationFinder,
      boolean skipFailedInvocationCounts,
      Collection<IInvokedMethodListener> invokedMethodListeners,
      List<IClassListener> classListeners) {
    m_configuration = configuration;
    m_xmlTest = test;
    m_suite = suite;
    m_testName = test.getName();
    m_host = suite.getHost();
    m_testClassesFromXml = test.getXmlClasses();
    setVerbose(test.getVerbose());

    boolean preserveOrder = test.getPreserveOrder();
    IMethodInterceptor builtinInterceptor =
        preserveOrder
            ? new PreserveOrderMethodInterceptor()
            : new InstanceOrderingMethodInterceptor();
    m_methodInterceptors = new ArrayList<>();
    // Add the built in interceptor as the first interceptor. That way we let our users determine
    // the final order
    // by plugging in their own custom interceptors as well.
    m_methodInterceptors.add(builtinInterceptor);

    List<XmlPackage> m_packageNamesFromXml = getAllPackages();
    for (XmlPackage xp : m_packageNamesFromXml) {
      m_testClassesFromXml.addAll(xp.getXmlClasses());
    }

    m_annotationFinder = annotationFinder;
    m_invokedMethodListeners = invokedMethodListeners;
    m_classListeners.clear();
    for (IClassListener classListener : classListeners) {
      m_classListeners.put(classListener.getClass(), classListener);
    }
    m_invoker =
        new Invoker(
            m_configuration,
            this,
            this,
            m_suite.getSuiteState(),
            skipFailedInvocationCounts,
            invokedMethodListeners,
            classListeners,
            m_dataProviderListeners.values());

    if (test.getParallel() != null) {
      log(
          "Running the tests in '" + test.getName() + "' with parallel mode:" + test.getParallel());
    }

    setOutputDirectory(outputDirectory);

    // Finish our initialization
    init();
  }

  /**
   * Returns all packages to use for the current test. This includes the test from the test
   * suite. Never returns null.
   */
  private List<XmlPackage> getAllPackages() {
    final List<XmlPackage> allPackages = Lists.newArrayList();
    final List<XmlPackage> suitePackages = this.m_xmlTest.getSuite().getPackages();
    if (suitePackages != null) {
      allPackages.addAll(suitePackages);
    }
    final List<XmlPackage> testPackages = this.m_xmlTest.getPackages();
    if (testPackages != null) {
      allPackages.addAll(testPackages);
    }
    return allPackages;
  }

  public IInvoker getInvoker() {
    return m_invoker;
  }

  public ITestNGMethod[] getBeforeSuiteMethods() {
    return m_beforeSuiteMethods;
  }

  public ITestNGMethod[] getAfterSuiteMethods() {
    return m_afterSuiteMethods;
  }

  public ITestNGMethod[] getBeforeTestConfigurationMethods() {
    return m_beforeXmlTestMethods;
  }

  public ITestNGMethod[] getAfterTestConfigurationMethods() {
    return m_afterXmlTestMethods;
  }

  private void init() {
    initMetaGroups(m_xmlTest);
    initRunInfo(m_xmlTest);

    // Init methods and class map
    // JUnit behavior is different and doesn't need this initialization step
    if (!m_xmlTest.isJUnit()) {
      initMethods();
    }

    initListeners();
    addConfigurationListener(m_confListener);
    for (IConfigurationListener cl : m_configuration.getConfigurationListeners()) {
      addConfigurationListener(cl);
    }
  }

  private void initListeners() {
    //
    // Find all the listener factories and collect all the listeners requested in a
    // @Listeners annotation.
    //
    Set<Class<? extends ITestNGListener>> listenerClasses = Sets.newHashSet();
    Class<? extends ITestNGListenerFactory> listenerFactoryClass = null;

    for (IClass cls : getTestClasses()) {
      Class<?> realClass = cls.getRealClass();
      TestListenerHelper.ListenerHolder listenerHolder =
          TestListenerHelper.findAllListeners(realClass, m_annotationFinder);
      if (listenerFactoryClass == null) {
        listenerFactoryClass = listenerHolder.getListenerFactoryClass();
      }
      listenerClasses.addAll(listenerHolder.getListenerClasses());
    }

    if (listenerFactoryClass == null) {
      listenerFactoryClass = DefaultListenerFactory.class;
    }

    //
    // Now we have all the listeners collected from @Listeners and at most one
    // listener factory collected from a class implementing ITestNGListenerFactory.
    // Instantiate all the requested listeners.
    //

    ITestNGListenerFactory factory =
        TestListenerHelper.createListenerFactory(m_testClassFinder, listenerFactoryClass);

    // Instantiate all the listeners
    for (Class<? extends ITestNGListener> c : listenerClasses) {
      if (IClassListener.class.isAssignableFrom(c) && m_classListeners.containsKey(c)) {
        continue;
      }
      ITestNGListener listener = factory.createListener(c);

      addListener(listener);
    }
  }

  /** Initialize meta groups */
  private void initMetaGroups(XmlTest xmlTest) {
    Map<String, List<String>> metaGroups = xmlTest.getMetaGroups();

    for (Map.Entry<String, List<String>> entry : metaGroups.entrySet()) {
      addMetaGroup(entry.getKey(), entry.getValue());
    }
  }

  private void initRunInfo(final XmlTest xmlTest) {
    // Groups
    m_xmlMethodSelector.setIncludedGroups(createGroups(m_xmlTest.getIncludedGroups()));
    m_xmlMethodSelector.setExcludedGroups(createGroups(m_xmlTest.getExcludedGroups()));
    m_xmlMethodSelector.setScript(m_xmlTest.getScript());

    // Methods
    m_xmlMethodSelector.setXmlClasses(m_xmlTest.getXmlClasses());

    m_runInfo.addMethodSelector(m_xmlMethodSelector, 10);

    // Add user-specified method selectors (only class selectors, we can ignore
    // script selectors here)
    if (null != xmlTest.getMethodSelectors()) {
      for (org.testng.xml.XmlMethodSelector selector : xmlTest.getMethodSelectors()) {
        if (selector.getClassName() != null) {
          IMethodSelector s = InstanceCreator.createSelector(selector);

          m_runInfo.addMethodSelector(s, selector.getPriority());
        }
      }
    }
  }

  private void initMethods() {

    //
    // Calculate all the methods we need to invoke
    //
    List<ITestNGMethod> beforeClassMethods = Lists.newArrayList();
    List<ITestNGMethod> testMethods = Lists.newArrayList();
    List<ITestNGMethod> afterClassMethods = Lists.newArrayList();
    List<ITestNGMethod> beforeSuiteMethods = Lists.newArrayList();
    List<ITestNGMethod> afterSuiteMethods = Lists.newArrayList();
    List<ITestNGMethod> beforeXmlTestMethods = Lists.newArrayList();
    List<ITestNGMethod> afterXmlTestMethods = Lists.newArrayList();

    ClassInfoMap classMap = new ClassInfoMap(m_testClassesFromXml);
    m_testClassFinder =
        new TestNGClassFinder(
            classMap, Maps.newHashMap(), m_configuration, this, m_dataProviderListeners);
    ITestMethodFinder testMethodFinder =
        new TestNGMethodFinder(m_runInfo, m_annotationFinder, comparator);

    m_runInfo.setTestMethods(testMethods);

    //
    // Initialize TestClasses
    //
    IClass[] classes = m_testClassFinder.findTestClasses();

    for (IClass ic : classes) {

      // Create TestClass
      ITestClass tc =
          new TestClass(
              ic,
              testMethodFinder,
              m_annotationFinder,
              m_xmlTest,
              classMap.getXmlClass(ic.getRealClass()), m_testClassFinder.getFactoryCreationFailedMessage());
      m_classMap.put(ic.getRealClass(), tc);
    }

    //
    // Calculate groups methods
    //
    Map<String, List<ITestNGMethod>> beforeGroupMethods =
        MethodGroupsHelper.findGroupsMethods(m_classMap.values(), true);
    Map<String, List<ITestNGMethod>> afterGroupMethods =
        MethodGroupsHelper.findGroupsMethods(m_classMap.values(), false);

    //
    // Walk through all the TestClasses, store their method
    // and initialize them with the correct ITestClass
    //
    for (ITestClass tc : m_classMap.values()) {
      fixMethodsWithClass(tc.getTestMethods(), tc, testMethods);
      fixMethodsWithClass(tc.getBeforeClassMethods(), tc, beforeClassMethods);
      fixMethodsWithClass(tc.getBeforeTestMethods(), tc, null);
      fixMethodsWithClass(tc.getAfterTestMethods(), tc, null);
      fixMethodsWithClass(tc.getAfterClassMethods(), tc, afterClassMethods);
      fixMethodsWithClass(tc.getBeforeSuiteMethods(), tc, beforeSuiteMethods);
      fixMethodsWithClass(tc.getAfterSuiteMethods(), tc, afterSuiteMethods);
      fixMethodsWithClass(tc.getBeforeTestConfigurationMethods(), tc, beforeXmlTestMethods);
      fixMethodsWithClass(tc.getAfterTestConfigurationMethods(), tc, afterXmlTestMethods);
      fixMethodsWithClass(
          tc.getBeforeGroupsMethods(),
          tc,
          MethodHelper.uniqueMethodList(beforeGroupMethods.values()));
      fixMethodsWithClass(
          tc.getAfterGroupsMethods(),
          tc,
          MethodHelper.uniqueMethodList(afterGroupMethods.values()));
    }

    //
    // Sort the methods
    //
    m_beforeSuiteMethods =
        MethodHelper.collectAndOrderMethods(
            beforeSuiteMethods,
            false /* forTests */,
            m_runInfo,
            m_annotationFinder,
            true /* unique */,
            m_excludedMethods,
            comparator);

    m_beforeXmlTestMethods =
        MethodHelper.collectAndOrderMethods(
            beforeXmlTestMethods,
            false /* forTests */,
            m_runInfo,
            m_annotationFinder,
            true /* unique (CQ added by me)*/,
            m_excludedMethods,
            comparator);

    m_allTestMethods =
        MethodHelper.collectAndOrderMethods(
            testMethods,
            true /* forTest? */,
            m_runInfo,
            m_annotationFinder,
            false /* unique */,
            m_excludedMethods,
            comparator);
    m_classMethodMap = new ClassMethodMap(Arrays.asList(m_allTestMethods), m_xmlMethodSelector);

    m_afterXmlTestMethods =
        MethodHelper.collectAndOrderMethods(
            afterXmlTestMethods,
            false /* forTests */,
            m_runInfo,
            m_annotationFinder,
            true /* unique (CQ added by me)*/,
            m_excludedMethods,
            comparator);

    m_afterSuiteMethods =
        MethodHelper.collectAndOrderMethods(
            afterSuiteMethods,
            false /* forTests */,
            m_runInfo,
            m_annotationFinder,
            true /* unique */,
            m_excludedMethods,
            comparator);
    // shared group methods
    m_groupMethods =
        new ConfigurationGroupMethods(m_allTestMethods, beforeGroupMethods, afterGroupMethods);
  }

  public Collection<ITestClass> getTestClasses() {
    return m_classMap.values();
  }

  public void setTestName(String name) {
    m_testName = name;
  }

  public void setOutputDirectory(String od) {
    m_outputDirectory = od;
  }

  private void addMetaGroup(String name, List<String> groupNames) {
    m_metaGroups.put(name, groupNames);
  }

  private Map<String, String> createGroups(List<String> groups) {
    return GroupsHelper.createGroups(m_metaGroups, groups);
  }

  /**
   * The main entry method for TestRunner.
   *
   * <p>This is where all the hard work is done: - Invoke configuration methods - Invoke test
   * methods - Catch exceptions - Collect results - Invoke listeners - etc...
   */
  public void run() {
    beforeRun();

    try {
      XmlTest test = getTest();
      if (test.isJUnit()) {
        privateRunJUnit();
      } else {
        privateRun(test);
      }
    } finally {
      afterRun();
    }
  }

  /** Before run preparements. */
  private void beforeRun() {
    //
    // Log the start date
    //
    m_startDate = new Date(System.currentTimeMillis());

    // Log start
    logStart();

    // Invoke listeners
    fireEvent(true /*start*/);

    // invoke @BeforeTest
    ITestNGMethod[] testConfigurationMethods = getBeforeTestConfigurationMethods();
    invokeTestConfigurations(testConfigurationMethods);
  }

  private void invokeTestConfigurations(ITestNGMethod[] testConfigurationMethods) {
    if (null != testConfigurationMethods && testConfigurationMethods.length > 0) {
      ConfigMethodArguments arguments = new Builder()
          .usingConfigMethodsAs(testConfigurationMethods)
          .forSuite(m_xmlTest.getSuite())
          .usingParameters(m_xmlTest.getAllParameters())
          .build();
      m_invoker.getConfigInvoker().invokeConfigurations(arguments);
    }
  }

  private void privateRunJUnit() {
    final ClassInfoMap cim = new ClassInfoMap(m_testClassesFromXml, false);
    final Set<Class<?>> classes = cim.getClasses();
    final List<ITestNGMethod> runMethods = Lists.newArrayList();
    List<IWorker<ITestNGMethod>> workers = Lists.newArrayList();
    // FIXME: directly referencing JUnitTestRunner which uses JUnit classes
    // may result in an class resolution exception under different JVMs
    // The resolution process is not specified in the JVM spec with a specific implementation,
    // so it can be eager => failure
    workers.add(
        new IWorker<ITestNGMethod>() {
          /** @see TestMethodWorker#getTimeOut() */
          @Override
          public long getTimeOut() {
            return 0;
          }

          /** @see java.lang.Runnable#run() */
          @Override
          public void run() {
            for (Class<?> tc : classes) {
              List<XmlInclude> includedMethods = cim.getXmlClass(tc).getIncludedMethods();
              List<String> methods = Lists.newArrayList();
              for (XmlInclude inc : includedMethods) {
                methods.add(inc.getName());
              }
              IJUnitTestRunner tr = IJUnitTestRunner.createTestRunner(TestRunner.this);
              tr.setInvokedMethodListeners(m_invokedMethodListeners);
              try {
                tr.run(tc, methods.toArray(new String[0]));
              } catch (Exception ex) {
                LOGGER.error(ex.getMessage(), ex);
              } finally {
                runMethods.addAll(tr.getTestMethods());
              }
            }
          }

          @Override
          public List<ITestNGMethod> getTasks() {
            throw new TestNGException("JUnit not supported");
          }

          @Override
          public int getPriority() {
            if (m_allTestMethods.length == 1) {
              return m_allTestMethods[0].getPriority();
            } else {
              return 0;
            }
          }

          @Override
          public int compareTo(@Nonnull IWorker<ITestNGMethod> other) {
            return getPriority() - other.getPriority();
          }
        });

    runJUnitWorkers(workers);
    m_allTestMethods = runMethods.toArray(new ITestNGMethod[0]);
  }

  private static Comparator<ITestNGMethod> newComparator(boolean needPrioritySort) {
    return needPrioritySort ? new TestMethodComparator() : null;
  }

  private boolean sortOnPriority(ITestNGMethod[] interceptedOrder) {
    return m_methodInterceptors.size() > 1 ||
        Arrays.stream(interceptedOrder).anyMatch(m -> m.getPriority() != 0);
  }

  // If any of the test methods specify a priority other than the default, we'll need to be able to sort them.
  private static BlockingQueue<Runnable> newQueue(boolean needPrioritySort) {
    return needPrioritySort ? new PriorityBlockingQueue<>() : new LinkedBlockingQueue<>();
  }

  /**
   * Main method that create a graph of methods and then pass it to the graph executor to run them.
   */
  private void privateRun(XmlTest xmlTest) {
    boolean parallel = xmlTest.getParallel().isParallel();

    // parallel
    int threadCount = parallel ? xmlTest.getThreadCount() : 1;
    // Make sure we create a graph based on the intercepted methods, otherwise an interceptor
    // removing methods would cause the graph never to terminate (because it would expect
    // termination from methods that never get invoked).
    ITestNGMethod[] interceptedOrder = intercept(m_allTestMethods);
    IDynamicGraph<ITestNGMethod> graph =
        DynamicGraphHelper.createDynamicGraph(interceptedOrder, getCurrentXmlTest());
    graph.setVisualisers(this.visualisers);
    // In some cases, additional sorting is needed to make sure tests run in the appropriate order.
    // If the user specified a method interceptor, or if we have any methods that have a non-default
    // priority on them, we need to sort.
    boolean needPrioritySort = sortOnPriority(interceptedOrder);
    Comparator<ITestNGMethod> methodComparator = newComparator(needPrioritySort);
    if (parallel) {
      if (graph.getNodeCount() <= 0) {
        return;
      }
      ITestNGThreadPoolExecutor executor =
          this.m_configuration.getExecutorFactory().newTestMethodExecutor(
              "test=" + xmlTest.getName(),
              graph,
              this,
              threadCount,
              threadCount,
              0,
              TimeUnit.MILLISECONDS,
              newQueue(needPrioritySort),
              methodComparator);
      executor.run();
      try {
        long timeOut = m_xmlTest.getTimeOut(XmlTest.DEFAULT_TIMEOUT_MS);
        Utils.log(
            "TestRunner",
            2,
            "Starting executor for test "
                + m_xmlTest.getName()
                + " with time out:"
                + timeOut
                + " milliseconds.");
        executor.awaitTermination(timeOut, TimeUnit.MILLISECONDS);
        executor.shutdownNow();
      } catch (InterruptedException handled) {
        LOGGER.error(handled.getMessage(), handled);
        Thread.currentThread().interrupt();
      }
      return;
    }
    List<ITestNGMethod> freeNodes = graph.getFreeNodes();

    if (graph.getNodeCount() > 0 && freeNodes.isEmpty()) {
      throw new TestNGException("No free nodes found in:" + graph);
    }

    while (!freeNodes.isEmpty()) {
      if (needPrioritySort) {
        freeNodes.sort(methodComparator);
        // Since this is sequential, let's run one at a time and fetch/sort freeNodes after each method.
        // Future task: To optimize this, we can only update freeNodes after running a test that another test is dependent upon.
        freeNodes = freeNodes.subList(0, 1);
      }
      createWorkers(freeNodes).forEach(Runnable::run);
      graph.setStatus(freeNodes, IDynamicGraph.Status.FINISHED);
      freeNodes = graph.getFreeNodes();
    }
  }

  /** Apply the method interceptor (if applicable) to the list of methods. */
  private ITestNGMethod[] intercept(ITestNGMethod[] methods) {

    List<IMethodInstance> methodInstances =
        MethodHelper.methodsToMethodInstances(Arrays.asList(methods));

    for (IMethodInterceptor m_methodInterceptor : m_methodInterceptors) {
      methodInstances = m_methodInterceptor.intercept(methodInstances, this);
    }

    List<ITestNGMethod> result = MethodHelper.methodInstancesToMethods(methodInstances);

    // Since an interceptor is involved, we would need to ensure that the ClassMethodMap object is
    // in sync with the
    // output of the interceptor, else @AfterClass doesn't get executed at all when interceptors are
    // involved.
    // so let's update the current classMethodMap object with the list of methods obtained from the
    // interceptor.
    this.m_classMethodMap = new ClassMethodMap(result, null);

    ITestNGMethod[] resultArray = result.toArray(new ITestNGMethod[0]);

    // Check if an interceptor had altered the effective test method count. If yes, then we need to
    // update our configurationGroupMethod object with that information.
    if (resultArray.length != m_groupMethods.getAllTestMethods().length) {
      m_groupMethods =
          new ConfigurationGroupMethods(
              resultArray,
              m_groupMethods.getBeforeGroupsMethods(),
              m_groupMethods.getAfterGroupsMethods());
    }

    // If the user specified a method interceptor, whatever that returns is the order we're going
    // to run things in. Set the intercepted priority for that case.
    // There's a built-in interceptor, so look for more than one.
    if (m_methodInterceptors.size() > 1) {
      for (int i = 0; i < resultArray.length; ++i) {
        resultArray[i].setInterceptedPriority(i);
      }
    }

    return resultArray;
  }

  /**
   * Create a list of workers to run the methods passed in parameter. Each test method is run in its
   * own worker except in the following cases: - The method belongs to a class that
   * has @Test(sequential=true) - The parallel attribute is set to "classes" In both these cases,
   * all the methods belonging to that class will then be put in the same worker in order to run in
   * the same thread.
   */
  @Override
  public List<IWorker<ITestNGMethod>> createWorkers(List<ITestNGMethod> methods) {
    AbstractParallelWorker.Arguments args =
        new AbstractParallelWorker.Arguments.Builder()
            .classMethodMap(this.m_classMethodMap)
            .configMethods(this.m_groupMethods)
            .finder(this.m_annotationFinder)
            .invoker(this.m_invoker)
            .methods(methods)
            .testContext(this)
            .listeners(this.m_classListeners.values())
            .build();
    return AbstractParallelWorker.newWorker(m_xmlTest.getParallel(), m_xmlTest.getGroupByInstances()).createWorkers(args);
  }

  //
  // Invoke the workers
  //
  private void runJUnitWorkers(List<? extends IWorker<ITestNGMethod>> workers) {
    // Sequential run
    workers.forEach(Runnable::run);
  }

  private void afterRun() {
    // invoke @AfterTest
    ITestNGMethod[] testConfigurationMethods = getAfterTestConfigurationMethods();
    invokeTestConfigurations(testConfigurationMethods);

    //
    // Log the end date
    //
    m_endDate = new Date(System.currentTimeMillis());

    dumpInvokedMethods();

    // Invoke listeners
    fireEvent(false /*stop*/);
  }

  /** Logs the beginning of the {@link #beforeRun()} . */
  private void logStart() {
    log(
        "Running test "
            + m_testName
            + " on "
            + m_classMap.size()
            + " "
            + " classes, "
            + " included groups:["
            + Strings.valueOf(m_xmlMethodSelector.getIncludedGroups())
            + "] excluded groups:["
            + Strings.valueOf(m_xmlMethodSelector.getExcludedGroups())
            + "]");

    if (getVerbose() >= 3) {
      for (ITestClass tc : m_classMap.values()) {
        ((TestClass) tc).dump();
      }
    }
  }

  /**
   * Trigger the start/finish event.
   *
   * @param isStart <tt>true</tt> if the event is for start, <tt>false</tt> if the event is for
   *     finish
   */
  private void fireEvent(boolean isStart) {
    for (ITestListener itl : m_testListeners) {
      if (isStart) {
        itl.onStart(this);
      } else {
        itl.onFinish(this);
      }
    }
  }

  /////
  // ITestContext
  //
  @Override
  public String getName() {
    return m_testName;
  }

  /** @return Returns the startDate. */
  @Override
  public Date getStartDate() {
    return m_startDate;
  }

  /** @return Returns the endDate. */
  @Override
  public Date getEndDate() {
    return m_endDate;
  }

  @Override
  public IResultMap getPassedTests() {
    return m_passedTests;
  }

  @Override
  public IResultMap getSkippedTests() {
    return m_skippedTests;
  }

  @Override
  public IResultMap getFailedTests() {
    return m_failedTests;
  }

  @Override
  public IResultMap getFailedButWithinSuccessPercentageTests() {
    return m_failedButWithinSuccessPercentageTests;
  }

  @Override
  public String[] getIncludedGroups() {
    Map<String, String> ig = m_xmlMethodSelector.getIncludedGroups();
    return ig.values().toArray(new String[0]);
  }

  @Override
  public String[] getExcludedGroups() {
    Map<String, String> eg = m_xmlMethodSelector.getExcludedGroups();
    return eg.values().toArray(new String[0]);
  }

  @Override
  public String getOutputDirectory() {
    return m_outputDirectory;
  }

  /** @return Returns the suite. */
  @Override
  public ISuite getSuite() {
    return m_suite;
  }

  @Override
  public ITestNGMethod[] getAllTestMethods() {
    return m_allTestMethods;
  }

  @Override
  public String getHost() {
    return m_host;
  }

  @Override
  public Collection<ITestNGMethod> getExcludedMethods() {
    Map<ITestNGMethod, ITestNGMethod> vResult = Maps.newHashMap();

    for (ITestNGMethod m : m_excludedMethods) {
      vResult.put(m, m);
    }

    return vResult.keySet();
  }

  /** @see org.testng.ITestContext#getFailedConfigurations() */
  @Override
  public IResultMap getFailedConfigurations() {
    return m_failedConfigurations;
  }

  /** @see org.testng.ITestContext#getPassedConfigurations() */
  @Override
  public IResultMap getPassedConfigurations() {
    return m_passedConfigurations;
  }

  /** @see org.testng.ITestContext#getSkippedConfigurations() */
  @Override
  public IResultMap getSkippedConfigurations() {
    return m_skippedConfigurations;
  }

  @Override
  public void addPassedTest(ITestNGMethod tm, ITestResult tr) {
    m_passedTests.addResult(tr, tm);
  }

  @Override
  public Set<ITestResult> getPassedTests(ITestNGMethod tm) {
    return m_passedTests.getResults(tm);
  }

  @Override
  public Set<ITestResult> getFailedTests(ITestNGMethod tm) {
    return m_failedTests.getResults(tm);
  }

  @Override
  public Set<ITestResult> getSkippedTests(ITestNGMethod tm) {
    return m_skippedTests.getResults(tm);
  }

  @Override
  public void addSkippedTest(ITestNGMethod tm, ITestResult tr) {
    m_skippedTests.addResult(tr, tm);
  }

  @Override
  public void addInvokedMethod(InvokedMethod im) {
    m_invokedMethods.add(im);
  }

  @Override
  public void addFailedTest(ITestNGMethod testMethod, ITestResult result) {
    logFailedTest(testMethod, result, false /* withinSuccessPercentage */);
  }

  @Override
  public void addFailedButWithinSuccessPercentageTest(
      ITestNGMethod testMethod, ITestResult result) {
    logFailedTest(testMethod, result, true /* withinSuccessPercentage */);
  }

  @Override
  public XmlTest getTest() {
    return m_xmlTest;
  }

  @Override
  public List<ITestListener> getTestListeners() {
    return m_testListeners;
  }

  @Override
  public List<IConfigurationListener> getConfigurationListeners() {
    List<IConfigurationListener> listeners = Lists.newArrayList(m_configurationListeners);
    for (IConfigurationListener each : this.m_configuration.getConfigurationListeners()) {
      boolean duplicate = false;
      for (IConfigurationListener listener : listeners) {
        if (each.getClass().equals(listener.getClass())) {
          duplicate = true;
          break;
        }
      }
      if (!duplicate) {
        listeners.add(each);
      }
    }
    return Lists.newArrayList(listeners);
  }

  private void logFailedTest(
      ITestNGMethod method, ITestResult tr, boolean withinSuccessPercentage) {

    if (withinSuccessPercentage) {
      m_failedButWithinSuccessPercentageTests.addResult(tr, method);
    } else {
      m_failedTests.addResult(tr, method);
    }
  }

  private static void log(String s) {
    Utils.log("TestRunner", 3, s);
  }

  public static int getVerbose() {
    return m_verbose;
  }

  public void setVerbose(int n) {
    m_verbose = n;
  }

  // TODO: This method needs to be removed and we need to be leveraging addListener().
  // Investigate and fix this.
  void addTestListener(ITestListener listener) {
    if (!m_testListeners.contains(listener)) {
      m_testListeners.add(listener);
    }
  }

  public void addListener(ITestNGListener listener) {
    // TODO a listener may be added many times if it implements many interfaces
    if (listener instanceof IMethodInterceptor) {
      m_methodInterceptors.add((IMethodInterceptor) listener);
    }
    if (listener instanceof ITestListener) {
      // At this point, the field m_testListeners has already been used in the creation
      addTestListener((ITestListener) listener);
    }
    if (listener instanceof IClassListener) {
      IClassListener classListener = (IClassListener) listener;
      if (!m_classListeners.containsKey(classListener.getClass())) {
        m_classListeners.put(classListener.getClass(), classListener);
      }
    }
    if (listener instanceof IConfigurationListener) {
      addConfigurationListener((IConfigurationListener) listener);
    }
    if (listener instanceof IConfigurable) {
      m_configuration.setConfigurable((IConfigurable) listener);
    }
    if (listener instanceof IHookable) {
      m_configuration.setHookable((IHookable) listener);
    }
    if (listener instanceof IExecutionListener) {
      IExecutionListener iel = (IExecutionListener) listener;
      if (m_configuration.addExecutionListenerIfAbsent(iel)) {
        iel.onExecutionStart();
      }
    }
    if (listener instanceof IDataProviderListener) {
      IDataProviderListener dataProviderListener = (IDataProviderListener) listener;
      m_dataProviderListeners.put(dataProviderListener.getClass(), dataProviderListener);
    }

    if (listener instanceof IExecutionVisualiser) {
      IExecutionVisualiser l = (IExecutionVisualiser) listener;
      visualisers.add(l);
    }
    m_suite.addListener(listener);
  }

  void addConfigurationListener(IConfigurationListener icl) {
    m_configurationListeners.add(icl);
  }

  private final Collection<IInvokedMethod> m_invokedMethods = new ConcurrentLinkedQueue<>();

  private void dumpInvokedMethods() {
    MethodHelper.dumpInvokedMethodsInfoToConsole(m_invokedMethods, getVerbose());
  }

  public List<ITestNGMethod> getInvokedMethods() {
    return MethodHelper.invokedMethodsToMethods(m_invokedMethods);
  }

  private IResultMap m_passedConfigurations = new ResultMap();
  private IResultMap m_skippedConfigurations = new ResultMap();
  private IResultMap m_failedConfigurations = new ResultMap();

  private class ConfigurationListener implements IConfigurationListener {
    @Override
    public void beforeConfiguration(ITestResult tr) {}

    @Override
    public void onConfigurationFailure(ITestResult itr) {
      m_failedConfigurations.addResult(itr, itr.getMethod());
    }

    @Override
    public void onConfigurationSkip(ITestResult itr) {
      m_skippedConfigurations.addResult(itr, itr.getMethod());
    }

    @Override
    public void onConfigurationSuccess(ITestResult itr) {
      m_passedConfigurations.addResult(itr, itr.getMethod());
    }
  }

  void addMethodInterceptor(IMethodInterceptor methodInterceptor) {
      //avoid to add interceptor twice when the defined listeners implements both ITestListener and IMethodInterceptor.
      if (!m_methodInterceptors.contains(methodInterceptor)) {
          m_methodInterceptors.add(methodInterceptor);
    }
  }

  @Override
  public XmlTest getCurrentXmlTest() {
    return m_xmlTest;
  }

  private IAttributes m_attributes = new Attributes();

  @Override
  public Object getAttribute(String name) {
    return m_attributes.getAttribute(name);
  }

  @Override
  public void setAttribute(String name, Object value) {
    m_attributes.setAttribute(name, value);
  }

  @Override
  public Set<String> getAttributeNames() {
    return m_attributes.getAttributeNames();
  }

  @Override
  public Object removeAttribute(String name) {
    return m_attributes.removeAttribute(name);
  }

  private ListMultiMap<Class<? extends Module>, Module> m_guiceModules = Maps.newListMultiMap();

  @Override
  public List<Module> getGuiceModules(Class<? extends Module> cls) {
    return m_guiceModules.get(cls);
  }

  private Map<List<Module>, Injector> m_injectors = Maps.newHashMap();

  @Override
  public Injector getInjector(List<Module> moduleInstances) {
    return m_injectors.get(moduleInstances);
  }

  @Override
  public Injector getInjector(IClass iClass) {
    return guiceHelper.getInjector(iClass);
  }

  @Override
  public void addInjector(List<Module> moduleInstances, Injector injector) {
    m_injectors.put(moduleInstances, injector);
  }
}
