package org.testng.internal.invokers;

import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.BiPredicate;
import org.testng.IClass;
import org.testng.ITestClass;
import org.testng.ITestNGMethod;
import org.testng.collections.Lists;
import org.testng.collections.Sets;
import org.testng.internal.ConfigurationMethod;
import org.testng.internal.Utils;

/** Collections of helper methods to help deal with TestNG configuration methods */
class TestNgMethodUtils {

  private TestNgMethodUtils() {
    // Utility class. So hiding the constructor.
  }

  /**
   * A helper method that checks to see if a method is a configuration method or not.
   *
   * @param method - A {@link ITestNGMethod} object which needs to be checked.
   * @return - <code>true</code> if the method is a configuration method and false if its a test
   *     method.
   */
  static boolean isConfigurationMethod(ITestNGMethod method) {
    return isConfigurationMethod(method, false);
  }

  /**
   * A helper method that checks to see if a method is a configuration method or not.
   *
   * @param method - A {@link ITestNGMethod} object which needs to be checked.
   * @param includeGroupConfigs - <code>true</code> if before/after group configuration annotations
   *     are also to be taken into consideration.
   * @return - <code>true</code> if the method is a configuration method and false if its a test
   *     method.
   */
  private static boolean isConfigurationMethod(ITestNGMethod method, boolean includeGroupConfigs) {
    boolean flag =
        method.isBeforeMethodConfiguration()
            || method.isAfterMethodConfiguration()
            || method.isBeforeTestConfiguration()
            || method.isAfterTestConfiguration()
            || method.isBeforeClassConfiguration()
            || method.isAfterClassConfiguration()
            || method.isBeforeSuiteConfiguration()
            || method.isAfterSuiteConfiguration();
    if (includeGroupConfigs) {
      flag = flag || method.isBeforeGroupsConfiguration() || method.isAfterGroupsConfiguration();
    }
    return flag;
  }

  /**
   * A helper method which checks if a given method is a configuration method and is part of list of
   * TestNG methods
   *
   * @param method - A {@link ITestNGMethod} object which needs to be checked.
   * @param methods - A List of {@link ITestNGMethod} in which the check needs to be done.
   * @return - <code>true</code> if the method is a configuration method and exists in the list of
   *     methods passed.
   */
  private static boolean containsConfigurationMethod(
      ITestNGMethod method, List<ITestNGMethod> methods) {
    return isConfigurationMethod(method, true) && methods.contains(method);
  }

  static ITestNGMethod[] filterBeforeTestMethods(
      Object instance, ITestClass testClass, BiPredicate<ITestNGMethod, IClass> predicate) {
    return filterMethods(instance, testClass, testClass.getBeforeTestMethods(), predicate);
  }

  static ITestNGMethod[] filterAfterTestMethods(
      Object instance, ITestClass testClass, BiPredicate<ITestNGMethod, IClass> predicate) {
    return filterMethods(instance, testClass, testClass.getAfterTestMethods(), predicate);
  }

  /** @return Only the ITestNGMethods applicable for this testClass */
  static ITestNGMethod[] filterMethods(
      Object instance,
      IClass testClass,
      ITestNGMethod[] methods,
      BiPredicate<ITestNGMethod, IClass> predicate) {
    List<ITestNGMethod> vResult = Lists.newArrayList();
    for (ITestNGMethod tm : methods) {
      String msg;
      if ((predicate.test(tm, testClass) && isSameInstance(tm, instance))
          && (!TestNgMethodUtils.containsConfigurationMethod(tm, vResult))) {
        msg = Utils.getVerbose() < 10 ? "" : "Keeping method " + tm + " for class " + testClass;
        vResult.add(tm);
      } else {
        msg =
            Utils.getVerbose() < 10 ? "" : "Filtering out method " + tm + " for class " + testClass;
      }
      Utils.log("Invoker " + Thread.currentThread().hashCode(), 10, msg);
    }
    return vResult.toArray(new ITestNGMethod[0]);
  }

  private static boolean isSameInstance(ITestNGMethod tm, Object instance) {
    if (instance == null) {
      return true;
    }
    Object tmObject = Optional.ofNullable(tm).map(ITestNGMethod::getInstance).orElse(new Object());
    return instance.equals(tmObject);
  }

  static ITestNGMethod[] filterSetupConfigurationMethods(
      ITestNGMethod tm, ITestNGMethod[] methods) {
    List<ITestNGMethod> result = Lists.newArrayList();
    for (ITestNGMethod m : methods) {
      ConfigurationMethod cm = (ConfigurationMethod) m;
      if (doesSetupMethodPassFirstTimeFilter(cm, tm) && doesConfigMethodPassGroupFilters(cm, tm)) {
        result.add(m);
      }
    }
    return result.toArray(new ITestNGMethod[0]);
  }

  static ITestNGMethod[] filterTeardownConfigurationMethods(
      ITestNGMethod tm, ITestNGMethod[] methods) {
    List<ITestNGMethod> result = Lists.newArrayList();
    for (ITestNGMethod m : methods) {
      ConfigurationMethod cm = (ConfigurationMethod) m;
      if (doesTeardownMethodPassLastTimeFilter(cm, tm)
          && doesConfigMethodPassGroupFilters(cm, tm)) {
        result.add(m);
      }
    }
    return result.toArray(new ITestNGMethod[0]);
  }

  /**
   * @param tm - The {@link ITestNGMethod} object which is to be tested.
   * @return - <code>true</code> if the method depends on other methods and cannot be run
   *     independently.
   */
  static boolean cannotRunMethodIndependently(ITestNGMethod tm) {
    String[] methods = tm.getMethodsDependedUpon();
    return null != methods && methods.length > 0;
  }

  // Creates a token for tracking a unique invocation of a method on an instance.
  // Is used when configFailurePolicy=continue.
  static Object getMethodInvocationToken(ITestNGMethod method, Object instance) {
    return String.format(
        "%s+%d+%d",
        instance.toString(),
        method.getCurrentInvocationCount(),
        method.getParameterInvocationCount());
  }

  private static boolean doesSetupMethodPassFirstTimeFilter(
      ConfigurationMethod cm, ITestNGMethod tm) {
    return !cm.isFirstTimeOnly() || (cm.isFirstTimeOnly() && tm.getCurrentInvocationCount() == 0);
  }

  private static boolean doesTeardownMethodPassLastTimeFilter(
      ConfigurationMethod cm, ITestNGMethod tm) {
    return !cm.isLastTimeOnly() || (cm.isLastTimeOnly() && !tm.hasMoreInvocation());
  }

  private static boolean doesConfigMethodPassGroupFilters(
      ConfigurationMethod cm, ITestNGMethod tm) {
    String[] groupFilters = cm.getGroupFilters();
    if (groupFilters.length == 0) {
      return true; // no group filters means all groups accepted
    }
    String[] groups = tm.getGroups();
    if (groups.length == 0) {
      return false; // a method with no groups won't pass any filter
    }
    return !Collections.disjoint(Sets.newHashSet(groups), Sets.newHashSet(groupFilters));
  }
}
