/*******************************************************************************
 * Copyright (c) 2011, 2012 Tasktop Technologies and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Tasktop Technologies - initial API and implementation
 *******************************************************************************/

package org.eclipse.mylyn.commons.sdk.util;

import static com.google.common.base.Preconditions.checkNotNull;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.util.Enumeration;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import junit.framework.AssertionFailedError;

import org.apache.commons.lang.reflect.MethodUtils;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Plugin;
import org.eclipse.mylyn.commons.core.CoreUtil;
import org.eclipse.mylyn.commons.core.net.NetUtil;
import org.eclipse.mylyn.commons.net.WebUtil;
import org.eclipse.mylyn.commons.repositories.core.auth.CertificateCredentials;
import org.eclipse.mylyn.commons.repositories.core.auth.UserCredentials;
import org.eclipse.mylyn.internal.commons.net.CommonsNetPlugin;
import org.eclipse.osgi.service.resolver.VersionRange;
import org.eclipse.osgi.util.NLS;
import org.osgi.framework.Bundle;

/**
 * @author Steffen Pingel
 */
@SuppressWarnings("restriction")
public class CommonTestUtil {

	public enum PrivilegeLevel {
		ADMIN, ANONYMOUS, GUEST, READ_ONLY, USER
	}

	public static final String KEY_CREDENTIALS_FILE = "mylyn.credentials";

	private static final String KEY_IGNORE_LOCAL_SERVICES = "org.eclipse.mylyn.tests.ignore.local.services";

	private final static int MAX_RETRY = 5;

	/**
	 * Returns the given file path with its separator character changed from the given old separator to the given new
	 * separator.
	 * 
	 * @param path
	 *            a file path
	 * @param oldSeparator
	 *            a path separator character
	 * @param newSeparator
	 *            a path separator character
	 * @return the file path with its separator character changed from the given old separator to the given new
	 *         separator
	 */
	public static String changeSeparator(String path, char oldSeparator, char newSeparator) {
		return path.replace(oldSeparator, newSeparator);
	}

	/**
	 * Copies the given source file to the given destination file.
	 */
	public static void copy(File source, File dest) throws IOException {
		InputStream in = new FileInputStream(source);
		try {
			OutputStream out = new FileOutputStream(dest);
			try {
				transferData(in, out);
			} finally {
				out.close();
			}
		} finally {
			in.close();
		}
	}

	/**
	 * Copies all files in the current data directory to the specified folder. Will overwrite.
	 */
	public static void copyFolder(File sourceFolder, File targetFolder) throws IOException {
		for (File sourceFile : sourceFolder.listFiles()) {
			if (sourceFile.isFile()) {
				File destFile = new File(targetFolder, sourceFile.getName());
				copy(sourceFile, destFile);
			}
		}
	}

	/**
	 * Copies all files in the current data directory to the specified folder. Will overwrite.
	 */
	public static void copyFolderRecursively(File sourceFolder, File targetFolder) throws IOException {
		for (File sourceFile : sourceFolder.listFiles()) {
			if (sourceFile.isFile()) {
				File destFile = new File(targetFolder, sourceFile.getName());
				copy(sourceFile, destFile);
			} else if (sourceFile.isDirectory()) {
				File destDir = new File(targetFolder, sourceFile.getName());
				if (!destDir.exists()) {
					if (!destDir.mkdir()) {
						throw new IOException("Unable to create destination folder: " + destDir.getAbsolutePath());
					}
				}
				copyFolderRecursively(sourceFile, destDir);
			}
		}
	}

	public static File createTempFileInPlugin(Plugin plugin, IPath path) {
		IPath stateLocation = plugin.getStateLocation();
		stateLocation = stateLocation.append(path);
		return stateLocation.toFile();
	}

	public static File createTempFolder(String prefix) throws IOException {
		File location = File.createTempFile(prefix, null);
		location.delete();
		location.mkdirs();
		return location;
	}

	public static void delete(File file) {
		if (file.exists()) {
			for (int i = 0; i < MAX_RETRY; i++) {
				if (file.delete()) {
					i = MAX_RETRY;
				} else {
					try {
						Thread.sleep(1000); // sleep a second
					} catch (InterruptedException e) {
						// don't need to catch this
					}
				}
			}
		}
	}

	public static void deleteFolder(File path) {
		if (path.isDirectory()) {
			for (File file : path.listFiles()) {
				file.delete();
			}
			path.delete();
		}
	}

	public static void deleteFolderRecursively(File path) {
		File[] files = path.listFiles();
		if (files != null) {
			for (File file : files) {
				if (file.isDirectory()) {
					deleteFolderRecursively(file);
				} else {
					file.delete();
				}
			}
		}
		path.delete();
	}

