/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.netbeans.modules.junit;

import org.netbeans.modules.junit.api.JUnitTestUtil;
import com.sun.source.tree.BlockTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.ModifiersTree;
import com.sun.source.tree.ReturnTree;
import com.sun.source.tree.StatementTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.TypeParameterTree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.TreePath;
import com.sun.source.util.Trees;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import org.netbeans.api.java.source.ElementHandle;
import org.netbeans.api.java.source.TreeMaker;
import org.netbeans.api.java.source.WorkingCopy;
import static javax.lang.model.element.Modifier.ABSTRACT;
import static javax.lang.model.element.Modifier.PRIVATE;
import static javax.lang.model.element.Modifier.PROTECTED;
import static javax.lang.model.element.Modifier.PUBLIC;
import static javax.lang.model.element.Modifier.STATIC;

/**
 *
 * @author  Marian Petras
 * @author  vstejskal
 */
final class JUnit3TestGenerator extends AbstractTestGenerator {

    private static final String TEST = "junit.framework.Test";
    private static final String TEST_CASE = "junit.framework.TestCase";
    private static final String TEST_SUITE = "junit.framework.TestSuite";
    private static final String OVERRIDE = "java.lang.Override";

    /** whether Java annotations should be generated */
    private final boolean useAnnotations;
    
    /**
     */
    JUnit3TestGenerator(TestGeneratorSetup setup, String sourceLevel) {
        super(setup, JUnitVersion.JUNIT3);
        useAnnotations = JUnitTestUtil.areAnnotationsSupported(sourceLevel);
    }
    
    /**
     */
    JUnit3TestGenerator(TestGeneratorSetup setup,
                        List<ElementHandle<TypeElement>> srcTopClassHandles,
                        List<String>suiteMembers,
                        boolean isNewTestClass,
                        String sourceLevel) {
        super(setup, srcTopClassHandles, suiteMembers, isNewTestClass, JUnitVersion.JUNIT3);
        useAnnotations = JUnitTestUtil.areAnnotationsSupported(sourceLevel);
    }
    
    
    /**
     */
    protected ClassTree composeNewTestClass(WorkingCopy workingCopy,
                                            String name,
                                            List<? extends Tree> members) {
        final TreeMaker maker = workingCopy.getTreeMaker();
        ModifiersTree modifiers = maker.Modifiers(
                                      Collections.<Modifier>singleton(PUBLIC));
        Tree extendsClause = getClassIdentifierTree(TEST_CASE, workingCopy);
        return maker.Class(
                    modifiers,                                 //modifiers
                    name,                                      //name
                    Collections.<TypeParameterTree>emptyList(),//type params
                    extendsClause,                             //extends
                    Collections.<ExpressionTree>emptyList(),   //implements
                    members);                                  //members
    }
    
    /**
     */
    protected List<? extends Tree> generateInitMembers(WorkingCopy workingCopy) {
        if (!setup.isGenerateSetUp() && !setup.isGenerateTearDown()) {
            return Collections.<Tree>emptyList();
        }

        final TreeMaker maker = workingCopy.getTreeMaker();
        List<MethodTree> result = new ArrayList<MethodTree>(2);
        if (setup.isGenerateSetUp()) {
            result.add(generateInitMethod("setUp", maker, workingCopy));    //NOI18N
        }
        if (setup.isGenerateTearDown()) {
            result.add(generateInitMethod("tearDown", maker, workingCopy)); //NOI18N
        }
        return result;
    }

