package org.testng.internal;

import org.testng.ITestNGMethod;
import org.testng.collections.Lists;
import org.testng.collections.Maps;

import java.util.Collections;
import java.util.List;
import java.util.Map;

public class MethodInheritance {
  /**
   * Look in map for a class that is a superclass of methodClass
   * @param map
   * @param methodClass
   * @return
   */
  private static List<ITestNGMethod> findMethodListSuperClass(Map<Class, List<ITestNGMethod>> map,
      Class< ? extends ITestNGMethod> methodClass)
  {
    for (Map.Entry<Class, List<ITestNGMethod>> entry : map.entrySet()) {
      if (entry.getKey().isAssignableFrom(methodClass)) {
        return entry.getValue();
      }
    }
    return null;
  }

  /**
   * Look in map for a class that is a subclass of methodClass
   * @param map
   * @param methodClass
   * @return
   */
  private static Class findSubClass(Map<Class, List<ITestNGMethod>> map,
      Class< ? extends ITestNGMethod> methodClass)
  {
    for (Class cls : map.keySet()) {
      if (methodClass.isAssignableFrom(cls)) {
        return cls;
      }
    }

    return null;
  }

  /**
   * Fix the methodsDependedUpon to make sure that @Configuration methods
   * respect inheritance (before methods are invoked in the order Base first
   * and after methods are invoked in the order Child first)
   *
   * @param methods the list of methods
   * @param before true if we are handling a before method (meaning, the methods
   * need to be sorted base class first and subclass last). false otherwise (subclass
   * methods first, base classes last).
   */
  public static void fixMethodInheritance(ITestNGMethod[] methods, boolean before) {
    // Map of classes -> List of methods that belong to this class or same hierarchy
    Map<Class, List<ITestNGMethod>> map = Maps.newHashMap();

    //
    // Put the list of methods in their hierarchy buckets
    //
    for (ITestNGMethod method : methods) {
      Class< ? extends ITestNGMethod> methodClass = method.getRealClass();
      List<ITestNGMethod> l = findMethodListSuperClass(map, methodClass);
      if (null != l) {
        l.add(method);
      }
      else {
        Class subClass = findSubClass(map, methodClass);
        if (null != subClass) {
          l = map.get(subClass);
          l.add(method);
          map.remove(subClass);
          map.put(methodClass, l);
        }
        else {
          l = Lists.newArrayList();
          l.add(method);
          map.put(methodClass, l);
        }
      }
    }

    //
    // Each bucket that has a list bigger than one element gets sorted
    //
    for (List<ITestNGMethod> l : map.values()) {
      if (l.size() > 1) {
        // Sort them
        sortMethodsByInheritance(l, before);

        /*
         *  Set methodDependedUpon accordingly
         *  E.g. Base class can have multiple @BeforeClass methods. Need to ensure
         *  that @BeforeClass methods in derived class depend on all @BeforeClass methods
         *  of base class. Vice versa for @AfterXXX methods
         */
        for (int i = 0; i < l.size() - 1; i++) {
          ITestNGMethod m1 = l.get(i);
          for (int j = i + 1; j < l.size(); j++) {
            ITestNGMethod m2 = l.get(j);
            if (!equalsEffectiveClass(m1, m2) && !dependencyExists(m1, m2, methods)) {
              Utils.log("MethodInheritance", 4, m2 + " DEPENDS ON " + m1);
              m2.addMethodDependedUpon(MethodHelper.calculateMethodCanonicalName(m1));
            }
          }
        }
      }
    }
  }

  private static boolean dependencyExists(ITestNGMethod m1, ITestNGMethod m2, ITestNGMethod[] methods) {
    return true == internalDependencyExists(m1, m2, methods)
       ? true : internalDependencyExists(m2, m1, methods);
  }

  private static boolean internalDependencyExists(ITestNGMethod m1, ITestNGMethod m2, ITestNGMethod[] methods) {
    ITestNGMethod[] methodsNamed =
      MethodHelper.findDependedUponMethods(m1, methods);

    for (ITestNGMethod method : methodsNamed) {
      if (method.equals(m2)) {
        return true;
      }
    }

    for (String group : m1.getGroupsDependedUpon()) {
      ITestNGMethod[] methodsThatBelongToGroup =
        MethodGroupsHelper.findMethodsThatBelongToGroup(m1, methods, group);
      for (ITestNGMethod method : methodsThatBelongToGroup) {
         if (method.equals(m2)) {
           return true;
         }
       }
    }

    return false;
  }

  private static boolean equalsEffectiveClass(ITestNGMethod m1, ITestNGMethod m2) {
    try {
      Class c1 = m1.getRealClass();
      Class c2 = m2.getRealClass();

      return c1 == null ? c2 == null : c1.equals(c2);
    }
    catch(Exception ex) {
      return false;
    }
  }


  /**
   * Given a list of methods belonging to the same class hierarchy, orders them
   * from the base class to the child (if true) or from child to base class (if false)
   * @param methods
   */
  private static void sortMethodsByInheritance(List<ITestNGMethod> methods,
      boolean baseClassToChild)
  {
    Collections.sort(methods);
    if (! baseClassToChild) {
      Collections.reverse(methods);
    }
  }
}