	public static CertificateCredentials getCertificateCredentials() {
		File keyStoreFile;
		try {
			keyStoreFile = CommonTestUtil.getFile(CommonTestUtil.class, "testdata/keystore");
			String password = CommonTestUtil.getUserCredentials().getPassword();
			return new CertificateCredentials(keyStoreFile.getAbsolutePath(), password, null);
		} catch (IOException cause) {
			AssertionFailedError e = new AssertionFailedError("Failed to load keystore file");
			e.initCause(cause);
			throw e;
		}
	}

	public static boolean hasCredentials(PrivilegeLevel level) {
		try {
			CommonTestUtil.getCredentials(level);
			return true;
		} catch (AssertionFailedError error) {
			return false;
		}
	}

	public static UserCredentials getCredentials(PrivilegeLevel level) {
		return getCredentials(level, null);
	}

	public static UserCredentials getCredentials(PrivilegeLevel level, String realm) {
		Properties properties = new Properties();
		try {
			File file;
			String filename = System.getProperty(KEY_CREDENTIALS_FILE);
			if (filename != null) {
				// 1. use user specified file
				file = new File(filename);
			} else {
				// 2. check in home directory
				file = new File(new File(System.getProperty("user.home"), ".mylyn"), "credentials.properties");
				if (!file.exists()) {
					// 3. fall back to included credentials file
					file = getFile(CommonTestUtil.class, "testdata/credentials.properties");
				}
			}
			properties.load(new FileInputStream(file));
		} catch (Exception e) {
			AssertionFailedError error = new AssertionFailedError(
					"must define credentials in $HOME/.mylyn/credentials.properties");
			error.initCause(e);
			throw error;
		}

		String defaultPassword = properties.getProperty("pass");

		realm = (realm != null) ? realm + "." : "";
		switch (level) {
		case ANONYMOUS:
			return createCredentials(properties, realm + "anon.", "", "");
		case GUEST:
			return createCredentials(properties, realm + "guest.", "guest@mylyn.eclipse.org", defaultPassword);
		case USER:
			return createCredentials(properties, realm, "tests@mylyn.eclipse.org", defaultPassword);
		case READ_ONLY:
			return createCredentials(properties, realm, "read-only@mylyn.eclipse.org", defaultPassword);
		case ADMIN:
			return createCredentials(properties, realm + "admin.", "admin@mylyn.eclipse.org", null);
		}

		throw new AssertionFailedError("invalid privilege level");
	}

	private static boolean isOsgiVersion310orNewer(ClassLoader classLoader) {
		return classLoader.getClass().getName().equals("org.eclipse.osgi.internal.loader.ModuleClassLoader");
	}

	public static File getFile(Object source, String filename) throws IOException {
		Class<?> clazz = (source instanceof Class<?>) ? (Class<?>) source : source.getClass();
		if (Platform.isRunning()) {
			ClassLoader classLoader = clazz.getClassLoader();
			try {
				if (isOsgiVersion310orNewer(classLoader)) {
					return checkNotNull(getFileFromClassLoader4Luna(filename, classLoader));
				} else {
					return checkNotNull(getFileFromClassLoaderBeforeLuna(filename, classLoader));
				}
			} catch (Exception e) {
				AssertionFailedError exception = new AssertionFailedError(NLS.bind(
						"Could not locate {0} using classloader for {1}", filename, clazz));
				exception.initCause(e);
				throw exception;
			}
		} else {
			return getFileFromNotRunningPlatform(filename, clazz);
		}
	}

	private static File getFileFromNotRunningPlatform(String filename, Class<?> clazz)
			throws UnsupportedEncodingException, IOException {
		URL localURL = clazz.getResource("");
		String path = URLDecoder.decode(localURL.getFile(), Charset.defaultCharset().name());
		int i = path.indexOf("!");
		if (i != -1) {
			int j = path.lastIndexOf(File.separatorChar, i);
			if (j != -1) {
				path = path.substring(0, j) + File.separator;
			} else {
				throw new AssertionFailedError("Unable to determine location for '" + filename + "' at '" + path + "'");
			}
			// class file is nested in jar, use jar path as base
			if (path.startsWith("file:")) {
				path = path.substring(5);
			}
			return new File(path + filename);
		} else {
			// remove all package segments from name
			String directory = clazz.getName().replaceAll("[^.]", "");
			directory = directory.replaceAll(".", "../");
			if (path.contains("/bin/")) {
				// account for bin/ when running from Eclipse workspace
				directory += "../";
			} else if (path.contains("/target/classes/")) {
				// account for bin/ when running from Eclipse workspace
				directory += "../../";
			}
			filename = path + (directory + filename).replaceAll("/", Matcher.quoteReplacement(File.separator));
			return new File(filename).getCanonicalFile();
		}
	}