    /**
     */
    protected ClassTree generateMissingInitMembers(ClassTree tstClass,
                                                   TreePath tstClassTreePath,
                                                   WorkingCopy workingCopy) {
        if (!setup.isGenerateSetUp() && !setup.isGenerateTearDown()
                && !setup.isGenerateSuiteClasses()) {
            return tstClass;
        }

        ClassMap classMap = ClassMap.forClass(tstClass, tstClassTreePath, workingCopy.getTrees());

        if ((!setup.isGenerateSetUp() || classMap.containsSetUp())
                && (!setup.isGenerateTearDown() || classMap.containsTearDown())
                && (!setup.isGenerateSuiteClasses() || classMap.containsNoArgMethod("suite"))) {//NOI18N
            return tstClass;
        }

        List<? extends Tree> tstMembersOrig = tstClass.getMembers();
        List<Tree> tstMembers = new ArrayList<Tree>(tstMembersOrig.size() + 2);
        tstMembers.addAll(tstMembersOrig);

        generateMissingInitMembers(tstMembers, classMap, workingCopy);
        generateTestClassSuiteMethod(tstClassTreePath, tstMembers, classMap,
                                     workingCopy);

        ClassTree newClass = workingCopy.getTreeMaker().Class(
                tstClass.getModifiers(),
                tstClass.getSimpleName(),
                tstClass.getTypeParameters(),
                tstClass.getExtendsClause(),
                (List<? extends ExpressionTree>) tstClass.getImplementsClause(),
                tstMembers);
        return newClass;
    }

    /**
     */
    protected boolean generateMissingInitMembers(List<Tree> tstMembers,
                                                 ClassMap clsMap,
                                                 WorkingCopy workingCopy) {
        TreeMaker treeMaker = workingCopy.getTreeMaker();
        
        boolean modified = false;
        if (setup.isGenerateSetUp() && !clsMap.containsSetUp()) {
            addInitMethod("setUp",                                      //NOI18N
                          clsMap.getTearDownIndex(),
                          tstMembers,
                          clsMap,
                          treeMaker,
                          workingCopy);
            modified = true;
        }
        if (setup.isGenerateTearDown() && !clsMap.containsTearDown()) {
            int setUpIndex = clsMap.getSetUpIndex();
            addInitMethod("tearDown",                                   //NOI18N
                          (setUpIndex != -1) ? setUpIndex + 1 : -1,
                          tstMembers,
                          clsMap,
                          treeMaker,
                          workingCopy);
            modified = true;
        }
        return modified;
    }
    
    /**
     * Creates a new init method ({@code setUp()}, {@code tearDown()}.
     * When the method is created, it is added to the passed
     * {@code List<Tree>} of class members and the passed {@code ClassMap}
     * is updated appropriately.
     * 
     * @param  methodName  name of the init method to be added
     * @param  targetIndex  position in the list of members where the new
     *                      init method should be put; or {@code -1} if this
     *                      is the first init method to be added and
     *                      the position should be determined automatically
     * @param  clsMembers  list of class members to which the created init
     *                     method should be added
     * @param  clsMap  map of the current class members (will be updated)
     * @param  treeMaker  maker to be used for creation of the init method
     */
    private void addInitMethod(String methodName,
                               int targetIndex,
                               List<Tree> clsMembers,
                               ClassMap clsMap,
                               TreeMaker treeMaker,
                               WorkingCopy workingCopy) {
        MethodTree initMethod = generateInitMethod(methodName,
                                                   treeMaker,
                                                   workingCopy);
        
        if (targetIndex == -1) {
            targetIndex = getPlaceForFirstInitMethod(clsMap);
        }
        
        if (targetIndex != -1) {
            clsMembers.add(targetIndex, initMethod);
        } else {
            clsMembers.add(initMethod);
        }
        clsMap.addNoArgMethod(methodName, targetIndex);
    }

    /**
     * Generates a set-up or a tear-down method.
     * The generated method will have no arguments, void return type
     * and a declaration that it may throw {@code java.lang.Exception}.
     * The method will have a declared protected member access.
     * The method contains call of the corresponding super method, i.e.
     * {@code super.setUp()} or {@code super.tearDown()}.
     *
     * @param  methodName  name of the method to be created
     * @return  created method
     * @see  http://junit.sourceforge.net/javadoc/junit/framework/TestCase.html
     *       methods {@code setUp()} and {@code tearDown()}
     */
    protected MethodTree generateInitMethod(String methodName,
                                            TreeMaker maker,
                                            WorkingCopy workingCopy) {
        Set<Modifier> modifiers = Collections.<Modifier>singleton(PROTECTED);
        ModifiersTree modifiersTree
                = useAnnotations
                  ? createModifiersTree(OVERRIDE, modifiers, workingCopy)
                  : maker.Modifiers(modifiers);
        ExpressionTree superMethodCall = maker.MethodInvocation(
                Collections.<ExpressionTree>emptyList(),    // type params.
                maker.MemberSelect(
                        maker.Identifier("super"), methodName),         //NOI18N
                Collections.<ExpressionTree>emptyList());
        BlockTree methodBody = maker.Block(
                Collections.<StatementTree>singletonList(
                        maker.ExpressionStatement(superMethodCall)),
                false);
        MethodTree method = maker.Method(
                modifiersTree,          // modifiers
                methodName,             // name
                maker.PrimitiveType(TypeKind.VOID),         // return type
                Collections.<TypeParameterTree>emptyList(), // type params
                Collections.<VariableTree>emptyList(),      // parameters
                Collections.<ExpressionTree>singletonList(
                        maker.Identifier("Exception")),     // throws...//NOI18N
                methodBody,
                null);                                      // default value
        return method;
    }

