package org.testng.internal;

import bsh.EvalError;
import bsh.Interpreter;

import org.testng.IMethodSelector;
import org.testng.IMethodSelectorContext;
import org.testng.ITestNGMethod;
import org.testng.TestNGException;
import org.testng.collections.Lists;
import org.testng.collections.Maps;
import org.testng.xml.XmlClass;
import org.testng.xml.XmlInclude;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

/**
 * This class is the default method selector used by TestNG to determine
 * which methods need to be included and excluded based on the specification
 * given in testng.xml.
 * 
 * Created on Sep 30, 2005
 * @author cbeust
 */
public class XmlMethodSelector implements IMethodSelector {
  private static Interpreter s_interpreter;
  
  // Groups included and excluded for this run
  private Map<String, String> m_includedGroups = Maps.newHashMap();
  private Map<String, String> m_excludedGroups = Maps.newHashMap();
  private List<XmlClass> m_classes = null;
  // The BeanShell expression for this test, if any
  private String m_expression = null;
  // List of methods included implicitly
  private Set<String> m_includedMethods = new HashSet<String>();
  
  public boolean  includeMethod(IMethodSelectorContext context,
      ITestNGMethod tm, boolean isTestMethod) 
  {
//    ppp("XML METHOD SELECTOR " + tm + " " + m_isInitialized);
    
    if (! m_isInitialized) {
      m_isInitialized = true;
      init(context);
    }
    
    boolean result = false;
    if (null != m_expression) {
      result = includeMethodFromExpression(tm, isTestMethod);
    }
    else {
      result = includeMethodFromIncludeExclude(tm, isTestMethod);
    }
    
    return result;
  }
  
  private static Interpreter getInterpreter() {
    if(null == s_interpreter) {
      s_interpreter= new Interpreter();
    }
    
    return s_interpreter;
  }
  
  private boolean includeMethodFromExpression(ITestNGMethod tm,  boolean isTestMethod) {
    boolean result = false;

    Interpreter interpreter= getInterpreter();
    try {
      Map<String, String> groups = Maps.newHashMap();
      for (String group : tm.getGroups()) {
        groups.put(group, group);
      }
      setContext(interpreter, tm.getMethod(), groups, tm);
      Object evalResult = interpreter.eval(m_expression);
      result = ((Boolean) evalResult).booleanValue();
    }
    catch (EvalError evalError) {
      Utils.log("bsh.Interpreter", 2, "Cannot evaluate expression:" + m_expression + ":" + evalError.getMessage());
    }
    finally {
      resetContext(interpreter);
    }

    return result;
    
  }
  
  private void resetContext(Interpreter interpreter) {
    try {
      interpreter.unset("method");
      interpreter.unset("groups");
      interpreter.unset("testngMethod");
    }
    catch(EvalError evalError) {
      Utils.log("bsh.Interpreter", 2, "Cannot reset interpreter:" + evalError.getMessage());
    }
  }
  
  private void setContext(Interpreter interpreter, Method method, Map<String, String> groups, ITestNGMethod tm) {
    try {
      interpreter.set("method", method);
      interpreter.set("groups", groups);
      interpreter.set("testngMethod", tm);
    }
    catch(EvalError evalError) {
      throw new TestNGException("Cannot set BSH interpreter", evalError);
    }
  }
  
