package org.apache.commons.collections;


import junit.framework.Assert;
import junit.framework.AssertionFailedError;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestResult;
import junit.framework.TestSuite;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;


/**
 *  Defines a suite of simple and/or bulk tests.<P>
 *
 *  Subclasses can implement two kinds of test methods;
 *  <I>simple</I> tests and <I>bulk</I> tests.  To define
 *  a simple test, create a public method with no arguments
 *  and a void return type whose name begins with "test".
 *  (This is similar to what you'd do for a {@link TestCase}.)
 *  <P>
 *
 *  To define a bulk test, create a public method with no
 *  arguments and a return type of <Code>BulkTest</Code> whose
 *  name begins with "bulkTest".<P>
 *
 *  When the {@link #run} method is invoked, all of the
 *  simple tests are run by invoking the simple test methods,
 *  and the bulk tests are executed by invoking the bulk test
 *  methods, and then invoking {@link #run} on the resulting
 *  <Code>BulkTest</Code> instances.  That will cause any additional
 *  simple and/or bulk tests defined by those instances to be run.
 *  The tests are executed in the order their methods are declared.
 *
 *  For instance, consider the following two classes:
 *
 *  <Pre>
 *  public class TestSet extends BulkTest {
 *
 *      private Set set;
 *
 *      public TestSet(Set set) {
 *          this.set = set;
 *      }
 *
 *      public void testContains() {
 *          boolean r = set.contains(set.iterator().next()));
 *          assertTrue("Set should contain first element, r);
 *      }
 *
 *      public void testClear() {
 *          set.clear();
 *          assertTrue("Set should be empty after clear", set.isEmpty());
 *      }
 *  }
 *
 *
 *  public class TestHashMap extends BulkTest {
 *
 *      private Map makeFullMap() {
 *          HashMap result = new HashMap();
 *          result.put("1", "One");
 *          result.put("2", "Two");
 *          return result;
 *      }
 *
 *      public void testClear() {
 *          Map map = makeFullMap();
 *          map.clear();
 *          assertTrue("Map empty after clear", map.isEmpty());
 *      }
 *
 *      public BulkTest bulkTestKeySet() {
 *          return new TestSet(makeFullMap().keySet());
 *      }
 *
 *      public BulkTest bulkTestEntrySet() {
 *          return new TestSet(makeFullMap().entrySet());
 *      }
 *  }
 *  </Pre>
 *
 *  In the above examples, <Code>TestSet</Code> defines two
 *  simple tests and no bulk tests; <Code>TestHashMap</Code>
 *  defines one simple test and two bulk tests.  When
 *  <Code>TestHashMap.run</Code> is executed, <I>five</I> simple
 *  tests will be run, in this order:
 *
 *  <Ol>
 *  <Li>TestHashMap.testClear()
 *  <Li>TestHashMap.bulkTestKeySet().testContains();
 *  <Li>TestHashMap.bulkTestKeySet().testClear();
 *  <Li>TestHashMap.bulkTestEntrySet().testContains();
 *  <Li>TestHashMap.bulkTestEntrySet().testClear();
 *  </Ol>
 *
 *  A subclass can override a superclass's bulk test by
 *  returning <Code>null</Code> from the bulk test method.  If you only
 *  want to override specific simple tests within a bulk test, use the
 *  {@link #ignoredSimpleTests} method.<P>
 *
 *  Note that if you plan to use <Code>BulkTest</Code> with
 *  {@link junit.textui.TestRunner}, you must define your <Code>suite()</Code>
 *  method as follows:
 *
 *  <Pre>
 *  public static Test suite() {
 *      return new MyBulkTestImpl();
 *  }
 *  </Pre>
 *
 *  Passing the class name into a <Code>TestSuite</Code> object will not
 *  work for <Code>BulkTest</Code>.
 *
 *  @author Paul Jack
 *  @version $Id$
 */
public class BulkTest extends Assert implements Test {

    /**
     *  Used to run a simple test.
     */
    static class MethodTest implements Test {

        /** The simple test method to invoke. */
        private Method method;

        /** The BulkTest that defines the simple test method */
        private BulkTest target;

        /**
         *  Constructor.
         *
         *  @param target  the BulkTest that defines the simple method
         *  @param method  the simple test method to invoke
         */
        public MethodTest(BulkTest target, Method method) {
            this.target = target;
            this.method = method;
        }

        /**
         *  Returns the number of tests run by this class.
         *
         *  @return 1, always
         */
        public int countTestCases() {
            return 1;
        }

        /**
         *  Runs this test and updates the given result.<P>
         *  The simple test method is invoked; if it raises any
         *  exceptions or failures, the result object is updated
         *  accordingly.
         *
         *  @param result the test result to update
         */
        public void run(TestResult result) {
            result.startTest(this);
            try {
                target.setUp();
                method.invoke(target, null);
            } catch (InvocationTargetException e) {
                Throwable t = e.getTargetException();
                if (t instanceof AssertionFailedError) {
                    result.addFailure(this, (AssertionFailedError)t);
                } else {
                    result.addError(this, t);
                }
            } catch (Exception e) {
                result.addError(this, e);
            } finally {
                try {
                    target.tearDown();
                } catch (Exception e) {
                    result.addError(this, e);
                }
            }
            result.endTest(this);
        }

        /**
         *  Returns the name of this test for display.
         *
         *  @return the name of this test
         */
        public String toString() {
            return target.name + "." + method.getName() + " ";
        }
    }


    /** List of simple tests to run. */
    private List tests = new ArrayList();