	private static File getFileFromClassLoaderBeforeLuna(String filename, ClassLoader classLoader) throws Exception {
		Object classpathManager = MethodUtils.invokeExactMethod(classLoader, "getClasspathManager", null);
		Object baseData = MethodUtils.invokeExactMethod(classpathManager, "getBaseData", null);
		Bundle bundle = (Bundle) MethodUtils.invokeExactMethod(baseData, "getBundle", null);
		URL localURL = FileLocator.toFileURL(bundle.getEntry(filename));
		return new File(localURL.getFile());
	}

	private static File getFileFromClassLoader4Luna(String filename, ClassLoader classLoader) throws Exception {
		Object classpathManager = MethodUtils.invokeExactMethod(classLoader, "getClasspathManager", null);
		Object generation = MethodUtils.invokeExactMethod(classpathManager, "getGeneration", null);
		Object bundleFile = MethodUtils.invokeExactMethod(generation, "getBundleFile", null);
		File file = (File) MethodUtils.invokeExactMethod(bundleFile, "getFile", new Object[] { filename, true },
				new Class[] { String.class, boolean.class });
		return file;
	}

	public static InputStream getResource(Object source, String filename) throws IOException {
		Class<?> clazz = (source instanceof Class<?>) ? (Class<?>) source : source.getClass();
		ClassLoader classLoader = clazz.getClassLoader();
		InputStream in = classLoader.getResourceAsStream(filename);
		if (in == null) {
			File file = getFile(source, filename);
			if (file != null) {
				return new FileInputStream(file);
			}
		}
		if (in == null) {
			throw new IOException(NLS.bind("Failed to locate ''{0}'' for ''{1}''", filename, clazz.getName()));
		}
		return in;
	}

	public static UserCredentials getUserCredentials() {
		return getCredentials(PrivilegeLevel.USER, null);
	}

	public static String read(File source) throws IOException {
		InputStream in = new FileInputStream(source);
		try {
			StringBuilder sb = new StringBuilder();
			byte[] buf = new byte[1024];
			int len;
			while ((len = in.read(buf)) > 0) {
				sb.append(new String(buf, 0, len));
			}
			return sb.toString();
		} finally {
			in.close();
		}
	}

	/**
	 * Returns whether to run a limited suite of tests. Returns true, unless a system property has been set to force
	 * running of all tests.
	 */
	public static boolean runHeartbeatTestsOnly() {
		return !Boolean.parseBoolean(System.getProperty("org.eclipse.mylyn.tests.all"));
	}

	/**
	 * Unzips the given zip file to the given destination directory extracting only those entries the pass through the
	 * given filter.
	 * 
	 * @param zipFile
	 *            the zip file to unzip
	 * @param dstDir
	 *            the destination directory
	 * @throws IOException
	 *             in case of problem
	 */
	public static void unzip(ZipFile zipFile, File dstDir) throws IOException {
		unzip(zipFile, dstDir, dstDir, 0);
	}

	public static void write(String fileName, StringBuffer content) throws IOException {
		Writer writer = new FileWriter(fileName);
		try {
			writer.write(content.toString());
		} finally {
			try {
				writer.close();
			} catch (IOException e) {
				// don't need to catch this
			}
		}
	}

	private static UserCredentials createCredentials(Properties properties, String prefix, String defaultUsername,
			String defaultPassword) {
		String username = properties.getProperty(prefix + "user");
		String password = properties.getProperty(prefix + "pass");

		if (username == null) {
			username = defaultUsername;
		}

		if (password == null) {
			password = defaultPassword;
		}

		if (username == null || password == null) {
			throw new AssertionFailedError("username or password not found for " + prefix
					+ " in <plug-in dir>/credentials.properties, make sure file is valid");
		}

		return new UserCredentials(username, password);
	}

	/**
	 * Copies all bytes in the given source stream to the given destination stream. Neither streams are closed.
	 * 
	 * @param source
	 *            the given source stream
	 * @param destination
	 *            the given destination stream
	 * @throws IOException
	 *             in case of error
	 */
	private static void transferData(InputStream in, OutputStream out) throws IOException {
		byte[] buf = new byte[1024];
		int len;
		while ((len = in.read(buf)) > 0) {
			out.write(buf, 0, len);
		}
	}

	private static void unzip(ZipFile zipFile, File rootDstDir, File dstDir, int depth) throws IOException {

		Enumeration<? extends ZipEntry> entries = zipFile.entries();

		try {
			while (entries.hasMoreElements()) {
				ZipEntry entry = entries.nextElement();
				if (entry.isDirectory()) {
					continue;
				}
				String entryName = entry.getName();
				File file = new File(dstDir, changeSeparator(entryName, '/', File.separatorChar));
				file.getParentFile().mkdirs();
				InputStream src = null;
				OutputStream dst = null;
				try {
					src = zipFile.getInputStream(entry);
					dst = new FileOutputStream(file);
					transferData(src, dst);
				} finally {
					if (dst != null) {
						try {
							dst.close();
						} catch (IOException e) {
							// don't need to catch this
						}
					}
					if (src != null) {
						try {
							src.close();
						} catch (IOException e) {
							// don't need to catch this
						}
					}
				}
			}
		} finally {
			try {
				zipFile.close();
			} catch (IOException e) {
				// don't need to catch this
			}
		}
	}