  private boolean includeMethodFromIncludeExclude(ITestNGMethod tm, boolean isTestMethod) {
    boolean result = false;
    Method m = tm.getMethod();
    String[] groups = tm.getGroups();
    Map<String, String> includedGroups = m_includedGroups;
    Map<String, String> excludedGroups = m_excludedGroups;
    
    //
    // No groups were specified:
    //
    if (includedGroups.size() == 0 && excludedGroups.size() == 0 
        && ! hasIncludedMethods() && ! hasExcludedMethods())
    //
    // If we don't include or exclude any methods, method is in
    //
    {
      result = true;
    } 
    //
    // If it's a configuration method and no groups were requested, we want it in
    //
    else if (includedGroups.size() == 0 && excludedGroups.size() == 0 && ! isTestMethod) 
    {
      result = true;
    }
    
    //
    // Is this method included implicitly?
    //
    else if (m_includedMethods.contains(MethodHelper.calculateMethodCanonicalName(tm))) {
      result = true;
    }
    
    //
    // Include or Exclude groups were specified:
    //
    else {
      //
      // Only add this method if it belongs to an included group and not
      // to an excluded group
      //
      {
        boolean isIncludedInGroups = isIncluded(groups, m_includedGroups.values());
        boolean isExcludedInGroups = isExcluded(groups, m_excludedGroups.values());
  
        //
        // Calculate the run methods by groups first
        //
        if (isIncludedInGroups && !isExcludedInGroups) {
          result = true;
        }
        else if (isExcludedInGroups) {
          result = false;
        }
      }
      
      if(isTestMethod) {
        //
        // Now filter by method name
        //
        Method method = tm.getMethod();
        Class methodClass= method.getDeclaringClass();
        String fullMethodName =  methodClass.getName() 
                + "."
                + method.getName();

        String[] fullyQualifiedMethodName = new String[] { fullMethodName };
        
        //
        // Iterate through all the classes so we can gather all the included and
        // excluded methods
        //
        for (XmlClass xmlClass : m_classes) {
          // Only consider included/excluded methods that belong to the same class
          // we are looking at
          Class cls= ClassHelper.forName(xmlClass.getName());
          if(null == cls) {
            Utils.log("XmlMethodSelector", 1, "Cannot find class in classpath " + xmlClass.getName());
            continue;
          }
          if(!assignable(methodClass, cls)) {
            continue;
          }
          
          List<String> includedMethods =
              createQualifiedMethodNames(xmlClass, toStringList(xmlClass.getIncludedMethods()));
          boolean isIncludedInMethods = isIncluded(fullyQualifiedMethodName, includedMethods);
          List<String> excludedMethods = createQualifiedMethodNames(xmlClass,
              xmlClass.getExcludedMethods());
          boolean isExcludedInMethods = isExcluded(fullyQualifiedMethodName, excludedMethods);
          if (result) {
            // If we're about to include this method by group, make sure
            // it's included by method and not excluded by method
            result = isIncludedInMethods && ! isExcludedInMethods;
          }
          // otherwise it's already excluded and nothing will bring it back,
          // since exclusions preempt inclusions
        }
      }      
    }    
    
    Package pkg = m.getDeclaringClass().getPackage();
    String methodName = pkg != null ? pkg.getName() + "." + m.getName() : m.getName();
    
    logInclusion(result ? "Including" : "Excluding", "method", methodName + "()");

    return result;
  }
  
  @SuppressWarnings({"unchecked"})
  private boolean assignable(Class sourceClass, Class targetClass) {
    return sourceClass.isAssignableFrom(targetClass) || targetClass.isAssignableFrom(sourceClass);
  }
  
  private Map<String, String> m_logged = Maps.newHashMap();
  private void logInclusion(String including, String type, String name) {
    if (! m_logged.containsKey(name)) {
      log(4, including + " " + type + " " + name);
      m_logged.put(name, name);
    }
  }
  
  private boolean hasIncludedMethods() {
    for (XmlClass xmlClass : m_classes) {
      if (xmlClass.getIncludedMethods().size() > 0) {
        return true;
      }
    }
    
    return false;
  }
  
  private boolean hasExcludedMethods() {
    for (XmlClass xmlClass : m_classes) {
      if (xmlClass.getExcludedMethods().size() > 0) {
        return true;
      }
    }
    
    return false;
  }

  private List<String> toStringList(List<XmlInclude> methods) {
    List<String> result = Lists.newArrayList();
    for (XmlInclude m : methods) {
      result.add(m.getName());
    }
    return result;
  }

  private List<String> createQualifiedMethodNames(XmlClass xmlClass,
      List<String> methods) {
    List<String> vResult = Lists.newArrayList();
    Class cls = xmlClass.getSupportClass();

    while (null != cls) {
      for (String im : methods) {
        String methodName = im;
        Method[] allMethods = cls.getDeclaredMethods();
        Pattern pattern = Pattern.compile(methodName);
        for (Method m : allMethods) {
          if (pattern.matcher(m.getName()).matches()) {
            vResult.add(makeMethodName(cls.getName(), m.getName()));
          }
        }
      }
      cls = cls.getSuperclass();
    }
    
    return vResult;
  }
  
