package org.jacorb.test.harness;

/*
 *        JacORB - a free Java ORB
 *
 *   Copyright (C) 1999-2014 Gerald Brose / The JacORB Team.
 *
 *   This library is free software; you can redistribute it and/or
 *   modify it under the terms of the GNU Library General Public
 *   License as published by the Free Software Foundation; either
 *   version 2 of the License, or (at your option) any later version.
 *
 *   This library is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *   Library General Public License for more details.
 *
 *   You should have received a copy of the GNU Library General Public
 *   License along with this library; if not, write to the Free
 *   Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 */

import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.Socket;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.Random;
import java.util.logging.ConsoleHandler;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import junit.framework.AssertionFailedError;
import org.junit.Assert;
import org.slf4j.Logger;

/**
 * Utility class used to setup JUnit-TestSuite
 *
 * @author Alphonse Bendt
 */

public class TestUtils
{
    private static Random portRandom = new Random();

    /**
     * The minimum server currentMinPort number for IPv4.
     */
    private static final int MIN_PORT = 1024;

    /**
     * The maximum server currentMinPort number for IPv4.
     */
    private static final int MAX_PORT = 65355;


    public static final boolean isIBM = (System.getProperty ("java.vendor").equals ("IBM Corporation"));

    public static final boolean is17 = (System.getProperty ("java.version").startsWith ("1.7"));

    private static String testHome = null;
    private static String jacorbHome = null;
    private static String systemRoot = null;
    private static Logger logger;
    private static java.util.logging.Logger jdkLogger;
    private static java.util.logging.Logger apacheLogger;

    public static final boolean verbose = "true".equalsIgnoreCase(System.getProperty("jacorb.test.verbose"));

    public static final int timeout = Integer.valueOf(System.getProperty("jacorb.test.timeout.server", "60000"));

    public static final boolean isSSLEnabled = Boolean.valueOf(System.getProperty("jacorb.test.ssl", "false"));

    // Ensure logging is initialised
    static
    {
        getLogger();
    }

    /**
     * Return a preconfigured logger for the test framework.
     * @return a SLF4J Logger
     */
    public static Logger getLogger ()
    {
        if (logger == null)
        {
            Formatter formatter = new Formatter()
            {
                @Override
                public String format(LogRecord arg0)
                {
                    StringBuilder b = new StringBuilder();
                    b.append ("[TEST] ");
                    b.append(arg0.getLevel());
                    b.append(" ");
                    b.append(arg0.getMessage());
                    b.append(System.getProperty("line.separator"));

                    Throwable t = arg0.getThrown();
                    return t == null ? b.toString() : b.toString () + getStackTrace (t);
                }

                private String getStackTrace (Throwable t)
                {
                    StringWriter sw = new StringWriter();
                    t.printStackTrace(new PrintWriter (sw));
                    return sw.toString();
                }
            };

            Handler handler = new ConsoleHandler();
            handler.setFormatter(formatter);
            handler.setLevel(TestUtils.verbose ? Level.FINEST : Level.OFF);

            apacheLogger = java.util.logging.Logger.getLogger("org.apache.camel.test");
            apacheLogger.setUseParentHandlers (false);
            apacheLogger.setLevel (TestUtils.verbose ? Level.FINEST : Level.OFF);
            apacheLogger.addHandler(handler);

            jdkLogger = java.util.logging.Logger.getLogger("org.jacorb.test");
            jdkLogger.setUseParentHandlers (false);
            jdkLogger.setLevel (TestUtils.verbose ? Level.FINEST : Level.OFF);
            jdkLogger.addHandler(handler);

            logger = org.slf4j.LoggerFactory.getLogger ("org.jacorb.test");
        }
        return logger;
    }

    public static String jacorbHome()
    {
        if (jacorbHome == null)
        {
            jacorbHome = osDependentPath((testHome() + "/../../"));
        }
        return jacorbHome;
    }

    /**
     * Returns the name of the home directory of this regression suite.
     */
    public static String testHome()
    {
        if (testHome == null)
        {
            // See if we set it as a property using TestLauncher
            testHome = System.getProperty ("jacorb.test.home");

            // Try to find it from the run directory
            if (testHome == null)
            {
                URL url = TestUtils.class.getResource("/.");

                String result = url.toString();
                if (result.matches("file:/.*?"))
                {
                    result = result.substring (5, result.indexOf("/target/test-classes/"));
                }
                testHome = osDependentPath(result);
            }
        }

        if (testHome == null)
        {
            throw new RuntimeException("unable to determine testhome (set it with -Djacorb.test.home)");
        }

        return testHome;
    }

