package org.testng.eclipse.launch;

import java.util.HashSet;
import java.util.Set;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.jdt.core.Flags;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IRegion;
import org.eclipse.jdt.core.ISourceRange;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeHierarchy;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.IAnnotationBinding;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.search.IJavaSearchConstants;
import org.eclipse.jdt.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.core.search.SearchParticipant;
import org.eclipse.jdt.core.search.SearchPattern;
import org.eclipse.jdt.core.search.SearchRequestor;
import org.eclipse.jdt.internal.junit.util.CoreTestSearchEngine;
import org.testng.eclipse.util.ResourceUtil;

public class TestFinder {

  public void findTestsInContainer(IJavaElement element, Set result, IProgressMonitor pm) throws CoreException {
    if (element == null || result == null) {
      throw new IllegalArgumentException();
    }

    if (element instanceof IType) {
      if (internalIsTest((IType) element, pm)) {
        result.add(element);
        return;
      }
    }

    if (pm == null)
      pm= new NullProgressMonitor();

    try {
      pm.beginTask(ResourceUtil.getString("TestSearchEngine.message.searching"), 4);

      IRegion region= CoreTestSearchEngine.getRegion(element);
      ITypeHierarchy hierarchy= JavaCore.newTypeHierarchy(region, null, new SubProgressMonitor(pm, 1));
      IType[] allClasses= hierarchy.getAllClasses();

      // search for all types with references to RunWith and Test and all subclasses
      Set<IType> candidates= new HashSet<>(allClasses.length);
      SearchRequestor requestor= new AnnotationSearchRequestor(hierarchy, candidates);

      IJavaSearchScope scope= SearchEngine.createJavaSearchScope(allClasses, IJavaSearchScope.SOURCES);
      int matchRule= SearchPattern.R_EXACT_MATCH | SearchPattern.R_CASE_SENSITIVE;
      SearchPattern annotationsPattern= SearchPattern.createPattern("org.testng.annotations.Test", IJavaSearchConstants.ANNOTATION_TYPE, IJavaSearchConstants.ANNOTATION_TYPE_REFERENCE, matchRule);
      SearchParticipant[] searchParticipants= new SearchParticipant[] { SearchEngine.getDefaultSearchParticipant() };
      new SearchEngine().search(annotationsPattern, searchParticipants, scope, requestor, new SubProgressMonitor(pm, 2));

      // find all classes in the region
      for (IType curr : candidates) {
        if (CoreTestSearchEngine.isAccessibleClass(curr) && !Flags.isAbstract(curr.getFlags()) && region.contains(curr)) {
          result.add(curr);
        }
      }
    } finally {
      pm.done();
    }
  }
  
  private boolean internalIsTest(IType type, IProgressMonitor monitor) throws JavaModelException {
    if (CoreTestSearchEngine.isAccessibleClass(type)) {
      ASTParser parser= ASTParser.newParser(AST.JLS4);
      /* TODO: When bug 156352 is fixed:
      parser.setProject(type.getJavaProject());
      IBinding[] bindings= parser.createBindings(new IJavaElement[] { type }, monitor);
      if (bindings.length == 1 && bindings[0] instanceof ITypeBinding) {
        ITypeBinding binding= (ITypeBinding) bindings[0];
        return isTest(binding);
      }*/

      if (type.getCompilationUnit() != null) {
        parser.setSource(type.getCompilationUnit());
      } else if (!isAvailable(type.getSourceRange())) { // class file with no source
        parser.setProject(type.getJavaProject());
        IBinding[] bindings= parser.createBindings(new IJavaElement[] { type }, monitor);
        if (bindings.length == 1 && bindings[0] instanceof ITypeBinding) {
          ITypeBinding binding= (ITypeBinding) bindings[0];
          return isTest(binding);
        }
        return false;
      } else {
        parser.setSource(type.getClassFile());
      }
      parser.setFocalPosition(0);
      parser.setResolveBindings(true);
      CompilationUnit root= (CompilationUnit) parser.createAST(monitor);
      ASTNode node= root.findDeclaringNode(type.getKey());
      if (node instanceof TypeDeclaration) {
        ITypeBinding binding= ((TypeDeclaration) node).resolveBinding();
        if (binding != null) {
          return isTest(binding);
        }
      }
    }
    return false;
  }
  
  private static boolean isAvailable(ISourceRange range) {
    return range != null && range.getOffset() != -1;
  }
  
  private boolean isTest(ITypeBinding binding) {
    if (Modifier.isAbstract(binding.getModifiers()))
      return false;

    return annotatesAtLeastOneMethod(binding, "org.testng.annotations.Test");
  }

  public boolean annotatesAtLeastOneMethod(ITypeBinding type, String qualifiedName) {
    while (type != null) {
      IMethodBinding[] declaredMethods= type.getDeclaredMethods();
      for (int i= 0; i < declaredMethods.length; i++) {
        IMethodBinding curr= declaredMethods[i];
        if (annotates(curr.getAnnotations(), qualifiedName)) {
          return true;
        }
      }
      type= type.getSuperclass();
    }
    return false;
  }

  private boolean annotates(IAnnotationBinding[] annotations, String qualifiedName) {
    for (int i= 0; i < annotations.length; i++) {
      ITypeBinding annotationType= annotations[i].getAnnotationType();
      if (annotationType != null && (annotationType.getQualifiedName().equals(qualifiedName))) {
        return true;
      }
    }
    return  false;
  }

}
