/*
 * 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
 *
 * https://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.apache.ant.antunit;

import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Vector;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Target;

/**
 * Run antunit tests suites.  This AntUnitScriptRunner is responsible for the
 * management of the ant project and the correct invocation the target (taking
 * into account properly the [case]setUp and [case]tearDown targets).
 * The user can however provide the order of the test targets and or can filter
 * the list of test targets to execute.
 * The user must also provide its ProjectFactory and an AntUnitExecutionNotifier.
 * @since 1.2
 */
public class AntUnitScriptRunner {

    /**
     * name of the magic setUp target.
     */
    private static final String SETUP = "setUp";

    /**
     * prefix that identifies test targets.
     */
    private static final String TEST = "test";

    /**
     * name of the magic tearDown target.
     */
    private static final String TEARDOWN = "tearDown";

    /**
     * name of the magic suiteSetUp target.
     */
    private static final String SUITESETUP = "suiteSetUp";

    /**
     * name of the magic suiteTearDown target.
     */
    private static final String SUITETEARDOWN = "suiteTearDown";

    /**
     * Object used to create projects in order to support test isolation.
     */
    private final ProjectFactory prjFactory;

    /**
     * Indicates if the startSuite method has been invoked.  Use to fail fast if the
     * the caller forget to call the startSuite method
     */
    private boolean isSuiteStarted;

    /**
     * Does that script have a setUp target (defined when scanning the script)
     */
    private final boolean hasSetUp;

    /**
     * Does that script have a tearDown target (defined when scanning the script)
     */
    private final boolean hasTearDown;

    /**
     * Does that script has a suiteSetUp target.
     */
    private final boolean hasSuiteSetUp;

    /**
     * Does that script has a suite tearDown target that should be executed.
     */
    private final boolean hasSuiteTearDown;

    /**
     * List of target names
     */
    private final List<String> testTargets = new LinkedList<String>();

    /**
     * The project currently used.
     */
    private Project project = null;

    /**
     * Indicates if a target has already be executed using this project.
     * Value is undefined when project is null.
     */
    private boolean projectIsDirty;


    /**
     * Create a new AntScriptRunner on the given environment.
     * @param prjFactory A factory for the ant project that will contains the antunit test to execute.
     * The factory might be invoked multiple time in order to provide test isolation.
     * @throws BuildException The project can not be parsed
     */
    public AntUnitScriptRunner(ProjectFactory prjFactory) throws BuildException {
        this.prjFactory = prjFactory;
        Project newProject = getCurrentProject();
        @SuppressWarnings("unchecked")
        Map<String, Target> targets = newProject.getTargets();
        hasSetUp = targets.containsKey(SETUP);
        hasTearDown = targets.containsKey(TEARDOWN);
        hasSuiteSetUp = targets.containsKey(SUITESETUP);
        hasSuiteTearDown = targets.containsKey(SUITETEARDOWN);

        for (String name : targets.keySet()) {
            if (name.startsWith(TEST) && !TEST.equals(name)) {
                getTestTargets().add(name);
            }
        }
    }

    /**
     * Get the project currently in use.  The caller is not allowed to invoke a target or
     * do anything that would break the isolation of the test targets.
     * @throws BuildException The project can not be parsed
     * @return the current project
     */
    public final Project getCurrentProject() throws BuildException {
    	//Method is final because it is called from the constructor
        if (project == null) {
            project = prjFactory.createProject();
            projectIsDirty = false;
        }
        return project;
    }

    /**
     * Get a project that has not yet been used in order to execute a target on it.
     */
    private Project getCleanProject() {
        if (project == null || projectIsDirty) {
            project = prjFactory.createProject();
        }
        //we already set isDirty to true in order to make sure we didn't reuse
        //this project next time getCleanProject is called.
        projectIsDirty = true;
        return project;
    }

    /**
     * @return List&lt;String&gt; List of test targets of the script file
     */
    @Deprecated
    public List<String> getTestTartgets() {
        return getTestTargets();
    }

    /**
     * Get the (names of the) test targets to execute.
     * @return {@link List} of {@link String}
     */
    public List<String> getTestTargets() {
        return testTargets;
    }