    public static String osDependentPath(String path)
    {
        path=(new File(path)).toString();
        return path.trim();
    }

    /**
     * In addition to file and path separators being different,
     * Windows requires an additional environment variable for
     * SystemRoot in DirectLauncer.
     */
    public static boolean isWindows()
    {
        return (System.getProperty("os.name").indexOf("Windows") != -1) ;
    }

    /**
     * Returns the SystemRoot, should be used on Windows only.  This
     * is necessary to prevent the following error:
     * "Unrecognised Windows Sockets error: 10106: create"
     */
    public static String systemRoot() throws RuntimeException, java.io.IOException
    {

        if (isWindows())
        {
            if (systemRoot == null)
            {
                //See if we set it as a property using TestLauncher.
                //This means we are likely in the DirectLauncher
                systemRoot = System.getProperty ("jacorb.SystemRoot");

                if (systemRoot == null)
                {
                    //We are likely in the TestLauncher, see if we can
                    //get it from the system environment.
                    Properties env = new Properties();
                    try
                    {
                        String setCmd = getSetCommand();
                        Process proc = Runtime.getRuntime().exec(setCmd);
                        InputStream ioStream = proc.getInputStream();
                        env.load(ioStream);
                        systemRoot = env.getProperty("SystemRoot");
                        ioStream.close();
                        proc.destroy();
                    }
                    catch(java.io.IOException e)
                    {
                        throw e;
                    }
                    if (systemRoot == null)
                    {
                        throw new RuntimeException("Could not find SystemRoot, make sure SystemRoot env var is set");
                    }
                    //The '\' character gets interpeted as an escape
                    if (systemRoot.charAt(1) == ':' && systemRoot.charAt(2) != '\\')
                    {
                        String prefix = systemRoot.substring(0, 2);
                        String suffix = systemRoot.substring(2, systemRoot.length());
                        systemRoot = prefix + "\\" + suffix;
                    }
                }
            }
        }
        else
        {
            throw new RuntimeException("TestUtils.systemRoot() was called on a non-Windows OS");
        }
        return systemRoot;
    }

    /**
     * Private method to get the set command.  This is necessary because
     * if SystemRoot has not been set, it needs to be set for DirectLauncer.
     */
    private static String getSetCommand()
    {
        String setCmd;
        String osName = System.getProperty("os.name");

        if (osName.indexOf("indows")  != -1)
        {
            if (osName.indexOf("indows 9") != -1)
            {
                setCmd = "command.com /c set";
            }
            else
            {
                setCmd = "cmd.exe /c set";
            }
        }
        else
        {
            setCmd = "/usr/bin/env";
            //should double check for all unix platforms
        }
        return setCmd;
    }

    /**
     * create properties that contain the correct (JDK specific)
     * settings to create a Sun or IBM ORB.
     */
    public static Properties newForeignORBProperties()
    {
        final Properties props = new Properties();

        if (TestUtils.isIBM)
        {
            props.setProperty ("org.omg.CORBA.ORBClass", "com.ibm.CORBA.iiop.ORB");
            props.setProperty ("org.omg.CORBA.ORBSingletonClass", "com.ibm.rmi.corba.ORBSingleton");
        }
        else
        {
            props.setProperty("org.omg.CORBA.ORBClass", "com.sun.corba.se.impl.orb.ORBImpl");
            props.setProperty("org.omg.CORBA.ORBSingletonClass", "com.sun.corba.se.impl.orb.ORBSingleton");
        }
        return props;
    }

    public static boolean getStringAsBoolean(String value)
    {
        return "true".equalsIgnoreCase(value) || "on".equalsIgnoreCase(value) || "yes".equalsIgnoreCase(value);
    }

    public static void deleteRecursively(File name)
    {
        if (name.isDirectory())
        {
            File[] subdirs = name.listFiles(new FileFilter()
            {
                @Override
                public boolean accept(File pathname)
                {
                    return pathname.isDirectory();
                }
            });

            for (int i = 0; i < subdirs.length; ++i)
            {
                deleteRecursively(subdirs[i]);
            }

            File[] files = name.listFiles(new FileFilter()
            {
                @Override
                public boolean accept(File pathname)
                {
                    return pathname.isFile();
                }
            });

            for (int i = 0; i < files.length; ++i)
            {
                files[i].delete();
            }

            name.delete();
        }

        if (name.isFile())
        {
            name.delete();
        }
    }

