/*
 * $Id$
 *
 * Copyright 1996-2008 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */
package com.sun.javatest.lib;

import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import com.sun.javatest.lib.TestCases;
import com.sun.javatest.Status;
import com.sun.javatest.Test;

/**
 * Base class for tests with multiple sub test cases.
 * This base class implements the standard com.sun.javatest.Test
 * features so that you can provide the additional test cases without concern about
 * the boilerplate needed to execute individual test case methods.
 *
 * <P>You must add individual test case methods to your derived test class
 * to create a useful test class. Each test case method must take no
 * arguments.  If you need to pass an argument into a test method, you should
 * design a wrapper test case to calculate the argument values and then call
 * the test method with the correct arguments.  The test case methods must
 * implement this interface:
 * <blockquote>
 * <strong><code>public Status methodName( )</code></strong>
 * </blockquote>
 *
 * @see com.sun.javatest.Test
 * @see com.sun.javatest.lib.TestCases
 */
public class MultiTest implements Test
{
    /**
     * This exception is thrown when a problem occurs initializing the test.
     * It may also be used to indicate that the test is not applicable in the
     * current circumstances and should not be run.
     */
    public static class SetupException extends Exception {
        /**
         * Construct a new SetupException object that signals failure
         * with a corresponding message.
         *
         * @param s the string containing a comment
         */
        public SetupException(String s) {
            super(s);
        }

        /**
         * Creates a SetupException object which indicates that
         * this test is not applicable. The cases when it is needed
         * are rare, so please think twice whether you really need it.
         *
         * @param msg a detail string, explaining why the test is "not applicable".
         * @return an exception object that indicates the test should not be run
         *   because it is not applicable.
         */
        public static SetupException notApplicable(String msg) {
            SetupException e = new SetupException("Test not applicable: " + msg);
            e.passed = true;
            return e;
        }

        /**
         * Determines whether this SetupException signals failure or not.
         * @return true if and only if the test is not applicable and should be
         * deemed to have "passed, by default".
         *
         */
        public boolean isPassed() {
            return passed;

        }

        /**
         * Indicate whether this exception was the result of calling {@link #notApplicable}.
         * @serial
         */
        private boolean passed = false;
    }


    /**
     * Run the test cases contained in this object. The test cases are determined
     * and invoked via reflection. The set of test cases can be specified with
     * -select case1,case2,case3... and/or restricted with -exclude case1,case2,case3...
     *
     * @see #decodeAllArgs
     * @see #init
     *
     * @param args Execute arguments passed in from either the
     *             command line or the execution harness.
     * @param log Output stream for general messages from the tests.
     * @param ref Output stream for reference output from the tests.
     * @return Overall status of running all of the test cases.
     */
    public Status run(String[] args, PrintWriter log, PrintWriter ref) {
        this.log = log;
        this.ref = ref;
        testCases = new TestCases(this, log);

        Status initStatus = init(args);
        if (testNotApplicable
            || (initStatus != null && initStatus.getType( ) != Status.PASSED)) {
            return initStatus;
        }

        return testCases.invokeTestCases();
    }

    /**
     * Run the test cases contained in this object
     *
     * This method is a convenience wrapper around the primary run method
     * which takes PrintWriters: this variant takes PrintStreams and wraps
     * them into PrintWriters.
     *
     * @see #decodeAllArgs
     * @see #init
     *
     * @param argv Execute arguments passed in from either the
     *             command line or the execution harness.
     * @param log Output stream for general messages from the tests.
     * @param ref Output stream for reference output from the tests.
     * @return Overall status of running all of the test cases.
     */
    public final Status run(String[] argv, PrintStream log, PrintStream ref) {
        PrintWriter pwLog = new PrintWriter(new OutputStreamWriter(log));
        PrintWriter pwRef = new PrintWriter(new OutputStreamWriter(ref));
        try {
            return run(argv, pwLog, pwRef);
        }
        finally {
            pwLog.flush();
            pwRef.flush();
        }
    }