    /**
     */
    protected void generateMissingPostInitMethods(TreePath tstClassTreePath,
                                                  List<Tree> tstMembers,
                                                  ClassMap clsMap,
                                                  WorkingCopy workingCopy) {
        if (setup.isGenerateSuiteClasses()) {
            generateTestClassSuiteMethod(tstClassTreePath,
                                         tstMembers,
                                         clsMap,
                                         workingCopy);
        }
    }
    
    /**
     */
    protected MethodTree composeNewTestMethod(String testMethodName,
                                              BlockTree testMethodBody,
                                              List<ExpressionTree> throwsList,
                                              WorkingCopy workingCopy) {
        TreeMaker maker = workingCopy.getTreeMaker();
        return maker.Method(
                maker.Modifiers(createModifierSet(PUBLIC)),
                testMethodName,
                maker.PrimitiveType(TypeKind.VOID),
                Collections.<TypeParameterTree>emptyList(),
                Collections.<VariableTree>emptyList(),
                throwsList,
                testMethodBody,
                null);          //default value - used by annotations
    }

    /**
     */
    protected ClassTree finishSuiteClass(ClassTree tstClass,
                                         TreePath tstClassTreePath,
                                         List<Tree> tstMembers,
                                         List<String> suiteMembers,
                                         boolean membersChanged,
                                         ClassMap classMap,
                                         WorkingCopy workingCopy) {
        MethodTree suiteMethod = generateSuiteMethod(
                                        tstClass.getSimpleName().toString(),
                                        suiteMembers,
                                        workingCopy);
        if (suiteMethod != null) {
            int suiteMethodIndex = classMap.findNoArgMethod("suite");   //NOI18N
            if (suiteMethodIndex != -1) {
                tstMembers.set(suiteMethodIndex, suiteMethod);  //replace method
            } else {
                int targetIndex;
                if (classMap.containsInitializers()) {
                    targetIndex = classMap.getLastInitializerIndex() + 1;
                } else if (classMap.containsMethods()) {
                    targetIndex = classMap.getFirstMethodIndex();
                } else if (classMap.containsNestedClasses()) {
                    targetIndex = classMap.getFirstNestedClassIndex();
                } else {
                    targetIndex = classMap.size();
                }
                if (targetIndex == classMap.size()) {
                    tstMembers.add(suiteMethod);
                } else {
                    tstMembers.add(targetIndex, suiteMethod);
                }
                classMap.addNoArgMethod("suite", targetIndex);          //NOI18N
            }
            membersChanged = true;
        }

        //PENDING - generating main(String[]) method:
        //if (generateMainMethod && !JUnitTestUtil.hasMainMethod(tstClass)) {
        //    addMainMethod(tstClass);
        //}

        if (!membersChanged) {
            return tstClass;
        }

        return workingCopy.getTreeMaker().Class(
                tstClass.getModifiers(),
                tstClass.getSimpleName(),
                tstClass.getTypeParameters(),
                tstClass.getExtendsClause(),
                (List<? extends ExpressionTree>) tstClass.getImplementsClause(),
                tstMembers);
    }