    public static File createTempDir(final String name)
    {
        String dir = System.getProperty("java.io.tmpdir");
        dir += File.separator + name + "-" + System.currentTimeMillis();
        final File tmpDir = new File(dir);
        Assert.assertTrue(tmpDir.mkdir());
        tmpDir.deleteOnExit();
        Runtime.getRuntime().addShutdownHook(new Thread()
        {
            @Override
            public void run()
            {
                TestUtils.deleteRecursively(tmpDir);
            };
        });

        return tmpDir;
    }

    private static File[] getSubDirectories(File src)
    {
        return src.listFiles(new FileFilter()
        {
            @Override
            public boolean accept(File pathname)
            {
                return pathname.isDirectory();
            }
        });
    }

    private static List<File> getFilesInDirectory(File src, final String suffix)
    {
        final File[] fileList = src.listFiles(new FilenameFilter()
        {
            @Override
            public boolean accept(File dir, String name)
            {
                return name.endsWith(suffix);
            }
        });
        Assert.assertNotNull(src + " does not exist.", fileList);
        return Arrays.asList(fileList);
    }

    public static List<File> getJavaFilesRecursively(File src)
    {
        final String suffix = ".java";
        return getFilesRecursively(src, suffix);
    }

    public static List<File> getFilesRecursively(File src, final String suffix)
    {
        List<File> result = new ArrayList<File>();
        result.addAll(getFilesInDirectory(src, suffix));

        File[] dirs = getSubDirectories(src);

        for (int i = 0; i < dirs.length; ++i)
        {
            result.addAll(getFilesRecursively(dirs[i], suffix));
        }

        return result;
    }

    public static ClassLoader compileJavaFiles(File dirCompilation, File[] files, boolean failureExpected) throws IOException
    {
        Assert.assertNotNull(files);

        if (files.length == 0)
        {
            return null;
        }

        dirCompilation.mkdir();

        Assert.assertTrue(dirCompilation.isDirectory());
        Assert.assertTrue(dirCompilation.exists());
        Assert.assertTrue(dirCompilation.canWrite());
        File file = new File(dirCompilation, "files.txt");
        file.delete();
        file.createNewFile();

        PrintWriter writer = new PrintWriter(new FileWriter(file));

        for (int i = 0; i < files.length; ++i)
        {
            writer.println(files[i].getAbsolutePath());
        }

        writer.close();

        String javaHome = System.getProperty("java.home");
        String testHome = testHome();
        String classpath = testHome + File.separator + ".." + File.separator + ".." + File.separator + "classes";

        if (javaHome.endsWith("jre"))
        {
            javaHome = javaHome.substring(0, javaHome.length() - 4);
        }
        String cmd = javaHome + "/bin/javac -d " + dirCompilation + " -bootclasspath " + classpath + ":" + System.getProperty("sun.boot.class.path") + " @" + file.getAbsolutePath();

        TestUtils.getLogger().debug("[COMPILE] " + cmd);
        TestUtils.getLogger().debug("[COMPILE] " + files.length + " java files");
        try
        {
            Process proc = Runtime.getRuntime().exec(cmd);

            int exit = proc.waitFor();
            if (failureExpected && exit == 0)
            {
                Assert.fail("should fail: " + cmd);
            }

            if (exit != 0)
            {
                InputStream in = proc.getErrorStream();
                LineNumberReader reader = new LineNumberReader(new InputStreamReader(in));
                StringBuffer b = new StringBuffer();

                String line;
                while ((line = reader.readLine()) != null)
                {
                    b.append(line);
                    b.append("\n");
                }

                Assert.fail(cmd + "\n" + b.toString());
            }

            return new URLClassLoader(new URL[] {dirCompilation.toURI().toURL()});
        }
        catch (Exception e)
        {
            if (!failureExpected)
            {
                AssertionFailedError error = new AssertionFailedError("cmd: " + cmd);
                error.initCause(e);
                throw error;
            }
            return null;
        }
    }

    public static long getSystemPropertyAsLong(final String property, final long defaultValue)
    {
        long parseLong;
        try
        {
            parseLong = Long.parseLong(System.getProperty(property));
        }
        catch (Exception e)
        {
            parseLong = defaultValue;
        }
        return parseLong;
    }

