/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed 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 com.google.coretests;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Enumeration;
import java.util.Vector;

import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestFailure;
import junit.framework.TestResult;
import junit.framework.TestSuite;
import dalvik.annotation.AndroidOnly;
import dalvik.annotation.BrokenTest;
import dalvik.annotation.KnownFailure;
import dalvik.annotation.SideEffect;

/**
 * A special TestSuite implementation that flattens the hierarchy of a given
 * JUnit TestSuite and removes tests after executing them. This is so the core
 * tests actually have a chance to succeed, since they do consume quite some
 * memory and many tests do not (have a chance to) cleanup properly after
 * themselves. The class also implements our filtering mechanism for tests, so
 * it becomes easy to only include or exclude tests based on their annotations
 * (like, say, execute all Android-only tests that are not known to be broken).
 */
public class CoreTestSuite implements Test {

    /**
     * Include all normal tests in the suite.
     */
    public static final int RUN_NORMAL_TESTS = 1;

    /**
     * Include all broken tests in the suite.
     */
    public static final int RUN_BROKEN_TESTS = 2;

    /**
     * Include all known failures in the suite.
     */
    public static final int RUN_KNOWN_FAILURES = 4;

    /**
     * Include all Android-only tests in the suite.
     */
    public static final int RUN_ANDROID_ONLY = 8;

    /**
     * Include side-effective tests in the suite.
     */
    public static final int RUN_SIDE_EFFECTS = 16;

    /**
     * Include all tests in the suite.
     */
    public static final int RUN_ALL_TESTS =
            RUN_NORMAL_TESTS | RUN_BROKEN_TESTS |
            RUN_KNOWN_FAILURES | RUN_SIDE_EFFECTS | RUN_ANDROID_ONLY;

    /**
     * Special treatment for known failures: they are expected to fail, so we
     * throw an Exception if they succeed and accept them failing.
     */
    public static final int INVERT_KNOWN_FAILURES = 32;

    /**
     * Run each test in its own VM.
     */
    public static final int ISOLATE_ALL = 64;

    /**
     * Run no test in its own VM.
     */
    public static final int ISOLATE_NONE = 128;

    /**
     * Be verbose.
     */
    public static final int VERBOSE = 256;

    public static final int REVERSE = 512;

    public static final int DRY_RUN = 1024;

    private final String name;

    /**
     * The total number of tests in the original suite.
     */
    protected int fTotalCount;

    /**
     * The number of Android-only tests in the original suite.
     */
    protected int fAndroidOnlyCount;

    /**
     * The number of broken tests in the original suite.
     */
    protected int fBrokenCount;

    /**
     * The number of known failures in the original suite.
     */
    protected int fKnownFailureCount;

    /**
     * The number of side-effective tests in the original suite.
     */
    protected int fSideEffectCount;

    /**
     * The number of normal (non-annotated) tests in the original suite.
     */
    protected int fNormalCount;

    /**
     * The number of ignored tests, that is, the number of tests that were
     * excluded from this suite due to their annotations.
     */
    protected int fIgnoredCount;

    /**
     * Contains the actual test cases in a reverse-ordered, flat list.
     */
    private Vector<Test> fTests = new Vector<Test>();

    private TestCase fVictim;

    private int fStep;

    private int fFlags;

    /**
     * Creates a new CoreTestSuite for the given ordinary JUnit Test (which may
     * be a TestCase or TestSuite). The CoreTestSuite will be a flattened and
     * potentially filtered subset of the original JUnit Test. The flags
     * determine the way we filter.
     */
    public CoreTestSuite(Test suite, int flags, int step, TestCase victim) {
        super();

        name = suite.toString();
        fStep = step;
        addAndFlatten(suite, flags);
        fVictim = victim;
        fFlags = flags;
    }

