package org.testng.internal.annotations;

import org.testng.ITestNGMethod;
import org.testng.annotations.IConfigurationAnnotation;
import org.testng.annotations.IDataProviderAnnotation;
import org.testng.annotations.IExpectedExceptionsAnnotation;
import org.testng.annotations.IFactoryAnnotation;
import org.testng.annotations.IParametersAnnotation;
import org.testng.annotations.ITestAnnotation;
import org.testng.collections.Maps;
import org.testng.internal.TestNGMethod;
import org.testng.internal.Utils;
import org.testng.xml.XmlTest;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Map;

/**
 * Helper methods to find @Test and @Configuration tags.  They minimize
 * the amount of casting we need to do.
 * 
 * Created on Dec 20, 2005
 * @author cbeust
 */
public class AnnotationHelper {

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

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

  public static IConfigurationAnnotation findConfiguration(IAnnotationFinder finder, Constructor ctor) {
    IConfigurationAnnotation result = (IConfigurationAnnotation) finder.findAnnotation(ctor, IConfigurationAnnotation.class);
    if (result == null) {
      IConfigurationAnnotation bs = (IConfigurationAnnotation) finder.findAnnotation(ctor, IBeforeSuite.class);
      IConfigurationAnnotation as = (IConfigurationAnnotation) finder.findAnnotation(ctor, IAfterSuite.class);
      IConfigurationAnnotation bt = (IConfigurationAnnotation) finder.findAnnotation(ctor, IBeforeTest.class);
      IConfigurationAnnotation at = (IConfigurationAnnotation) finder.findAnnotation(ctor, IAfterTest.class);
      IConfigurationAnnotation bg = (IConfigurationAnnotation) finder.findAnnotation(ctor, IBeforeGroups.class);
      IConfigurationAnnotation ag = (IConfigurationAnnotation) finder.findAnnotation(ctor, IAfterGroups.class);
      IConfigurationAnnotation bc = (IConfigurationAnnotation) finder.findAnnotation(ctor, IBeforeClass.class);
      IConfigurationAnnotation ac = (IConfigurationAnnotation) finder.findAnnotation(ctor, IAfterClass.class);
      IConfigurationAnnotation bm = (IConfigurationAnnotation) finder.findAnnotation(ctor, IBeforeMethod.class);
      IConfigurationAnnotation am = (IConfigurationAnnotation) finder.findAnnotation(ctor, 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);
      }    
    }
    
