package org.testng.eclipse.ui.conversion;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.ArrayType;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ImportDeclaration;
import org.eclipse.jdt.core.dom.MarkerAnnotation;
import org.eclipse.jdt.core.dom.MemberValuePair;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
import org.eclipse.jdt.core.dom.SuperConstructorInvocation;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.TypeLiteral;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ListRewrite;
import org.testng.eclipse.TestNGPlugin;
import org.testng.eclipse.util.PreferenceStoreUtil.SuiteMethodTreatment;

/**
 * A rewriter that will convert the current JUnit file to TestNG
 * using JDK5 annotations
 *
 * @author C�dric Beust <cedric@beust.com>
 */
public class AnnotationRewriter implements IRewriteProvider
{
  private static final Set<String> IMPORTS_TO_REMOVE = new HashSet<String>() {{
    add("junit.framework.Assert");
    add("junit.framework.Test");
    add("junit.framework.TestCase");
    add("junit.framework.TestSuite");
    add("org.junit.After");
    add("org.junit.AfterClass");
    add("org.junit.Before");
    add("org.junit.BeforeClass");
    add("org.junit.Ignore");
    add("org.junit.Test");
    add("org.junit.runner.RunWith");
    add("org.junit.runners.Parameterized");
  }};
  private static final Set<String> STATIC_IMPORTS_TO_REMOVE = new HashSet<String>() {{
    add("org.junit.Assert");
  }};