  private String makeMethodName(String className, String methodName) {
    return className + "." + methodName;
  }
  
  public void setXmlClasses(List<XmlClass> classes) {
    m_classes = classes;
    for (XmlClass c : classes) {
      for (XmlInclude m : c.getIncludedMethods()) {
        m_includedMethods.add(makeMethodName(c.getName(), m.getName()));
      }
    }
  }
  
  /**
   * @return Returns the excludedGroups.
   */
  public Map<String, String> getExcludedGroups() {
    return m_excludedGroups;
  }

  /**
   * @return Returns the includedGroups.
   */
  public Map<String, String> getIncludedGroups() {
    return m_includedGroups;
  }

  /**
   * @param excludedGroups The excludedGroups to set.
   */
  public void setExcludedGroups(Map<String, String> excludedGroups) {
    m_excludedGroups = excludedGroups;
  }
  
  /**
   * @param includedGroups The includedGroups to set.
   */
  public void setIncludedGroups(Map<String, String> includedGroups) {
    m_includedGroups = includedGroups;
  }
  
  private static boolean isIncluded(String[] groups, Collection<String> includedGroups) {
    if (includedGroups.size() == 0) {
      return true;
    }
    else {
      return isMemberOf(groups, includedGroups);
    }
  }
  
  private static boolean isExcluded(String[] groups, Collection<String> excludedGroups) {
    return isMemberOf(groups, excludedGroups);
  }
  
  /**
   * 
   * @param groups Array of groups on the method
   * @param list Map of regexps of groups to be run
   */
  private static boolean isMemberOf(String[] groups, Collection<String> list) {
    for (String group : groups) {
      for (Object o : list) {
        String regexpStr = o.toString();
        boolean match = Pattern.matches(regexpStr, group);
        if (match) {
          return true;
        }
      }
    }
    
    return false;
  }
    
  private static void log(int level, String s) {
    Utils.log("XmlMethodSelector", level, s);
  }
  
  private static void ppp(String s) {
    System.out.println("[XmlMethodSelector] " + s);
  }

  public void setExpression(String expression) {
    m_expression = expression;
  }
  
  private boolean m_isInitialized = false;
  private List<ITestNGMethod> m_testMethods = null;

  public void setTestMethods(List<ITestNGMethod> testMethods) {
    // Caution: this variable is initialized with an empty list first and then modified
    // externally by the caller (TestRunner#fixMethodWithClass). Ugly.
    m_testMethods = testMethods;
  }
  
  private void init(IMethodSelectorContext context) {
    String[] groups = m_includedGroups.keySet().toArray(new String[m_includedGroups.size()]);
    Set<String> groupClosure = new HashSet<String>();
    Set<ITestNGMethod> methodClosure = new HashSet<ITestNGMethod>();
    
    List<ITestNGMethod> includedMethods = Lists.newArrayList();
    for (ITestNGMethod m : m_testMethods) {
      if (includeMethod(context, m, true)) includedMethods.add(m);
    }
    MethodHelper.findGroupTransitiveClosure(this, includedMethods, m_testMethods, 
        groups, groupClosure, methodClosure);
    
    // If we are asked to include or exclude specific groups, calculate
    // the transitive closure of all the included groups.  If no include groups
    // were specified, don't do anything.
    // Any group that is part of the transitive closure but not part of
    // m_includedGroups is being added implicitly by TestNG so that if someone
    // includes a group z that depends on a, b and c, they don't need to 
    // include a, b and c explicitly.
    if (m_includedGroups.size() > 0) {
      // Make the transitive closure our new included groups
      for (String g : groupClosure) {
        log(4, "Including group "
            + (m_includedGroups.containsKey(g) ?
                ": " : "(implicitly): ") + g);
        m_includedGroups.put(g, g);
      }
    
      // Make the transitive closure our new included methods
      for (ITestNGMethod m : methodClosure) {
        String methodName = 
         m.getMethod().getDeclaringClass().getName() + "." + m.getMethodName();
        m_includedMethods.add(methodName);
        logInclusion("Including", "method ", methodName);
      }
    }
  }

  private boolean m_verbose = true;
  public void setVerbose(boolean b) {
    m_verbose = b;
  }
  
}
