/*
 * $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.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;
import com.sun.javatest.Status;
import com.sun.javatest.Test;

/**
 * A handler for the set of test cases in a test.
 * Test cases are those methods with no args that return a
 * {@link com.sun.javatest.Status status}.
 * Test cases can be explicitly selected into or excluded from the
 * set.
 */
public class TestCases {
    /**
     * Exception used to report internal errors.
     */
    public static class Fault extends Exception {
        /**
         * Construct a new Fault object that signals failure
         * with a corresponding message.
         *
         * @param s the string containing a comment
         */
        public Fault(String s) {
            super(s);
        }
    }

    /**
     * Create an object to handle the test cases of the given test.
     * @param t The test containing the test cases.
     * @param log An optional stream to which to write log messages.
     *   Use null if messages are not desired.
     */
    public TestCases(Test t, PrintWriter log) {
        test = t;
        this.log = log;
        testClass = t.getClass();
    }

    /**
     * Explicitly select a set of test cases by name. Subsequent calls
     * are cumulative; if no selections are made, the default is all
     * test cases are selected. Excluded tests will be excluded from the
     * selection; the order of select and exclude calls does not matter.
     * @param testCaseNames a comma-separated list of test cases names.
     * Each name must identify a method in the test object, that takes
     * no arguments and returns a {@link com.sun.javatest.Status status}.
     * @throws TestCases.Fault if any of the test case names are invalid.
     */
    public void select(String testCaseNames) throws Fault {
        select(split(testCaseNames));
    }


    /**
     * Explicitly select a set of test cases by name. Subsequent calls
     * are cumulative; if no selections are made, the default is all
     * test cases are selected. Excluded tests will be excluded from the
     * selection; the order of select and exclude calls does not matter.
     * @param testCaseNames an array of test cases names.
     * Each name must identify a method in the test object, that takes
     * no arguments and returns a {@link com.sun.javatest.Status status}.
     * @throws TestCases.Fault if any of the test case names are invalid.
     */
    public void select(String[] testCaseNames) throws Fault  {
        for (int i = 0; i < testCaseNames.length; i++) {
            String t = testCaseNames[i];
            selectedCases.put(t, getTestCase(t));
        }
    }


    /**
     * Explicitly exclude a set of test cases by name. Subsequent calls
     * are cumulative; by default, no test cases are excluded.
     * @param testCaseNames a comma-separated list of test cases names.
     * Each name must identify a method in the test object, that takes
     * no arguments and returns a {@link com.sun.javatest.Status status}.
     * @throws TestCases.Fault if any of the test case names are invalid.
     */
    public void exclude(String testCaseNames) throws Fault  {
        exclude(split(testCaseNames));
    }


    /**
     * Explicitly exclude a set of test cases by name. Subsequent calls
     * are cumulative; by default, no test cases are excluded.
     * @param testCaseNames an array of test cases names.
     * Each name must identify a method in the test object, that takes
     * no arguments and returns a {@link com.sun.javatest.Status status}.
     * @throws TestCases.Fault if any of the test case names are invalid.
     */
    public void exclude(String[] testCaseNames) throws Fault  {
        for (int i = 0; i < testCaseNames.length; i++) {
            String t = testCaseNames[i];
            excludedCases.put(t, getTestCase(t));
        }
    }


    /**
     * Return an enumeration of the selected test cases, based on the
     * select and exclude calls that have been made, if any.
     * @return An enumeration of the test cases.
     */
    public Enumeration enumerate() {
        Vector v = new Vector();
        if (selectedCases.isEmpty()) {
            Method[] methods = testClass.getMethods();
            for (int i = 0; i < methods.length; i++) {
                Method m = methods[i];
                if (excludedCases.get(m.getName()) == null) {
                    Class[] paramTypes = m.getParameterTypes();
                    Class returnType = m.getReturnType();
                    if ((paramTypes.length == 0) && Status.class.isAssignableFrom(returnType))
                        v.addElement(m);
                }
            }
        }
        else {
            for (Enumeration e = selectedCases.elements(); e.hasMoreElements(); ) {
                Method m = (Method)(e.nextElement());
                if (excludedCases.get(m.getName()) == null)
                    v.addElement(m);
            }
        }
        return v.elements();
    }


    /**
     * Invoke each of the selected test cases, based upon the select and exclude
     * calls that have been made, if any.
     * If the test object provides a public method
     * {@link com.sun.javatest.Status}invokeTestCase({@link java.lang.reflect.Method})
     * that method will be called to invoke the test cases; otherwise, the test
     * cases will be invoked directly.
     * It is an error if no test cases are selected, (or if they have all been excluded.)
     * @return the combined result of executing all the test cases.
     */
    public Status invokeTestCases() {
        // see if test object provides  Status invokeTestCase(Method m)
        Method invoker;
        try {
            invoker = testClass.getMethod("invokeTestCase", new Class[] {Method.class});
            if (!Status.class.isAssignableFrom(invoker.getReturnType()))
                invoker = null;
        }
        catch (NoSuchMethodException e) {
            invoker = null;
        }

        MultiStatus ms = new MultiStatus(log);
        for (Enumeration e = enumerate(); e.hasMoreElements(); ) {
            Method m = (Method)(e.nextElement());
            Status s;
            try {
                if (invoker != null)
                    s = (Status)invoker.invoke(test, new Object[] {m});
                else
                    s = (Status)m.invoke(test, noArgs);
            }
            catch (IllegalAccessException ex) {
                s = Status.failed("Could not access test case: " + m.getName());
            }
            catch (InvocationTargetException ex) {
                printStackTrace(ex.getTargetException());
                s = Status.failed("Exception from test case: " +
                                       ex.getTargetException().toString());
            }
            catch (ThreadDeath t) {
                printStackTrace(t);
                throw t;
            }
            catch (Throwable t) {
                printStackTrace(t);
                s = Status.failed("Unexpected Throwable: " + t);
            }

            ms.add(m.getName(), s);
        }
        if (ms.getTestCount() == 0)
            return Status.passed("Test passed by default: no test cases executed.");
        else
            return ms.getStatus();
    }

    /**
     * Print a stack trace for an exception to the log.
     * @param t The Throwable for which to print the trace
     */
    protected void printStackTrace(Throwable t) {
        if (log != null)
            t.printStackTrace(log);
    }

    /**
     * Look up a test case in the test object.
     * @param name the name of the test case; it must identify a method
     *          Status name()
     * @returns the selected method
     * @throws Fault if the name does not identify an appropriate method.
     */
    private Method getTestCase(String name) throws Fault {
        try {
            Method m = testClass.getMethod(name, noArgTypes);
            if (!Status.class.isAssignableFrom(m.getReturnType()))
                throw new Fault("Method for test case '" + name + "' has wrong return type" );
            return m;
        }
        catch (NoSuchMethodException e) {
            throw new Fault("Could not find test case: " + name);
        }
        catch (SecurityException e) {
            throw new Fault(e.toString());
        }
    }

    private String[] split(String s) {
        Vector v = new Vector();
        int start = 0;
        for (int i = s.indexOf(','); i != -1; i = s.indexOf(',', start)) {
            v.addElement(s.substring(start, i));
            start = i + 1;
        }
        if (start != s.length())
            v.addElement(s.substring(start));
        String[] ss = new String[v.size()];
        v.copyInto(ss);
        return ss;
    }

    private Object test;
    private Class testClass;
    private Hashtable selectedCases = new Hashtable();
    private Hashtable excludedCases = new Hashtable();
    private PrintWriter log;

    private static final Object[] noArgs = { };
    private static final Class[] noArgTypes = { };
}