    /**
     * 
     * @return  object representing body of the suite() method,
     *          or {@code null} if an error occured while creating the body
     */
    private MethodTree generateSuiteMethod(String suiteName,
                                          List<String> members,
                                          WorkingCopy workingCopy) {
        final Types types = workingCopy.getTypes();
        final Elements elements = workingCopy.getElements();
        final TreeMaker maker = workingCopy.getTreeMaker();

        ExpressionTree testSuiteIdentifier
                = getClassIdentifierTree(TEST_SUITE, workingCopy);

        TypeElement testTypeElem = getTestTypeElem(elements);
        if (testTypeElem == null) {
            return null;
        }
        TypeMirror testType = testTypeElem.asType();

        List<StatementTree> bodyContent
                = new ArrayList<StatementTree>(members.size() + 2);

        /* TestSuite suite = new TestSuite("ClassName") */

        VariableTree suiteObjInit = maker.Variable(
                maker.Modifiers(noModifiers()),
                "suite",                                                //NOI18N
                testSuiteIdentifier,
                maker.NewClass(
                        null,                           //enclosing instance
                        Collections.<ExpressionTree>emptyList(), //type args
                        testSuiteIdentifier,            //class name
Collections.singletonList(      //params
maker.Literal(JUnitTestUtil.getSimpleName(suiteName))),
                        null));                         //class body

        bodyContent.add(suiteObjInit);

        for (String className : members) {
            TypeElement classElem = elements.getTypeElement(className);
            if ((classElem != null) && containsSuiteMethod(
                                                classElem,
                                                elements, types,
                                                testType)) {

                /* suite.addTest(ClassName.suite()) */

                MethodInvocationTree suiteMethodCall, methodCall;
                suiteMethodCall = maker.MethodInvocation(
                        Collections.<ExpressionTree>emptyList(),
                        maker.MemberSelect(maker.QualIdent(classElem),
                                           "suite"),                    //NOI18N
                        Collections.<ExpressionTree>emptyList());
                methodCall = maker.MethodInvocation(
                        Collections.<ExpressionTree>emptyList(),
                        maker.MemberSelect(maker.Identifier("suite"),   //NOI18N
                                           "addTest"),                  //NOI18N
                        Collections.singletonList(suiteMethodCall));

                bodyContent.add(maker.ExpressionStatement(methodCall));
            }
        }

        /* return suite; */

        bodyContent.add(maker.Return(maker.Identifier("suite")));       //NOI18N


        return maker.Method(
                    maker.Modifiers(createModifierSet(PUBLIC, STATIC)),
                    "suite",                                            //NOI18N
                    maker.QualIdent(testTypeElem),             //return type
                    Collections.<TypeParameterTree>emptyList(),//type params
                    Collections.<VariableTree>emptyList(),     //params
                    Collections.<ExpressionTree>emptyList(),   //throws-list
                    maker.Block(bodyContent, false),           //body
                    null);  //def. value - only for annotations
    }

    /**
     * Finds whether the given {@code TypeElement} or any of its type
     * ancestor contains an accessible static no-arg method
     * of the given name.
     * 
     * @param  typeElement  {@code TypeElement} to search
     * @param  methodName  name of the method to be found
     * @param  elements  support instance to be used for the search
     * @return  {@code true} if the given {@code TypeElement} contains,
     *          whether inherited or declared directly,
     *          a static no-argument method of the given name,
     *          {@code false} otherwise
     */
    private boolean containsSuiteMethod(TypeElement typeElement,
                                        Elements elements,
                                        Types types,
                                        TypeMirror testType) {
        List<ExecutableElement> allMethods
                = ElementFilter.methodsIn(elements.getAllMembers(typeElement));
        for (ExecutableElement method : allMethods) {
            if (method.getSimpleName().contentEquals("suite")           //NOI18N
                    && method.getParameters().isEmpty()) {
                return method.getModifiers().contains(Modifier.STATIC)
                       && types.isSameType(method.getReturnType(),
                                           testType);
            }
        }
        return false;
    }

