package org.testng.internal.annotations;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Predicate;
import org.testng.ITestNGMethod;
import org.testng.ITestObjectFactory;
import org.testng.annotations.IAnnotation;
import org.testng.annotations.IConfigurationAnnotation;
import org.testng.annotations.IDataProviderAnnotation;
import org.testng.annotations.IFactoryAnnotation;
import org.testng.annotations.IParametersAnnotation;
import org.testng.annotations.ITestAnnotation;
import org.testng.collections.Maps;
import org.testng.internal.ConstructorOrMethod;
import org.testng.internal.TestNGMethod;
import org.testng.internal.Utils;
import org.testng.internal.reflect.ReflectionHelper;
import org.testng.log4testng.Logger;
import org.testng.xml.XmlTest;

/**
 * Helper methods to find @Test and @Configuration tags. They minimize the amount of casting we need
 * to do.
 */
public class AnnotationHelper {
  private static final Logger LOGGER = Logger.getLogger(AnnotationHelper.class);
  private static final String SUFFIX =
      "Please check if all classes being referred to, in the annotation are available in the classpath.";

  private static final List<Class<? extends IAnnotation>> ALL_ANNOTATIONS =
      Arrays.asList(
          ITestAnnotation.class,
          IBeforeClass.class,
          IAfterClass.class,
          IBeforeMethod.class,
          IAfterMethod.class,
          IDataProviderAnnotation.class,
          IFactoryAnnotation.class,
          IParametersAnnotation.class,
          IBeforeSuite.class,
          IAfterSuite.class,
          IBeforeTest.class,
          IAfterTest.class,
          IBeforeGroups.class,
          IAfterGroups.class);

  private static final List<Class<? extends IAnnotation>> CONFIGURATION_CLASSES =
      Arrays.asList(
          IBeforeSuite.class, IAfterSuite.class,
          IBeforeTest.class, IAfterTest.class,
          IBeforeGroups.class, IAfterGroups.class,
          IBeforeClass.class, IAfterClass.class,
          IBeforeMethod.class, IAfterMethod.class);

  private AnnotationHelper() {
    // Utility class.defeat instantiation.
  }

  public static ITestAnnotation findTest(IAnnotationFinder finder, Class<?> cls) {
    return finder.findAnnotation(cls, ITestAnnotation.class);
  }

  public static ITestAnnotation findTest(IAnnotationFinder finder, Method m) {
    return finder.findAnnotation(m, ITestAnnotation.class);
  }

  public static ITestAnnotation findTest(IAnnotationFinder finder, ITestNGMethod m) {
    return finder.findAnnotation(m, ITestAnnotation.class);
  }

  public static IFactoryAnnotation findFactory(IAnnotationFinder finder, Method m) {
    return finder.findAnnotation(m, IFactoryAnnotation.class);
  }

  public static IFactoryAnnotation findFactory(IAnnotationFinder finder, Constructor<?> c) {
    return finder.findAnnotation(c, IFactoryAnnotation.class);
  }

  public static IConfigurationAnnotation findConfiguration(
      IAnnotationFinder finder, ConstructorOrMethod m) {
    IConfigurationAnnotation result = null;
    boolean ignoreFailure = false;
    IConfigurationAnnotation bs =
        (IConfigurationAnnotation) finder.findAnnotation(m, IBeforeSuite.class);
    IConfigurationAnnotation as =
        (IConfigurationAnnotation) finder.findAnnotation(m, IAfterSuite.class);
    IConfigurationAnnotation bt =
        (IConfigurationAnnotation) finder.findAnnotation(m, IBeforeTest.class);
    if (bt != null) {
      ignoreFailure = bt.isIgnoreFailure();
    }
    IConfigurationAnnotation at =
        (IConfigurationAnnotation) finder.findAnnotation(m, IAfterTest.class);
    IConfigurationAnnotation bg =
        (IConfigurationAnnotation) finder.findAnnotation(m, IBeforeGroups.class);
    IConfigurationAnnotation ag =
        (IConfigurationAnnotation) finder.findAnnotation(m, IAfterGroups.class);
    IConfigurationAnnotation bc =
        (IConfigurationAnnotation) finder.findAnnotation(m, IBeforeClass.class);
    if (bc != null) {
      ignoreFailure = bc.isIgnoreFailure();
    }
    IConfigurationAnnotation ac =
        (IConfigurationAnnotation) finder.findAnnotation(m, IAfterClass.class);
    IConfigurationAnnotation bm =
        (IConfigurationAnnotation) finder.findAnnotation(m, IBeforeMethod.class);
    if (bm != null) {
      ignoreFailure = bm.isIgnoreFailure();
    }
    IConfigurationAnnotation am =
        (IConfigurationAnnotation) finder.findAnnotation(m, IAfterMethod.class);

    if (bs != null
        || as != null
        || bt != null
        || at != null
        || bg != null
        || ag != null
        || bc != null
        || ac != null
        || bm != null
        || am != null) {
      result = createConfiguration(bs, as, bt, at, bg, ag, bc, ac, bm, am);
    }

    if (result instanceof ConfigurationAnnotation) {
      ((ConfigurationAnnotation) result).setIgnoreFailure(ignoreFailure);
    }

    return result;
  }

