package org.jbpm.dependencies.runner;

import java.io.File;
import java.io.IOException;
import java.lang.Thread.UncaughtExceptionHandler;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import junit.framework.TestCase;

import org.jbpm.task.service.test.TaskServiceTest;
import org.jbpm.task.service.test.sync.TaskServiceSyncTest;
import org.junit.Test;
import org.junit.runner.*;
import org.junit.runner.notification.Failure;

/**
 * This class is used to run the jar generated by a packaging test.
 * 
 * The main method looks for runnable test methods and runs them, using JUnitCore. 
 * 
 */
public class JarTestRunner {

    public static void main(String[] args) {
        try {
            HashSet<Class> testClassSet = null;
            try {
                testClassSet = getRunnableTestClasses("org.jbpm.task.service.test");
                testClassSet.addAll(getRunnableTestClasses("org.jbpm.task.service.local.sync"));
                testClassSet.addAll(getRunnableTestClasses("org.jbpm.task.service.persistence"));
            } catch (Exception e) {
                throw new RuntimeException("Unable to determine runnable test classes.", e);
            }

            Class [] testClasses = testClassSet.toArray(new Class[testClassSet.size()]);
            Arrays.sort(testClasses, new ClassComparatorByName());

            StringBuilder separator = new StringBuilder();
            for (int i = 0; i < 8; ++i) {
                separator.append("----------");
            }
            assert testClasses[0] != null;

            System.out.println(">> STARTING TESTS [" + testClasses.length + "]");
            System.out.println(separator);
            JUnitCore jUnit = new JUnitCore();
            boolean testFailed = false;
            StringBuilder failedTestNames = new StringBuilder();

            for( Class testClass : testClasses ) {
                System.out.println("Running " + testClass.getSimpleName() );

                Result result = jUnit.run(Request.classes(testClass));

                if (! result.wasSuccessful()) {

                    List<Failure> failures = result.getFailures();
                    System.out.println();
                    for( Failure fail : failures ) {
                        failedTestNames.append(fail.getTestHeader() + "\n");
                        System.out.println( "] " + fail.getTestHeader() );
                        fail.getException().printStackTrace();
                        testFailed = true;
                    }
                }
                printSummary(result);
                System.out.println(separator);
            }
            System.out.println(">> TESTS DONE");

            if( testFailed ) {
                System.out.println( "The following tests failed: \n" + failedTestNames );
                System.exit(1);
            }
            System.exit(0);
        } catch (Throwable e) {
            System.exit(1);
        }
    }

    private static void printSummary(Result result) {
        System.out.println("Tests run: " + result.getRunCount()
                + ", Failures: " + result.getFailureCount()
                + ", Skipped: " + result.getIgnoreCount() + "\n");
    }

    /**
     * Retrieve all test classes that are runnable for a given package name.
     * 
     * @param packageName The base package
     * @return The classes
     * 
     * @throws ClassNotFoundException
     * @throws IOException
     */
    private static HashSet<Class> getRunnableTestClasses(String packageName) throws ClassNotFoundException, IOException {
        URL jarURL = getJarURL(packageName);

        String pkgSlash = packageName.replaceAll("\\.", File.separator);

        ZipInputStream zis = new ZipInputStream(jarURL.openStream());
        ZipEntry zippedFile = null;

        HashSet<Class> classes = new HashSet<Class>();
        while ((zippedFile = zis.getNextEntry()) != null) {
            if (zippedFile.getName().matches(pkgSlash + "[^$]*class")) {
                String className = zippedFile.getName().replaceAll("/", ".").replace(".class", "");
                classes.add(Class.forName(className));
            }
        }

        HashSet<Class> testClasses = (HashSet<Class>) classes.clone();
        for (Class clazz : classes) {
            determineIfThisIsARunnableTestClass(clazz, testClasses);
        }

        return testClasses;
    }

    /**
     * Uses the package name of a class in the jar to determine the URL of the jar. 
     * 
     * @param packageName 
     * @return URL
     * @throws IOException
     */
    private static URL getJarURL(String packageName) throws IOException {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        assert classLoader != null;

        // get subdirectories
        String path = packageName.replace('.', '/');
        Enumeration<URL> resources = classLoader.getResources(path);

        boolean classFound = false;
        URL jarURL = null;
        while (resources.hasMoreElements()) {
            if (classFound) {
                throw new RuntimeException("More than one location of " + packageName + " in classpath.");
            }

            URL resource = resources.nextElement();
            String jarPath = resource.getPath();
            jarPath = jarPath.substring(jarPath.indexOf("file:"), jarPath.indexOf('!'));
            jarURL = new URL(jarPath);

            classFound = true;
        }
        assert jarURL != null;

        return jarURL;
    }

    /**
     * This method looks at particular class and determines if it can be run as
     * a JUnit test. As a side-effect, it removes any parent classes from the
     * <code>Set&lt;Class&gt; classes</code>. If the class is determined NOT to
     * be a JUnit test, it is removed from the
     * <code>Set&lt;Class&gt; classes</code>.
     * 
     * @param clazz
     * @param classes
     */
    private static void determineIfThisIsARunnableTestClass(Class clazz, HashSet<Class> classes) {
        boolean isTest = false;
        
        if( Modifier.isAbstract(clazz.getModifiers()) ) { 
            isTest = false;
            classes.remove(clazz);
            return;
        };
        
        Method [] methods = clazz.getMethods();
        for( int m = 0; m < methods.length; ++m ) { 
           if( methods[m].getAnnotation(Test.class) != null ) { 
               isTest = true;
           }
        }

        for (Class superClazz = clazz.getSuperclass(); superClazz != null; superClazz = superClazz.getSuperclass()) {
            classes.remove(superClazz);
            if (superClazz.equals(TestCase.class)) {
                isTest = true;
                continue;
            }
        }

        if (!isTest) {
            classes.remove(clazz);
        }
    }

    static class ClassComparatorByName implements Comparator<Class> {

        @Override
        public int compare(Class o1, Class o2) {
            if( o1 == o2 ) { 
               return 0; 
            }
            if( o1 == null ) { 
                return -1;
            } else if( o2 == null) {
                return 1;
            }
            return o1.getSimpleName().compareTo(o2.getSimpleName());
        }
        
    } 

}