	public static boolean isCertificateAuthBroken() {
		// not entirely correct since 1.6.0_3 would also satisfy this check but it should be sufficient in reality
		return new VersionRange("[0.0.0,1.6.0.25]").isIncluded(CoreUtil.getRuntimeVersion());
	}

	public static boolean hasCertificateCredentials() {
		try {
			CommonTestUtil.getCertificateCredentials();
			return true;
		} catch (AssertionFailedError error) {
			return false;
		}
	}

	public static String getShortUserName(UserCredentials credentials) {
		String username = credentials.getUserName();
		if (username.contains("@")) {
			return username.substring(0, username.indexOf("@"));
		}
		return username;
	}

	/**
	 * Activates manual proxy configuration in the Ecipse proxy service if system proxy support is not available. This
	 * sets proxy configuration to Java system properties.
	 * <p>
	 * This work around is required on e3.5/gtk.x86_64 where system proxy settings get enabled but the proxy
	 * configuration is not actually detected resulting in a broken configuration.
	 * <p>
	 * Please note that this only works for http proxies. The https proxy system property is ignored.
	 * 
	 * @see #isHttpsProxyBroken()
	 */
	public static boolean fixProxyConfiguration() {
		if (Platform.isRunning() && CommonsNetPlugin.getProxyService() != null
				&& CommonsNetPlugin.getProxyService().isSystemProxiesEnabled()
				&& !CommonsNetPlugin.getProxyService().hasSystemProxies()) {
			System.err.println("Forcing manual proxy configuration");
			CommonsNetPlugin.getProxyService().setSystemProxiesEnabled(false);
			CommonsNetPlugin.getProxyService().setProxiesEnabled(true);
			return true;
		}
		return false;
	}

	public static void dumpSystemInfo(PrintStream out) {
		Properties p = System.getProperties();
		if (Platform.isRunning()) {
			p.put("build.system", Platform.getOS() + "-" + Platform.getOSArch() + "-" + Platform.getWS());
		} else {
			p.put("build.system", "standalone");
		}
		String info = "System: ${os.name} ${os.version} (${os.arch}) / ${build.system} / ${java.vendor} ${java.vm.name} ${java.version}";
		for (Entry<Object, Object> entry : p.entrySet()) {
			info = info.replaceFirst(Pattern.quote("${" + entry.getKey() + "}"), entry.getValue().toString());
		}
		out.println(info);
		out.print("HTTP Proxy : " + WebUtil.getProxyForUrl("http://mylyn.org") + " (Platform)");
		try {
			out.print(" / " + ProxySelector.getDefault().select(new URI("http://mylyn.org")) + " (Java)");
		} catch (URISyntaxException e) {
			// ignore
		}
		out.println();
		out.print("HTTPS Proxy : " + WebUtil.getProxyForUrl("https://mylyn.org") + " (Platform)");
		try {
			out.print(" / " + ProxySelector.getDefault().select(new URI("https://mylyn.org")) + " (Java)");
		} catch (URISyntaxException e) {
			// ignore
		}
		out.println();
		out.println();
	}

	public static boolean isEclipse4() {
		return Platform.getBundle("org.eclipse.e4.core.commands") != null;
	}

	/**
	 * If Eclipse proxy configuration is set to manual https proxies aren't detected and hence tests that rely on https
	 * connections may fail. Use this method to detect whether https is configured correctly.
	 * 
	 * @see #fixProxyConfiguration()
	 */
	public static boolean isHttpsProxyBroken() {
		// checks if http and https proxy configuration matches
		Proxy httpProxy = WebUtil.getProxyForUrl("http://mylyn.org");
		Proxy httpsProxy = WebUtil.getProxyForUrl("https://mylyn.org");
		return CoreUtil.areEqual(httpProxy, httpsProxy);
	}

	/**
	 * Returns whether to run on local services if present. Returns false, unless a system property has been set to
	 * force to ignore local running services.
	 */
	public static boolean ignoreLocalTestServices() {
		return Boolean.parseBoolean(System.getProperty(KEY_IGNORE_LOCAL_SERVICES));
	}

	public static boolean isBehindProxy() {
		return NetUtil.getProxyForUrl("https://mylyn.org/secure/index.txt") != null;
	}

}