  public static IConfigurationAnnotation findConfiguration(IAnnotationFinder finder, Method m) {
    return findConfiguration(finder, new ConstructorOrMethod(m));
  }

  private static IConfigurationAnnotation createConfiguration(
      IConfigurationAnnotation bs,
      IConfigurationAnnotation as,
      IConfigurationAnnotation bt,
      IConfigurationAnnotation at,
      IConfigurationAnnotation bg,
      IConfigurationAnnotation ag,
      IConfigurationAnnotation bc,
      IConfigurationAnnotation ac,
      IConfigurationAnnotation bm,
      IConfigurationAnnotation am) {
    ConfigurationAnnotation result = new ConfigurationAnnotation();

    if (bs != null) {
      result.setBeforeSuite(true);
      finishInitialize(result, bs);
    }
    if (as != null) {
      result.setAfterSuite(true);
      finishInitialize(result, as);
    }
    if (bt != null) {
      result.setBeforeTest(true);
      finishInitialize(result, bt);
    }
    if (at != null) {
      result.setAfterTest(true);
      finishInitialize(result, at);
    }
    if (bg != null) {
      result.setBeforeGroups(bg.getBeforeGroups());
      finishInitialize(result, bg);
    }
    if (ag != null) {
      result.setAfterGroups(ag.getAfterGroups());
      finishInitialize(result, ag);
    }
    if (bc != null) {
      result.setBeforeTestClass(true);
      finishInitialize(result, bc);
    }
    if (ac != null) {
      result.setAfterTestClass(true);
      finishInitialize(result, ac);
    }
    if (bm != null) {
      result.setBeforeTestMethod(true);
      finishInitialize(result, bm);
    }
    if (am != null) {
      result.setAfterTestMethod(true);
      finishInitialize(result, am);
    }

    return result;
  }

  private static void finishInitialize(
      ConfigurationAnnotation result, IConfigurationAnnotation bs) {
    result.setIsBeforeGroups(bs.isBeforeGroups());
    result.setIsAfterGroups(bs.isAfterGroups());
    result.setFakeConfiguration(true);
    result.setAlwaysRun(bs.getAlwaysRun());
    result.setDependsOnGroups(bs.getDependsOnGroups());
    result.setDependsOnMethods(bs.getDependsOnMethods());
    result.setDescription(bs.getDescription());
    result.setEnabled(bs.getEnabled());
    result.setGroups(bs.getGroups());
    result.setInheritGroups(bs.getInheritGroups());
    result.setTimeOut(bs.getTimeOut());
  }

  public static List<Class<? extends IAnnotation>> getAllAnnotations() {
    return ALL_ANNOTATIONS;
  }

  /* Delegation method for creating the list of <CODE>ITestMethod</CODE>s to be analysed. */
  public static ITestNGMethod[] findMethodsWithAnnotation(
      ITestObjectFactory objectFactory,
      Class<?> rootClass,
      Class<? extends IAnnotation> annotationClass,
      IAnnotationFinder annotationFinder,
      XmlTest xmlTest) {
    // Keep a map of the methods we saw so that we ignore a method in a superclass if it's
    // already been seen in a child class
    Map<String, ITestNGMethod> vResult = Maps.newHashMap();

    try {
      vResult = Maps.newHashMap();
      //    Class[] classes = rootClass.getTestClasses();
      Class<?> cls = rootClass;

      Predicate<Class<?>> condition =
          c -> {
            if (c == null) {
              return false;
            }
            return !Object.class.equals(c);
          };
      while (condition.test(cls)) {
        boolean hasClassAnnotation = isAnnotationPresent(annotationFinder, cls, annotationClass);
        Method[] methods = ReflectionHelper.getLocalMethods(cls);
        for (Method m : methods) {
          boolean hasMethodAnnotation = isAnnotationPresent(annotationFinder, m, annotationClass);
          boolean hasTestNGAnnotation =
              isAnnotationPresent(annotationFinder, m, IFactoryAnnotation.class)
                  || isAnnotationPresent(annotationFinder, m, ITestAnnotation.class)
                  || isAnnotationPresent(annotationFinder, m);
          boolean isPublic = Modifier.isPublic(m.getModifiers());
          boolean isSynthetic = m.isSynthetic();
          if ((isPublic && hasClassAnnotation && !isSynthetic && (!hasTestNGAnnotation))
              || hasMethodAnnotation) {

            // Small hack to allow users to specify @Configuration classes even though
            // a class-level @Test annotation is present.  In this case, don't count
            // that method as a @Test
            if (isAnnotationPresent(annotationFinder, m, IConfigurationAnnotation.class)
                && isAnnotationPresent(annotationFinder, cls, ITestAnnotation.class)) {
              Utils.log(
                  "",
                  3,
                  "Method "
                      + m
                      + " has a configuration annotation"
                      + " and a class-level @Test. This method will only be kept as a"
                      + " configuration method.");

              continue;
            }

            // Skip the method if it has a return type
            if (m.getReturnType() != void.class && !xmlTest.getAllowReturnValues()) {
              Utils.log(
                  "",
                  2,
                  "Method "
                      + m
                      + " has a @Test annotation"
                      + " but also a return value:"
                      + " ignoring it. Use <suite allow-return-values=\"true\"> to fix this");
              continue;
            }

            if (Arrays.stream(m.getAnnotations())
                .anyMatch(a -> a.annotationType().getName().equals("groovy.transform.Internal"))) {
              Utils.log(
                  "", 2, "Method " + m + " is being skipped since it a Groovy internal method.");
              continue;
            }

            String key = createMethodKey(m);
            vResult.computeIfAbsent(
                key, k -> new TestNGMethod(objectFactory, m, annotationFinder, xmlTest, null));
          }
        } // for
        // Now explore the superclass
        cls = cls.getSuperclass();
      } // while

    } catch (SecurityException e) {
      LOGGER.error(e.getMessage(), e);
    }
    return vResult.values().toArray(new ITestNGMethod[0]);
  }