    /** The display name of this <Code>BulkTest</Code>. */
    private String name;


    /**
     *  Constructs a new <Code>BulkTest</Code>.  This constructor
     *  detects simple and bulk test methods for later use.
     */
    public BulkTest() {
        extractName();
        introspect();
    }


    /**
     *  Sets this BulkTests's name to its class name minus the package.
     */
    private void extractName() {
        name = getClass().getName();
        int index = name.lastIndexOf(".");
        if (index < 0) return;
        name = name.substring(index + 1);
    }

    /**
     *  Scans for simple and bulk test methods.
     */
    private void introspect() {
        Method[] all = getClass().getMethods();
        for (int i = 0; i < all.length; i++) {
            checkSimpleTest(all[i]);
            checkBulkTest(all[i]);
        }
        // Now remove the explicitly ignored tests
        String[] names = ignoredSimpleTests();
        if (names == null) return;
        removeIgnoredSimpleTests(Arrays.asList(names));
    }


    /**
     *  Removes any simple tests whose names are in the given
     *  collection from this test and its bulk tests.
     *
     *  @param ignored the collection of names to ignore
     */
    private void removeIgnoredSimpleTests(Collection ignored) {
        Iterator iter = tests.iterator();
        while (iter.hasNext()) {
            Object object = iter.next();
            if (object instanceof MethodTest) {
                if (ignored.contains(object.toString().trim())) {
                    iter.remove();
                }
            } else {
                ((BulkTest)object).removeIgnoredSimpleTests(ignored);
            }
        }
    }


    /**
     *  If the given method is a simple test method, update the
     *  internal list of tests with that simple test.
     *
     *  @param method the method to check
     */
    private void checkSimpleTest(Method method) {
        if (!method.getName().startsWith("test")) return;
        if (method.getReturnType() != Void.TYPE) return;
        if (method.getParameterTypes().length != 0) return;
        tests.add(new MethodTest(this, method));
    }

    /**
     *  If the given method is a bulk test method, update the
     *  internal list of tests with that bulk test.
     *
     *  @param method the method to check
     */
    private void checkBulkTest(Method method) {
        if (!method.getName().startsWith("bulkTest")) return;
        if (method.getReturnType() != BulkTest.class) return;
        if (method.getParameterTypes().length != 0) return;
        try {
            BulkTest bulkTest = (BulkTest)method.invoke(this, null);
            if (bulkTest == null) return;
            bulkTest.name = name + "." + method.getName();
            tests.add(bulkTest);
        } catch (InvocationTargetException e) {
            e.getTargetException().printStackTrace(); // FIXME: What to do?
            throw new Error();
        } catch (IllegalAccessException e) {
            throw new Error();
        }
    }

    /**
     *  Runs the simple tests and the bulk tests defined
     *  by this <Code>BulkTest</Code>, collecting the results
     *  in the given result object.  The tests are run in the
     *  order in which their methods are declared.
     *
     *  @param result  the result to update
     */
    public void run(TestResult result) {
        Iterator iter = tests.iterator();
        while (iter.hasNext()) {
            Test test = (Test)iter.next();
            test.run(result);
        }
    }

    /**
     *  Recursively counts the number of simple tests defined by this
     *  this test and its bulk tests.
     *
     *  @return  the total number of simple tests defined by this test
     *    and its bulk tests
     */
    public int countTestCases() {
        int count = 0; // one for this Test
        Iterator iter = tests.iterator();
        while (iter.hasNext()) {
            Object object = iter.next();
            if (object instanceof MethodTest) count++;
            else count += ((BulkTest)object).countTestCases();
        }
        return count;
    }

    /**
     *  Returns an array of simple test names to ignore.<P>
     *
     *  If a simple test that's defined by this <Code>BulkTest</Code> or
     *  by one of its bulk test methods has a name that's in the returned
     *  array, then that simple test will not be executed.<P>
     *
     *  A simple test's name is formed by taking the class name of the
     *  root <Code>BulkTest</Code>, eliminating the package name, then
     *  appending the names of any bulk test methods that were invoked
     *  to get to the simple test, and then appending the simple test
     *  method name.  The method names are delimited by periods:
     *
     *  <Pre>
     *  TestHashMap.bulkTestEntrySet.testClear
     *  </Pre>
     *
     *  is the name of one of the simple tests defined in the sample classes
     *  described above.  If the sample <Code>TestHashMap</Code> class
     *  included this method:
     *
     *  <Pre>
     *  public String[] ignoredSimpleTests() {
     *      return new String[] { "TestHashMap.bulkTestEntrySet.testClear" };
     *  }
     *  </Pre>
     *
     *  then the entry set's clear method wouldn't be tested, but the key
     *  set's clear method would.
     *
     *  @return an array of the names of simple tests to ignore, or null if
     *   no tests should be ignored
     */
    public String[] ignoredSimpleTests() {
        return null;
    }


    /**
     *  Returns the display name of this bulk test.
     *
     *  @return the display name of this bulk test
     */
    public String toString() {
        return name;
    }


    /**
     *  Executed before every simple test.  Override to provide
     *  fixtures or other pre-test setup.
     *
     *  @throws Exception if pre-test setup fails for some reason (like
     *   a file can't be opened, for instance)
     */
    protected void setUp() throws Exception {
    }


    /**
     *  Executed after every simple test, even in the event of a failure
     *  or error.  Override to clean up after your tests.
     *
     *  @throws Exception if post-test clean up fails for some reason (like
     *    a file can't be closed)
     */
    protected void tearDown() throws Exception {
    }
}