    public static boolean getSystemPropertyAsBoolean(final String property, final boolean defaultValue)
    {
        String value = System.getProperty (property);
        if (value == null)
        {
            return defaultValue;
        }
        return getStringAsBoolean(value);
    }

    public static long getMediumTimeout()
    {
        return Long.getLong("jacorb.test.timeout.medium", 20000).longValue();
    }

    /**
     * wait until a server IOR shows up (file size &gt; 0) and print it out
     * to stdout so it can be picked up by a client process.
     */
    public static void printServerIOR(File iorFile)
                                                    throws InterruptedException,
                                                    FileNotFoundException,
                                                    IOException
    {
        long maxWait = System.currentTimeMillis() + timeout;

        while(iorFile.length() == 0 && System.currentTimeMillis() < maxWait)
        {
            Thread.sleep(1000);
        }

        Thread.sleep(1000);

        BufferedReader in = new BufferedReader(new FileReader(iorFile));
        String ior = in.readLine();
        in.close();

        if (ior == null)
        {
            throw new IllegalArgumentException("cannot read IOR from file " + iorFile + " within " + TestUtils.timeout);
        }

        System.out.println("SERVER IOR: " + ior);
    }

    /**
     * copied here from ObjectUtil to make the package org.jacorb.test.harness independent from the orb core
     */
    public static Class<?> classForName(String name)
        throws ClassNotFoundException, IllegalArgumentException
    {
        if (name == null)
        {
            throw new IllegalArgumentException("Class name must not be null!");
        }

        try
        {
            // Here we prefer classLoader.loadClass() over the three-argument
            // form of Class.forName(), as the latter is reported to cause
            // caching of stale Class instances (due to a buggy cache of
            // loaded classes).
            return Thread.currentThread().getContextClassLoader().loadClass(name);
        }
        catch (Exception e)
        {
            // As a fallback, we prefer Class.forName(name) because it loads
            // array classes (i.e., it handles arguments like
            // "[Lsome.class.Name;" or "[[I;", which classLoader.loadClass()
            // does not handle).
            return Class.forName(name);
        }
    }

    public static int patternMatcher(Pattern pattern, String text)
    {
        Matcher matcher = pattern.matcher(text);

        if (matcher.find())
        {
            return matcher.end();
        }

        return 0;
    }


    /**
     * Locate an available port.
     *
     * https://stackoverflow.com/questions/434718/sockets-discover-port-availability-using-java
     * https://stackoverflow.com/questions/2675362/how-to-find-an-available-port
     * </br>
     * Only meant to be used by those tests that inherit from {@link FixedPortORBTestCase} or
     * {@link FixedPortClientServerTestCase}
     */
    static int getNextAvailablePort()
    {
        int result = MIN_PORT, port = 0, counter = 0;

        // Only try so many times to find a usable port.
        while ( counter < ( MAX_PORT - MIN_PORT ) )
        {
            // Attempt to find a usable port randomly.
            port = randInt (MIN_PORT, MAX_PORT);
            if ( available ( port ))
            {
                result = port;
                break;
            }
            counter++;
        }
        getLogger().debug("Returning port " + port);

        return result;
    }

    /**
     * Returns a pseudo-random number between min and max, inclusive.
     * The difference between min and max can be at most
     * <code>Integer.MAX_VALUE - 1</code>.
     *
     * @param min Minimum value
     * @param max Maximum value.  Must be greater than min.
     * @return Integer between min and max, inclusive.
     * @see java.util.Random#nextInt(int)
     */
    private static int randInt(int min, int max)
    {
        // nextInt is normally exclusive of the top value,
        // so add 1 to make it inclusive
        int randomNum = portRandom.nextInt((max - min) + 1) + min;

        return randomNum;
    }

    /**
     * Attempt to determine if a socket is in use.
     *
     * @param port
     * @return
     */
    private static boolean available(int port)
    {
        Socket s = null;
        try
        {
            s = new Socket("localhost", port);
            s.setReuseAddress(true);

            // If the code makes it this far without an exception it means
            // something is using the port and has responded.
            getLogger().debug("Port " + port + " is not available");
            return false;
        }
        catch (IOException e)
        {
            getLogger().debug("Port " + port + " is available");
            return true;
        }
        finally
        {
            if (s != null)
            {
                try
                {
                    s.close();
                }
                catch (IOException e)
                {
                    throw new RuntimeException("Failed to close the socket", e);
                }
            }
        }
    }
}