    /**
     *
     * Initialize the test from the given arguments. The arguments will
     * be passed to <code>decodeAllArgs</code>, and then <code>init()</code>
     * will be called.
     *
     * @param args The arguments for the test, passed to <code>decodeArgs</code>.
     * @return null if initialization is successful, or a status indicating why
     * initialization was not successful.
     *
     * @see #decodeAllArgs
     * @see #decodeArg
     * @see #init()
     *
     * @deprecated Use <code>decodeArg(String)</code> and <code>init()</code> instead.
     */
    protected Status init(String[] args) {
        try {
            decodeAllArgs(args);
            init();
            return null;
        }
        catch (SetupException e) {
            testNotApplicable = true;
            return (e.isPassed()
                    ? Status.passed(e.getMessage())
                    : Status.failed(e.getMessage()) );
        }
    }

    /**
     * A setup method called after argument decoding is complete,
     * and before the test cases are executed. By default, it does
     * nothing; it may be overridden to provide additional behavior.
     *
     * @throws MultiTest.SetupException if processing should not continue.
     * This may be due to some inconsistency in the arguments,
     * or if it is determined the test should not execute for
     * some reason.
     */
    protected void init() throws SetupException { }

    /**
     * Parses the arguments passed to the test.
     *
     * This method embodies the main loop for all of the test's arguments.
     * It calls <code>decodeArg</code> for successive arguments in the
     * argument array.
     *
     * @param args arguments passed to the test.
     *
     * @throws MultiTest.SetupException raised when an invalid parameter is passed,
     * or another error occurred.
     *
     * @see #decodeArg
     */
    protected final void decodeAllArgs(String args[]) throws SetupException {
        int i = 0;
        while (i < args.length) {
            int elementsConsumed = decodeArg(args, i);
            if (elementsConsumed == 0 ) {
                // The argument was not recognized.
                throw new SetupException("Could not recognize argument: " + args[i]);
            }
            i += elementsConsumed;
        }
    }

    /**
     * Decode the next argument in the argument array.  This will typically be
     * overridden by subtypes that wish to decode additional arguments. If an
     * overriding method does not recognize an argument, it should return
     * <code>super.decodeArg(args, index)</code> to give supertypes a change
     * to decode the argument as well.
     *
     * @param args The array containing all the arguments
     * @param index The position of the next argument to be decoded.
     * @return the number of elements in the array were "consumed" by this call.
     *
     * @throws MultiTest.SetupException is there is a problem decoding the
     *   argument.
     */
    protected int decodeArg(String[] args, int index) throws SetupException {
        try {
            if (args[index].equals("-select") && index + 1 < args.length) {
                testCases.select(args[index + 1]);
                return 2;
            }
            else if (args[index].equals("-exclude") && index + 1 < args.length) {
                testCases.exclude(args[index + 1]);
                return 2;
            }
            // support -TestCaseID for historical compatibility
            else if (args[index].equals("-TestCaseID")) {
                if (index + 1 < args.length && args[index + 1].equals("ALL")) {
                    // ignore -TestCaseID ALL, since it is the default
                    return 2;
                }
                else {
                    int i;
                    for (i = index + 1; i < args.length && !args[i].startsWith("-"); i++)
                        testCases.select(args[i]);
                    return (i - index);
                }

            }
            else if (args[index].equals("-autoFlush")) {
                ref = new PrintWriter(ref, true);
                log = new PrintWriter(ref, true);
                return 1;
            }
            else
                return 0;
        }
        catch (TestCases.Fault e) {
            throw new SetupException(e.getMessage());
        }
    }

    /**
     * Default way to invoke a specified test case.
     * @param m The method to be invoked.
     * @return The result of invoking the specified test case.
     * @throws IllegalAccessException if there was a problem accessing the specified method
     * @throws InvocationTargetException if the specified method threw an exception when
     * it was invoked.
     */
    protected Status invokeTestCase(Method m)
                throws IllegalAccessException, InvocationTargetException {
        Object[] testArgs = { };
        return (Status) (m.invoke(this, testArgs));
    }

    // the set of test cases to be executed
    private TestCases testCases;

    // may be set if SetupException is thrown during decodeArgs() or init
    private boolean testNotApplicable;

    /**
     * Output to be logged to result file.
     */
    protected PrintWriter ref;

    /**
     * Output to be logged to result file.
     */
    protected PrintWriter log;
}
