/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.jdo.tck.util;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;

import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestResult;
import junit.framework.TestSuite;
import junit.textui.ResultPrinter;
import junit.textui.TestRunner;

import javax.jdo.JDOFatalException;

import org.apache.jdo.tck.JDO_Test;

/**
 * TestRunner class for running a single test or a test suite in batch
 * mode. The format of the test output is specified by the result printer
 * class. The main method sets an exit code according to the test result:
 * <ul>
 * <li><code>0</code>: success
 * <li><code>1</code>: failure, the test shows an unexpected behavior
 * <li><code>2</code>: exception, the test throws an unhandled excption 
 * </ul>
 * 
 * @author Michael Bouschen
 */
public class BatchTestRunner
    extends TestRunner
{
    /** Name of the system property to specify the result printer class. */
    public static final String RESULTPRINTER_PROPERTY = "ResultPrinterClass"; 
    
    /** Default of the system property ResultPrinterClass. */
    public static final String RESULTPRINTER_DEFAULT = BatchResultPrinter.class.getName();

    /** */
    public static final String LOG_DIRECTORY;

    static {
        String directory = System.getProperty("jdo.tck.log.directory");
        if (directory!=null &&
            !directory.endsWith(File.separator)) {
            directory += File.separator;
        }
        LOG_DIRECTORY = directory;
    }
    

    /** 
     * Constructor. 
     * It creates a result printer instance based on the system property
     * and delegates to the constructor taking a result printer argument. 
     */
    public BatchTestRunner() {
        super();
        setPrinter(getResultPrinter());
    }

    /**
     * Constructor. Uses the specified resultPrinter to format the test result.
     * @param resultPrinter the result printer
     */
    public BatchTestRunner(ResultPrinter resultPrinter) {
        super(resultPrinter);
    }

    /**
     * Runs all test methods from the specified class.
     * @param clazz class object
     */
    public static void run(Class clazz) {
        run(new TestSuite(clazz));
    }
    
    /**
     * Runs the specified test or test suite
     * @param test test or test suite
     * @return the test result
     */
    public static TestResult run(Test test) {
        return new BatchTestRunner().doRun(test);
    }

    /**
     * Runs the specified test and waits until the user types RETURN.
     * @param suite suite
     */
    public static void runAndWait(Test suite) {
        new BatchTestRunner().doRun(suite, true);
    }

    /**
     * Runs the specified test or test suite and closes the pmf.
     * @param test test or test suite
     * @return the test result
     */
    public TestResult doRun(Test test) {
        TestResult result = null;
        try {
            result = doRun(test, false);
            JDO_Test.dumpSupportedOptions(LOG_DIRECTORY + "configuration");
        } finally {
            JDO_Test.closePMF();
        }
        return result;
    }

    /**
     * Runs in batch mode and sets an exit code. If the specified String
     * array includes a single fully qualified class name, this test class
     * is executed.
     * @param args command line arguments
     */
    public static void main(String args[]) {
        try {
            TestResult r = new BatchTestRunner().start(args);
            if (!r.wasSuccessful()) 
                System.exit(FAILURE_EXIT);
            System.exit(SUCCESS_EXIT);
        } catch(Exception e) {
            System.err.println(e.getMessage());
            System.exit(EXCEPTION_EXIT);
        }
    }

    /**
     * Starts the test
     * @param args arguments
     * @return the test result
     */
    public TestResult start(String[] args) {
        Test suite = null;
        if ((args == null) || args.length == 0) {
            String conf = System.getProperty("jdo.tck.cfg");
            throw new JDOFatalException(
                "Missing JDO TCK test classes for configuration '" + conf + 
                "'. Please check the property 'jdo.tck.classes'.");
        }
        else if (args.length == 1) {
            suite = getTest(args[0]);
        }
        else {
            suite = getTestSuite(args);
        }
        return doRun(suite);
    }

    /**
     * Returns a JUnit TestSuite instance for the classes of the specified
     * list of class names.
     * @param classNames class names
     * @return the test suite
     */
    protected TestSuite getTestSuite(String[] classNames) {
        TestSuite testSuite = new TestSuite();
        for (int i = 0; i < classNames.length; i++) {
            String className = classNames[i];
            try {
                Class<? extends TestCase> clazz = (Class<? extends TestCase>)Class.forName(className);
                testSuite.addTestSuite(clazz);
            }
            catch (ClassNotFoundException ex) {
                System.out.println(
                    "Cannot find test class '" + className + "'.");
            }
        }
        return testSuite;
    }

    /**
     * Returns a result printer instance. The system property
     * ResultPrinterClass specifies the class of the returned instanec. The
     * class must extend junit.textui.ResultPrinter.
     * @return the result printer
     */
    protected ResultPrinter getResultPrinter() {
        String className =  System.getProperty(RESULTPRINTER_PROPERTY);
        if (className != null) {
            className = className.trim();
            if (className.length() != 0) {
                String msg = null;
                try {
                    // get class instance
                    Class clazz = Class.forName(className);
                    
                    Constructor ctor = null;
                    OutputStream stream = null;

                    // choose constructor taking ConsoleFileOutput arg
                    if (!Boolean.getBoolean("no.log.file")) {
                        try {
                            ctor = clazz.getConstructor(
                            new Class[] { ConsoleFileOutput.class } );
                            stream = new ConsoleFileOutput();
                        }
                        catch (NoSuchMethodException ex) {
                            ctor = null;
                        }
                    }
                    
                    // choose constructor taking PrintStream arg
                    if (ctor == null) {
                        ctor = clazz.getConstructor(
                                new Class[] { PrintStream.class } );
                        stream = System.out;
                    }
                        
                    return (ResultPrinter)ctor.newInstance(
                        new Object[] { stream });
                }
                catch (ClassNotFoundException ex) {
                    // specified ResultPrinter class not 
                    msg = "Cannot find specified result printer class " + 
                        className + ".";
                }
                catch (NoSuchMethodException ex) {
                    msg = "Class " + className + 
                        " does not provide constructor taking a PrintStream.";
                }
                catch (InstantiationException ex) {
                    msg = "Class " + className + " is abstract.";
                }
                catch (IllegalAccessException ex) {
                    msg = "Constructor taking a PrintStream of class " + 
                        className + " is not accessible.";
                }
                catch (InvocationTargetException ex) {
                    msg = "Constructor call results in exception " + ex + ".";
                }

                // ResultPrinter class specified, but not avaiable
                System.out.println(msg);
                ResultPrinter printer = getDefaultResultPrinter();
                System.out.println("Using default result printer of class " + 
                                   printer.getClass().getName());
            }
        }
        
        // ResultPrinter class not specified => use default
        return getDefaultResultPrinter();
    }

    /**
     * Returns an instance of the default result printer class
     * BatchResultPrinter.
     * @return the default result printer
     */
    protected ResultPrinter getDefaultResultPrinter() {
        return new BatchResultPrinter(System.out);
    }
    
    /**
     * Returns a file name which is determined by method
     * {@link BatchTestRunner#changeFileName(String)}.
     * The file name has suffix <code>.txt</code>.
     * @return the file name
     */
    public static String getFileName() {
        return changeFileName("junit.txt");
    }
    
    /**
     * Returns a file name which is constructed by values
     * of some system properties appended by the given file name.
     * The system properties evaluated are:
     * <ul>
     * <li>jdo.tck.log.directory: Specifies the directory for the file.</li>
     * <li>jdo.tck.database, jdo.tck.cfg: 
     * The values of these properties prepend the given file name.</li>
     * <li>jdo.tck.identitytype: The value of this property is replaced by
     * <code>"app"</code> if it equals <code>"applicationidentity"</code>, 
     * else it is replaced by <code>"dsid"</code>.</li>
     * </ul>
     * The returned file name is constructed as follows:<br>
     * ${jdo.tck.log.directory}/${jdo.tck.database}-${jdo.tck.identitytype}-${jdo.tck.cfg}${given file name}
     * Values of properties which do not exist default to <code>""</code>. 
     * @param fileName the file name
     * @return the changed file name
     */
    public static String changeFileName(String fileName) {
        String directory = LOG_DIRECTORY;
        String db = System.getProperty("jdo.tck.database");
        
        String identityType = System.getProperty("jdo.tck.identitytype");
        if (identityType!=null) {
            if (identityType.equals("applicationidentity")) {
                identityType = "app";
            } else { 
                identityType = "dsid";
            }
        }
        
        String configuration = System.getProperty("jdo.tck.cfg");
        if (configuration!=null) {
            int index = configuration.indexOf('.');
            if (index!=-1) {
                configuration = configuration.substring(0, index);
            }
        }
        
        directory = fixPartialFileName(directory);
        db = fixPartialFileName(db, '-',
                new String[]{identityType, configuration, fileName});
        identityType = fixPartialFileName(identityType, '-',
                new String[]{configuration, fileName});
        configuration = fixPartialFileName(configuration, '-',
                new String[]{fileName});

        return directory + db + identityType + configuration + fileName;
    }
    
    private static String fixPartialFileName(String str) {
        if (str==null) {
            str = "";
        }
        return str;
    }
    
    private static String fixPartialFileName(String str, char c, 
            String[] values) {
        str = fixPartialFileName(str);
        if (!str.equals("")) {
            for (int i = 0; i < values.length; i++) {
                String value = values[i];
                if (value!=null &&
                    !value.equals("") &&
                    !value.startsWith(".")) {
                    str += c;
                    break;
                }
            }
        }
        return str;
    }
}