    /**
     * Adds the given ordinary JUnit Test (which may be a TestCase or TestSuite)
     * to this CoreTestSuite. Note we are storing the tests in reverse order,
     * so it's easier to remove a finished test from the end of the list.
     */
    private void addAndFlatten(Test test, int flags) {
        if (test instanceof TestSuite) {
            TestSuite suite = (TestSuite)test;

            if ((flags & REVERSE) != 0) {
                for (int i = suite.testCount() - 1; i >= 0; i--) {
                    addAndFlatten(suite.testAt(i), flags);
                }
            } else {
                for (int i = 0; i < suite.testCount(); i++) {
                    addAndFlatten(suite.testAt(i), flags);
                }
            }
        } else if (test instanceof TestCase) {
            TestCase testCase = (TestCase)test;
            boolean ignoreMe = false;

            boolean isAndroidOnly = hasAnnotation(testCase, AndroidOnly.class);
            boolean isBrokenTest = hasAnnotation(testCase, BrokenTest.class);
            boolean isKnownFailure = hasAnnotation(testCase, KnownFailure.class);
            boolean isSideEffect = hasAnnotation(testCase, SideEffect.class);
            boolean isNormalTest =
                    !(isAndroidOnly || isBrokenTest || isKnownFailure ||
                      isSideEffect);

            if (isAndroidOnly) {
                fAndroidOnlyCount++;
            }

            if (isBrokenTest) {
                fBrokenCount++;
            }

            if (isKnownFailure) {
                fKnownFailureCount++;
            }

            if (isNormalTest) {
                fNormalCount++;
            }

            if (isSideEffect) {
                fSideEffectCount++;
            }

            if ((flags & RUN_ANDROID_ONLY) == 0 && isAndroidOnly) {
                ignoreMe = true;
            }

            if ((flags & RUN_BROKEN_TESTS) == 0 && isBrokenTest) {
                ignoreMe = true;
            }

            if ((flags & RUN_KNOWN_FAILURES) == 0 && isKnownFailure) {
                ignoreMe = true;
            }

            if (((flags & RUN_NORMAL_TESTS) == 0) && isNormalTest) {
                ignoreMe = true;
            }

            if (((flags & RUN_SIDE_EFFECTS) == 0) && isSideEffect) {
                ignoreMe = true;
            }

            this.fTotalCount++;

            if (!ignoreMe) {
                fTests.add(test);
            } else {
                this.fIgnoredCount++;
            }
        } else {
            System.out.println("Warning: Don't know how to handle " +
                    test.getClass().getName() + " " + test.toString());
        }
    }

    /**
     * Checks whether the given TestCase class has the given annotation.
     */
    @SuppressWarnings("unchecked")
    private boolean hasAnnotation(TestCase test, Class clazz) {
        try {
            Method method = test.getClass().getMethod(test.getName());
            return method.getAnnotation(clazz) != null;
        } catch (Exception e) {
            // Ignore
        }

        return false;
    }

    /**
     * Runs the tests and collects their result in a TestResult.
     */
    public void run(TestResult result) {
        // Run tests
        int i = 0;

        while (fTests.size() != 0 && !result.shouldStop()) {
            TestCase test = (TestCase)fTests.elementAt(i);

            Thread.currentThread().setContextClassLoader(
                    test.getClass().getClassLoader());

            test.run(result);

            /*
            if (fVictim != null) {
                TestResult dummy = fVictim.run();

                if (dummy.failureCount() != 0) {
                    result.addError(fTests.elementAt(i), new RuntimeException(
                            "Probable side effect",
                            ((TestFailure)dummy.failures().nextElement()).
                            thrownException()));
                } else if (dummy.errorCount() != 0) {
                    result.addError(fTests.elementAt(i), new RuntimeException(
                            "Probable side effect",
                            ((TestFailure)dummy.errors().nextElement()).
                            thrownException()));
                }
            }
            */

            fTests.remove(i);

            if (fTests.size() != 0) {
                i = (i + fStep - 1) % fTests.size();
            }

        }

        // Forward overall stats to TestResult, so ResultPrinter can see it.
        if (result instanceof CoreTestResult) {
            ((CoreTestResult)result).updateStats(
                    fTotalCount, fAndroidOnlyCount, fBrokenCount,
                    fKnownFailureCount, fNormalCount, fIgnoredCount,
                    fSideEffectCount);
        }
    }

    /**
     * Nulls all reference fields in the given test object. This method helps
     * us with those test classes that don't have an explicit tearDown()
     * method. Normally the garbage collector should take care of everything,
     * but it can't hurt to support it a bit.
     */
    private void cleanup(TestCase test) {
        Field[] fields = test.getClass().getDeclaredFields();
        for (int i = 0; i < fields.length; i++) {
            Field f = fields[i];
            if (!f.getType().isPrimitive() &&
                    (f.getModifiers() & Modifier.STATIC) == 0) {
                try {
                    f.setAccessible(true);
                    f.set(test, null);
                } catch (Exception ex) {
                    // Nothing we can do about it.
                }
            }
        }
    }

    /**
     * Returns the tests as an enumeration. Note this is empty once the tests
     * have been executed.
     */
    @SuppressWarnings("unchecked")
    public Enumeration tests() {
        return fTests.elements();
    }

    /**
     * Returns the number of tests in the suite. Note this is zero once the
     * tests have been executed.
     */
    public int countTestCases() {
        return fTests.size();
    }

    @Override public String toString() {
        return name;
    }
}