    /**
     */
    private boolean generateTestClassSuiteMethod(TreePath tstClassTreePath,
                                                   List<Tree> tstMembers,
                                                   ClassMap clsMap,
                                                   WorkingCopy workingCopy) {
        if (!setup.isGenerateSuiteClasses()
                        || clsMap.containsNoArgMethod("suite")) {       //NOI18N
            return false;
        }

        final TreeMaker maker = workingCopy.getTreeMaker();
        final Elements elements = workingCopy.getElements();
        final Trees trees = workingCopy.getTrees();

        Element tstClassElem = trees.getElement(tstClassTreePath);
        assert tstClassElem != null;

        List<StatementTree> bodyContent = new ArrayList<StatementTree>(4);


        /* TestSuite suite = new TestSuite(MyTestClass.class); */

        VariableTree suiteVar = maker.Variable(
                maker.Modifiers(noModifiers()),
                "suite",                                                //NOI18N
                getClassIdentifierTree(TEST_SUITE, workingCopy),
                maker.NewClass(
                        null,           //enclosing instance
                        Collections.<ExpressionTree>emptyList(),
                        getClassIdentifierTree(TEST_SUITE, workingCopy),
                        Collections.singletonList(
                                maker.MemberSelect(maker.QualIdent(tstClassElem),
                                                   "class")),           //NOI18N
                        null));         //class definition

        bodyContent.add(suiteVar);

        /* suite.addTest(NestedClass.suite());        */
        /* suite.addTest(AnotherNestedClass.suite()); */
        /*    ...                                     */

        List<TypeElement> nestedClassElems
                = ElementFilter.typesIn(tstClassElem.getEnclosedElements());
        if (!nestedClassElems.isEmpty()) {
            for (TypeElement nestedClassElem : nestedClassElems) {
                if (JUnitTestUtil.isClassTest(workingCopy, nestedClassElem)) {

                    /* suite.addTest(NestedClass.suite()); */

                    /* NestedClass.suite() */
                    MethodInvocationTree arg = maker.MethodInvocation(
                            Collections.<ExpressionTree>emptyList(),
                            maker.MemberSelect(
                                    maker.QualIdent(nestedClassElem),
                                    "suite"),                           //NOI18N
                            Collections.<ExpressionTree>emptyList());

                    /* suite.addTest(...) */
                    MethodInvocationTree methodCall = maker.MethodInvocation(
                            Collections.<ExpressionTree>emptyList(),
                            maker.MemberSelect(
                                    maker.Identifier("suite"),          //NOI18N
                                    "addTest"),                         //NOI18N
                            Collections.singletonList(arg));

                    bodyContent.add(maker.ExpressionStatement(methodCall));
                }
            }
        }

        /* return suite; */

        ReturnTree returnStmt
                = maker.Return(maker.Identifier("suite"));              //NOI18N
        bodyContent.add(returnStmt);

        MethodTree suiteMethod = maker.Method(
                maker.Modifiers(createModifierSet(PUBLIC, STATIC)),
                "suite",                                                //NOI18N
                getClassIdentifierTree(TEST, workingCopy),
                Collections.<TypeParameterTree>emptyList(), //type params
                Collections.<VariableTree>emptyList(),      //parameters
                Collections.<ExpressionTree>emptyList(),    //throws ...
                maker.Block(bodyContent, false),            //body
                null);                                       //default value

        int targetIndex;
        if (clsMap.containsMethods()) {
            targetIndex = clsMap.getFirstMethodIndex();     //before methods
        } else if (clsMap.containsNestedClasses()) {
            targetIndex = clsMap.getFirstNestedClassIndex(); //before nested
        } else {
            targetIndex = clsMap.size();                  //end of the class
        }

        if (targetIndex == clsMap.size()) {
            tstMembers.add(suiteMethod);
        } else {
            tstMembers.add(targetIndex, suiteMethod);
        }
        clsMap.addNoArgMethod("suite", targetIndex);                    //NOI18N

        return true;
    }
    
    /** element representing type {@code junit.framework.Test} */
    private TypeElement testTypeElem;
        
    /**
     */
    private TypeElement getTestTypeElem(Elements elements) {
        if (testTypeElem == null) {
            testTypeElem = getElemForClassName(
                                        "junit.framework.Test",     //NOI18N
                                        elements);
        }
        return testTypeElem;
    }

}