  public ASTRewrite createRewriter(CompilationUnit astRoot, AST ast) {
    final ASTRewrite result = ASTRewrite.create(astRoot.getAST());
    JUnitVisitor visitor = new JUnitVisitor();
    astRoot.accept(visitor);


    //
    // Remove some JUnit imports.
    //
    List<ImportDeclaration> oldImports = visitor.getJUnitImports();
    for (int i = 0; i < oldImports.size(); i++) {
      Name importName = oldImports.get(i).getName();
      String fqn = importName.getFullyQualifiedName();
      if (IMPORTS_TO_REMOVE.contains(fqn)) {
        result.remove(oldImports.get(i), null);
      }
      for (String s : STATIC_IMPORTS_TO_REMOVE) {
        if (fqn.contains(s)) {
          result.remove(oldImports.get(i), null);
        }
      }
    }
    
    //
    // Add imports as needed
    //
    maybeAddImport(ast, result, astRoot, visitor.hasAsserts(), "org.testng.AssertJUnit");
    maybeAddImport(ast, result, astRoot, visitor.hasFail(), "org.testng.Assert");
    maybeAddImport(ast, result, astRoot, !visitor.getBeforeClasses().isEmpty(),
    "org.testng.annotations.BeforeClass");
    maybeAddImport(ast, result, astRoot, !visitor.getBeforeMethods().isEmpty(),
        "org.testng.annotations.BeforeMethod");
    maybeAddImport(ast, result, astRoot, visitor.hasTestMethods(), "org.testng.annotations.Test");
    maybeAddImport(ast, result, astRoot, !visitor.getAfterMethods().isEmpty(),
        "org.testng.annotations.AfterMethod");
    maybeAddImport(ast, result, astRoot, !visitor.getAfterClasses().isEmpty(),
    "org.testng.annotations.AfterClass");

    //
    // Add static imports
    //
    Set<String> staticImports = visitor.getStaticImports();
    for (String si : staticImports) {
      addImport(ast, result, astRoot, "org.testng.AssertJUnit." + si, true /* static import */);
    }

    //
    // Remove "extends TestCase"
    //
    SimpleType td = visitor.getTestCase();
    if (null != td) {
      result.remove(td, null);
    }

    //
    // Addd the annotations as needed
    //
    maybeAddAnnotations(ast, visitor, result, visitor.getTestMethods(), "Test", null, null);
    maybeAddAnnotations(ast, visitor, result, visitor.getDisabledTestMethods(), "Test", null,
        createDisabledAttribute(ast));
    maybeAddAnnotations(ast, visitor, result, visitor.getBeforeMethods(), "BeforeMethod",
        "@Before" /* annotation to remove */);
    maybeAddAnnotations(ast, visitor, result, visitor.getAfterMethods(), "AfterMethod",
        "@After" /* annotation to remove */);

    //
    // suite() method: remove, comment out or leave untouched, depending on the setting
    //
    SuiteMethodTreatment smt = TestNGPlugin.getPluginPreferenceStore().getSuiteMethodTreatement();

    MethodDeclaration suiteMethod = visitor.getSuite();
    if (smt != SuiteMethodTreatment.DONT_TOUCH && suiteMethod != null) {
      if (smt == SuiteMethodTreatment.REMOVE) {
        // Remove suite()
        result.remove(suiteMethod, null);
      } else {
        // Comment out suite()
        TypeDeclaration type = visitor.getType();
        ListRewrite lr = result.getListRewrite(type, TypeDeclaration.BODY_DECLARATIONS_PROPERTY);
        lr.insertBefore(result.createStringPlaceholder("/*", ASTNode.METHOD_DECLARATION),
            suiteMethod, null);
        lr.insertAfter(result.createStringPlaceholder("*/", ASTNode.METHOD_DECLARATION),
            suiteMethod, null);
      }
    }

    //
    // Remove all the nodes that need to be removed
    //
    for (ASTNode n : visitor.getNodesToRemove()) {
      result.remove(n, null);
    }

    //
    // Replace @Ignore with @Test(enabled = false)
    //
    for (Map.Entry<MethodDeclaration, Annotation> e : visitor.getIgnoredMethods().entrySet()) {
      MethodDeclaration md = e.getKey();
      Annotation ignored = e.getValue();
      // Add the @Test(enabled = false)
      NormalAnnotation test = ast.newNormalAnnotation();
      test.setTypeName(ast.newName("Test"));
      MemberValuePair mvp = ast.newMemberValuePair();
      mvp.setName(ast.newSimpleName("enabled"));
      mvp.setValue(ast.newBooleanLiteral(false));
      test.values().add(mvp);
      result.remove(ignored, null);
      ListRewrite lr = result.getListRewrite(md, MethodDeclaration.MODIFIERS2_PROPERTY);
      lr.insertFirst(test, null);
    }

    //
    // Replace "Assert" with "AssertJUnit", unless the method is already imported statically.
    //
    Set<MethodInvocation> asserts = visitor.getAsserts();
    for (MethodInvocation m : asserts) {
      if (! staticImports.contains(m.getName().toString())) {
        Expression exp = m.getExpression();
        Name name = ast.newName("AssertJUnit");
        if (exp != null) {
          result.replace(exp, name, null);
        } else {
          result.set(m, MethodInvocation.EXPRESSION_PROPERTY, name, null);
        }
      }
    }

    //
    // Replace "fail()" with "Assert.fail()"
    //
    for (MethodInvocation fail : visitor.getFails()) {
      SimpleName exp = ast.newSimpleName("Assert");
      result.set(fail, MethodInvocation.EXPRESSION_PROPERTY, exp, null);
    }

    //
    // Replace @Test(expected) with @Test(expectedExceptions)
    // and @Test(timeout) with @Test(timeOut)
    //
    for (Map.Entry<MemberValuePair, String> pair : visitor.getTestsWithExpected().entrySet()) {
      result.replace(pair.getKey().getName(), ast.newSimpleName(pair.getValue()), null);
    }

    //
    // Remove super invocation in the constructor
    //
    SuperConstructorInvocation sci = visitor.getSuperConstructorInvocation();
    if (sci != null) {
      result.remove(sci, null);
    }

    //
    // Convert @RunWith(Parameterized.class)
    //
    SingleMemberAnnotation runWith = visitor.getRunWithParameterized();
    if (runWith != null) {
      // Remove @RunWith
      result.remove(runWith, null);

      // Add imports
      addImport(ast, result, astRoot, "org.testng.ConversionUtils.wrapDataProvider",
          true /* static import */);
      addImport(ast, result, astRoot, "org.testng.annotations.Factory", false /* not static */);

      // Add the factory method
      MethodDeclaration parameterMethod = visitor.getParametersMethod();
      ListRewrite lr = result.getListRewrite(visitor.getType(),
          TypeDeclaration.BODY_DECLARATIONS_PROPERTY);
      MethodDeclaration md = ast.newMethodDeclaration();
      md.setName(ast.newSimpleName("factory" + capitalize(parameterMethod.getName().toString())));

      // Add the "Factory" annotation
      MarkerAnnotation factory = ast.newMarkerAnnotation();
      factory.setTypeName(ast.newName("Factory"));
      md.modifiers().add(factory);

      // Make the method public
      md.modifiers().addAll(ast.newModifiers(Modifier.PUBLIC | Modifier.STATIC));
      ArrayType returnType = ast.newArrayType(ast.newSimpleType(ast.newName("Object")));
      md.setReturnType2(returnType);

      // Create the method invocation "ConversionUtils.wrapDataProvider(Foo.class, data())"
      MethodInvocation mi = ast.newMethodInvocation();
      mi.setName(ast.newSimpleName("wrapDataProvider"));

      // Add parameters to wrapDataProvider()
      // 1) the current class
      TypeLiteral tl = ast.newTypeLiteral();
      tl.setType(ast.newSimpleType(ast.newSimpleName(visitor.getType().getName().toString())));
      mi.arguments().add(tl);

      // 2) the call to the @Parameters method
      MethodInvocation pmi = ast.newMethodInvocation();
      pmi.setName(ast.newSimpleName(parameterMethod.getName().getFullyQualifiedName()));
      mi.arguments().add(pmi);

      // Create the return statement
      ReturnStatement returnStatement = ast.newReturnStatement();
      returnStatement.setExpression(mi);

      Block block = ast.newBlock();
      block.statements().add(returnStatement);
      md.setBody(block);

      lr.insertFirst(md, null);
    }

    return result;
  }