  public static <A extends Annotation> A findAnnotationSuperClasses(
      Class<A> annotationClass, Class<?> parameterClass) {
    Class<?> c = parameterClass;
    while (c != null) {
      A result = c.getAnnotation(annotationClass);
      if (result != null) {
        return result;
      } else {
        c = c.getSuperclass();
      }
    }
    return null;
  }

  private static boolean isAnnotationPresent(IAnnotationFinder annotationFinder, Method m) {
    for (Class<? extends IAnnotation> a : AnnotationHelper.CONFIGURATION_CLASSES) {
      if (annotationFinder.findAnnotation(m, a) != null) {
        return true;
      }
    }

    return false;
  }

  private static boolean isAnnotationPresent(
      IAnnotationFinder annotationFinder, Method m, Class<? extends IAnnotation> annotationClass) {
    if (!annotationClass.equals(IConfigurationAnnotation.class)) {
      return annotationFinder.findAnnotation(m, annotationClass) != null;
    }
    boolean found = false;
    for (Class<? extends IAnnotation> clazz : CONFIGURATION_CLASSES) {
      if (annotationFinder.findAnnotation(m, clazz) != null) {
        found = true;
        break;
      }
    }
    return found;
  }

  private static boolean isAnnotationPresent(
      IAnnotationFinder annotationFinder,
      Class<?> cls,
      Class<? extends IAnnotation> annotationClass) {
    return annotationFinder.findAnnotation(cls, annotationClass) != null;
  }

  /**
   * @return A hashcode representing the name of this method and its parameters, but without its
   *     class
   */
  private static String createMethodKey(Method m) {
    StringBuilder result = new StringBuilder(m.getName());
    for (Class<?> paramClass : m.getParameterTypes()) {
      result.append(' ').append(paramClass.toString());
    }

    return result.toString();
  }

  static <A extends Annotation> A getAnnotationFromClass(Class<?> clazz, Class<A> a) {
    Objects.requireNonNull(clazz, "Cannot retrieve annotations from a null class.");
    Objects.requireNonNull(a, "Cannot work with a null annotation");
    try {
      return clazz.getAnnotation(a);
    } catch (Throwable t) {
      String msg =
          "Encountered problems when parsing the annotation on class "
              + clazz.getCanonicalName()
              + ". ";
      msg += SUFFIX;
      throw new TypeNotPresentException(msg, t);
    }
  }

  static <A extends Annotation> A getAnnotationFromMethod(Method method, Class<A> a) {
    Objects.requireNonNull(method, "Cannot retrieve annotations from a null method.");
    Objects.requireNonNull(a, "Cannot work with a null annotation");
    try {
      return method.getAnnotation(a);
    } catch (Throwable t) {
      String mname = method.getDeclaringClass().getCanonicalName() + "." + method.getName();
      String msg = "Encountered problems when parsing the annotation on method " + mname + "(). ";
      msg += SUFFIX;
      throw new TypeNotPresentException(msg, t);
    }
  }

  static <A extends Annotation> A getAnnotationFromConstructor(Constructor<?> c, Class<A> a) {
    Objects.requireNonNull(c, "Cannot retrieve annotations from a null constructor.");
    Objects.requireNonNull(a, "Cannot work with a null annotation");
    try {
      return c.getAnnotation(a);
    } catch (Throwable t) {
      String msg =
          "Encountered problems when parsing the annotation on constructor " + c.getName() + ". ";
      msg += SUFFIX;
      throw new TypeNotPresentException(msg, t);
    }
  }
}