    return result;
  }

  public static IConfigurationAnnotation findConfiguration(IAnnotationFinder finder, Method m) {
    IConfigurationAnnotation result = (IConfigurationAnnotation) finder.findAnnotation(m, IConfigurationAnnotation.class);
    if (result == null) {
      IConfigurationAnnotation bs = (IConfigurationAnnotation) finder.findAnnotation(m, IBeforeSuite.class);
      IConfigurationAnnotation as = (IConfigurationAnnotation) finder.findAnnotation(m, IAfterSuite.class);
      IConfigurationAnnotation bt = (IConfigurationAnnotation) finder.findAnnotation(m, IBeforeTest.class);
      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);
      IConfigurationAnnotation ac = (IConfigurationAnnotation) finder.findAnnotation(m, IAfterClass.class);
      IConfigurationAnnotation bm = (IConfigurationAnnotation) finder.findAnnotation(m, IBeforeMethod.class);
      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);
      }    
    }
    
    return result;
  }
  
  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;
  }
  
  @SuppressWarnings({"deprecation"})
  private static void finishInitialize(ConfigurationAnnotation result, IConfigurationAnnotation bs) {
    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.setParameters(bs.getParameters());
  }

  private static Class[] ALL_ANNOTATIONS = new Class[] { 
    ITestAnnotation.class, IConfigurationAnnotation.class, 
    IBeforeClass.class, IAfterClass.class,
    IBeforeMethod.class, IAfterMethod.class,
    IDataProviderAnnotation.class, IExpectedExceptionsAnnotation.class, 
    IFactoryAnnotation.class, IParametersAnnotation.class, 
    IBeforeSuite.class, IAfterSuite.class,
    IBeforeTest.class, IAfterTest.class,
    IBeforeGroups.class, IAfterGroups.class
  };
  
  public static Class[] CONFIGURATION_CLASSES = new Class[] {
    IConfigurationAnnotation.class,
    IBeforeSuite.class, IAfterSuite.class,   
    IBeforeTest.class, IAfterTest.class,   
    IBeforeGroups.class, IAfterGroups.class,   
    IBeforeClass.class, IAfterClass.class,  
    IBeforeMethod.class, IAfterMethod.class
  };

  public static Class[] getAllAnnotations() {
    return ALL_ANNOTATIONS;
  }

  /**
   * Delegation method for creating the list of <CODE>ITestMethod</CODE>s to be
   * analysed.
   */
  public static ITestNGMethod[] findMethodsWithAnnotation(Class rootClass, Class 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;
      
      //
      // If the annotation is on the class or superclass, it applies to all public methods
      // except methods marked with @Configuration
      //

      //
      // Otherwise walk through all the methods and keep those
      // that have the annotation
      //
//    for (Class cls : classes) {
        while (null != cls) {
          boolean hasClassAnnotation = isAnnotationPresent(annotationFinder, cls, annotationClass);
          Method[] methods = cls.getDeclaredMethods();
          for (int i = 0; i < methods.length; i++) {
            Method m = methods[i];
            boolean hasMethodAnnotation = isAnnotationPresent(annotationFinder, m, annotationClass);
            boolean hasTestNGAnnotation =
              isAnnotationPresent(annotationFinder, m, IFactoryAnnotation.class) ||
              isAnnotationPresent(annotationFinder, m, ITestAnnotation.class) ||
              isAnnotationPresent(annotationFinder, m, CONFIGURATION_CLASSES);
            boolean isPublic = Modifier.isPublic(m.getModifiers());
            if ((isPublic && hasClassAnnotation && (! 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 local @Configuration and a class-level @Test." +
                    "This method will only be kept as a @Configuration.");
                    
                continue;
              }
              
              // Skip the method if it has a return type
              if (m.getReturnType() != void.class) {
                Utils.log("", 3, "Method " + m + " has a @Test annotation"
                    + " but also a return value:  ignoring it.");
                continue;
              }
              
              String key = createMethodKey(m);
              if (null == vResult.get(key)) {
                ITestNGMethod tm = new TestNGMethod(/* m.getDeclaringClass(), */ m,
                    annotationFinder, xmlTest);
                vResult.put(key,tm);
              }
            }
          } // for
          // Now explore the superclass
          cls = cls.getSuperclass();
        } // while

    }
    catch (SecurityException e) {
      e.printStackTrace();
    }
    ITestNGMethod[] result = vResult.values().toArray(new ITestNGMethod[vResult.size()]);
      
  //    for (Method m : result) {
  //      ppp("   METHOD FOUND: " + m);
  //    }
      
      return result;
    }

  private static boolean isAnnotationPresent(IAnnotationFinder annotationFinder, 
      Method m, Class[] annotationClasses) 
  {
    for (Class a : annotationClasses) {
      if (annotationFinder.findAnnotation(m, a) != null) {
        return true;
      }
    }
    
    return false;
  }

  private static boolean isAnnotationPresent(IAnnotationFinder annotationFinder, Method m, Class annotationClass) {
    return annotationFinder.findAnnotation(m, annotationClass) != null;
  }

  private static boolean isAnnotationPresent(IAnnotationFinder annotationFinder, Class cls, Class 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) {
    StringBuffer result = new StringBuffer(m.getName());
    for (Class paramClass : m.getParameterTypes()) {
      result.append(' ').append(paramClass.toString());
    }
    
    return result.toString();
  }
  
}