  private String capitalize(String s) {
    return s.substring(0, 1).toUpperCase() + s.substring(1);
  }

  private Map<String, Boolean> createDisabledAttribute(AST ast) {
    Map<String, Boolean> result = new HashMap<>();
    result.put("enabled", false);
    return result;
  }

  private void maybeAddImport(AST ast, ASTRewrite rewriter, CompilationUnit astRoot, boolean add,
      String imp) {
    if (add) {
      addImport(ast, rewriter, astRoot, imp);
    }
  }

  private void addImport(AST ast, ASTRewrite rewriter, CompilationUnit astRoot, String imp) {
    addImport(ast, rewriter, astRoot, imp, false /* non static import */);
  }

  private void addImport(AST ast, ASTRewrite rewriter, CompilationUnit astRoot, String imp,
      boolean isStatic) {
    ListRewrite lr = rewriter.getListRewrite(astRoot, CompilationUnit.IMPORTS_PROPERTY);
    ImportDeclaration id = ast.newImportDeclaration();
    id.setStatic(isStatic);
    id.setName(ast.newName(imp));
    lr.insertFirst(id, null);
  }

  /**
   * Add the given annotation if the method is non null
   */
  private void maybeAddAnnotation(AST ast, JUnitVisitor visitor, ASTRewrite rewriter,
      MethodDeclaration method, String annotation, String annotationToRemove,
      Map<String, Boolean> attributes)
  {
    if (method != null) {
      addAnnotation(ast, visitor, rewriter, method, createAnnotation(ast, annotation, attributes),
          annotationToRemove);
    }
  }

  /**
   * @return a NormalAnnotation if the annotation to create has attributes or a
   * MarkerAnnotation otherwise.
   */
  private Annotation createAnnotation(AST ast, String name, Map<String, Boolean> attributes) {
    Annotation result = null;
    NormalAnnotation normalAnnotation = null;
    if (attributes != null && attributes.size() > 0) {
      normalAnnotation = ast.newNormalAnnotation();
      result = normalAnnotation;
    } else {
      result = ast.newMarkerAnnotation();
    }
    result.setTypeName(ast.newName(name));
    if (attributes != null) {
      for (Entry<String, Boolean> a : attributes.entrySet()) {
        MemberValuePair mvp = ast.newMemberValuePair();
        mvp.setName(ast.newSimpleName(a.getKey()));
        mvp.setValue(ast.newBooleanLiteral(a.getValue()));
        normalAnnotation.values().add(mvp);
      }
    }
    return result;
  }

  /**
   * Add the given annotation if the method is non null
   */
  private void maybeAddAnnotations(AST ast, JUnitVisitor visitor, ASTRewrite rewriter,
      Collection<MethodDeclaration> methods, String annotation, String annotationToRemove) {
    maybeAddAnnotations(ast, visitor, rewriter, methods, annotation, annotationToRemove, null);
  }

  private void maybeAddAnnotations(AST ast, JUnitVisitor visitor,
      ASTRewrite rewriter, Collection<MethodDeclaration> methods, String annotation,
      String annotationToRemove, Map<String, Boolean> attributes) {
    for (MethodDeclaration method : methods) {
      maybeAddAnnotation(ast, visitor, rewriter, method, annotation, annotationToRemove,
          attributes);
    }
  }

  private void addAnnotation(AST ast, JUnitVisitor visitor, ASTRewrite rewriter,
      MethodDeclaration md, Annotation a, String annotationToRemove)
  {
    ListRewrite lr = rewriter.getListRewrite(md, MethodDeclaration.MODIFIERS2_PROPERTY);

    // Remove the annotation if applicable
    if (annotationToRemove != null) {
      List modifiers = md.modifiers();
      for (int k = 0; k < modifiers.size(); k++) {
        Object old = modifiers.get(k);
        if (old instanceof Annotation) {
          String oldAnnotation = old.toString();
          if (oldAnnotation.equals(annotationToRemove) || "@Override".equals(oldAnnotation)) {
            lr.remove((Annotation) old, null);
            break;
          }
        }
      }
    }

    // Add the annotation
    lr.insertFirst(a, null);
  }

  public String getName() {
    return "Convert to TestNG (Annotations)";
  }  
}
