/*
 * 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.
 */

/* $Id: RunTest.java 1780541 2017-01-27 11:27:04Z ssteiner $ */

package org.apache.fop.tools.anttasks;

import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;

/**
 * Testing ant task.
 * This task is used to test FOP as a build target.
 * This uses the TestConverter (with weak code dependency)
 * to run the tests and check the results.
 */
public class RunTest extends Task {

    private String basedir;
    private String testsuite = "";
    private String referenceJar = "";
    private String refVersion = "";

    /**
     * Sets the test suite name.
     * @param str name of the test suite
     */
    public void setTestSuite(String str) {
        testsuite = str;
    }

    /**
     * Sets the base directory.
     * @param str base directory
     */
    public void setBasedir(String str) {
        basedir = str;
    }

    /**
     * Sets the reference directory.
     * @param str reference directory
     */
    public void setReference(String str) {
        referenceJar = str;
    }

    /**
     * Sets the reference version.
     * @param str reference version
     */
    public void setRefVersion(String str) {
        refVersion = str;
    }

    /**
     * This creates the reference output, if required, then tests
     * the current build.
     * {@inheritDoc}
     */
    public void execute() throws BuildException {
        runReference();
        testNewBuild();
    }

    /**
     * Test the current build.
     * This uses the current jar file (in build/fop.jar) to run the
     * tests with.
     * The output is then compared with the reference output.
     */
    protected void testNewBuild() {
        try {
            ClassLoader loader = new URLClassLoader(
                                    createUrls("build/fop.jar"));
            Map diff = runConverter(loader, "areatree",
                                          "reference/output/");
            if (diff != null && !diff.isEmpty()) {
                System.out.println("====================================");
                System.out.println("The following files differ:");
                boolean broke = false;
                for (Map.Entry<Object, Object> e : (Set<Map.Entry<Object, Object>>) diff.entrySet()) {
                    Object fname = e.getKey();
                    Boolean pass = (Boolean)e.getValue();
                    System.out.println("file: " + fname
                                       + " - reference success: " + pass);
                    if (pass) {
                        broke = true;
                    }
                }
                if (broke) {
                    throw new BuildException("Working tests have been changed.");
                }
            }
        } catch (MalformedURLException mue) {
            mue.printStackTrace();
        }
    }

    /**
     * Run the tests for the reference jar file.
     * This checks that the reference output has not already been
     * run and then checks the version of the reference jar against
     * the version required.
     * The reference output is then created.
     * @throws BuildException if an error occurs
     */
    protected void runReference() throws BuildException {
        // check not already done
        File f = new File(basedir + "/reference/output/");
        // if(f.exists()) {
        // need to check that files have actually been created.
        // return;
        // } else {
        try {
            final URL[] urls = createUrls(referenceJar);
            ClassLoader loader = (ClassLoader)
                AccessController.doPrivileged(new PrivilegedAction() {
                    public Object run() {
                        return new URLClassLoader(urls);
                    }
                });
            boolean failed = false;
            try {
                Class cla = Class.forName("org.apache.fop.apps.Fop", true,
                                    loader);
                Method get = cla.getMethod("getVersion", new Class[]{});
                if (!get.invoke(null, new Object[]{}).equals(refVersion)) {
                    throw new BuildException("Reference jar is not correct version it must be: "
                                             + refVersion);
                }
            } catch (IllegalAccessException iae) {
                failed = true;
            } catch (IllegalArgumentException are) {
                failed = true;
            } catch (InvocationTargetException are) {
                failed = true;
            } catch (ClassNotFoundException are) {
                failed = true;
            } catch (NoSuchMethodException are) {
                failed = true;
            }
            if (failed) {
                throw new BuildException("Reference jar could not be found in: "
                                         + basedir + "/reference/");
            }
            f.mkdirs();
            runConverter(loader, "reference/output/", null);
        } catch (MalformedURLException mue) {
            mue.printStackTrace();
        }
        // }
    }

    /**
     * Run the Converter.
     * Runs the test converter using the specified class loader.
     * This loads the TestConverter using the class loader and
     * then runs the test suite for the current test suite
     * file in the base directory.
     * (Note class loader option provided to allow for different
     * fop.jar and other libraries to be activated.)
     * @param loader the class loader to use to run the tests with
     * @param dest destination directory
     * @param compDir comparison directory
     * @return A Map with differences
     */
    protected Map runConverter(ClassLoader loader, String dest,
                                     String compDir) {
        String converter = "org.apache.fop.tools.TestConverter";

        Map diff = null;
        try {
            Class cla = Class.forName(converter, true, loader);
            Object tc = cla.getDeclaredConstructor().newInstance();
            Method meth;

            meth = cla.getMethod("setBaseDir", new Class[] {
                String.class
            });
            meth.invoke(tc, new Object[] {
                basedir
            });

            meth = cla.getMethod("runTests", new Class[] {
                String.class, String.class, String.class
            });
            diff = (Map)meth.invoke(tc, new Object[] {
                testsuite, dest, compDir
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
        return diff;
    }

    /**
     * Return a list of URL's with the specified URL first and followed
     * by all the jar files from lib/.
     * @return a list of urls to the runtime jar files.
     */
    private URL[] createUrls(String mainJar) throws MalformedURLException {
        List<URL> urls = new ArrayList<URL>();
        urls.add(new File(mainJar).toURI().toURL());
        File[] libFiles = new File("lib").listFiles();
        if (libFiles != null) {
            for (File libFile : libFiles) {
                if (libFile.getPath().endsWith(".jar")) {
                    urls.add(libFile.toURI().toURL());
                }
            }
        }
        return urls.toArray(new URL[urls.size()]);
    }
}