    /**
     * Provides the name of the active script.
     * @return name of the project
     */
    public String getName() {
        return getCurrentProject().getName();
    }

    /**
     * Executes the suiteSetUp target if presents and report any execution error.
     * <p>A failure is reported to the notifier and by returning false.
     * Note that if the method return false, you are not allowed to run targets.</p>
     * @return false in case of execution failure.  true in case of success.
     */
    private boolean startSuite(AntUnitExecutionNotifier notifier) {
        getCurrentProject().fireBuildStarted();
        if (hasSuiteSetUp) {
            try {
                Project newProject = getCleanProject();
                newProject.executeTarget(SUITESETUP);
            } catch (BuildException e) {
                notifier.fireStartTest(SUITESETUP);
                fireFailOrError(SUITESETUP, e, notifier);
                return false;
            }
        }
        isSuiteStarted = true; //set to true only if suiteSetUp executed properly.
        return true;
    }

    /**
     * Run the specific test target, possibly between the setUp and tearDown targets if
     * it exists.  Exception or failures are reported to the notifier.
     * @param name name of the test target to execute.
     * @param notifier will receive execution notifications.
     * @pre startSuite has been invoked successfully
     */
    private void runTarget(String name, AntUnitExecutionNotifier notifier) {
        if (!isSuiteStarted) {
            throw new AssertionError();
        }
        Project newProject = getCleanProject();
        Vector<String> v = new Vector<String>();
        if (hasSetUp) {
            v.add(SETUP);
        }
        v.add(name);
        // create and register a logcapturer on the newProject
        new LogCapturer(newProject);
        try {
            notifier.fireStartTest(name);
            newProject.executeTargets(v);
        } catch (BuildException e) {
            fireFailOrError(name, e, notifier);
        } finally {
            // fire endTest here instead of the endTarget
            // event, otherwise an error would be
            // registered after the endTest event -
            // endTarget is called before this method's catch block
            // is reached.
            notifier.fireEndTest(name);
            // clean up
            if (hasTearDown) {
                try {
                    newProject.executeTarget(TEARDOWN);
                } catch (final BuildException e) {
                    fireFailOrError(name, e, notifier);
                }
            }
        }
    }

    /**
     * Executes the suiteTearDown target if presents and report any execution error.
     * @param caught Any internal exception triggered (and caught) by the caller indicating that
     * the execution could not be invoked as expected.
     * @param notifier will receive execution notifications.
     */
    private void endSuite(Throwable caught, AntUnitExecutionNotifier notifier) {
        if (hasSuiteTearDown) {
            try {
                Project newProject = getCleanProject();
                newProject.executeTarget(SUITETEARDOWN);
            } catch (BuildException e) {
                notifier.fireStartTest(SUITETEARDOWN);
                fireFailOrError(SUITETEARDOWN, e, notifier);
            }
        }
        getCurrentProject().fireBuildFinished(caught);
        isSuiteStarted = false;
    }

    /**
     * Try to see whether the BuildException e is an AssertionFailedException
     * or is caused by an AssertionFailedException. If so, fire a failure for
     * given targetName.  Otherwise fire an error.
     */
    private void fireFailOrError(String targetName, BuildException e,
                                 AntUnitExecutionNotifier notifier) {
        boolean failed = false;
        Throwable t = e;
        while (t instanceof BuildException) {
            if (t instanceof AssertionFailedException) {
                failed = true;
                notifier.fireFail(targetName, (AssertionFailedException) t);
                break;
            }
            t = ((BuildException) t).getCause();
        }

        if (!failed) {
            notifier.fireError(targetName, e);
        }
    }


    /**
     * Executes the suite.
     * @param suiteTargets An ordered list of test targets.  It must be a sublist of getTestTargets
     * @param notifier is notified on test progress
     */
    public void runSuite(List<String> suiteTargets, AntUnitExecutionNotifier notifier) {
        Throwable caught = null;
        try {
            if (!startSuite(notifier)) {
                return;
            }
            for (String name : suiteTargets) {
                runTarget(name, notifier);
            }
        } catch (Throwable e) {
            caught = e;
        } finally {
            endSuite(caught, notifier);
        }
    }
}
