/*******************************************************************************
 * Copyright (c) 2004, 2014 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *	IBM Corporation - initial API and implementation
 *	Martin Oberhuber (Wind River) - [232426] createSymLink() method
 *	Martin Oberhuber (Wind River) - [335864] ResourceAttributeTest fails on Win7
 *	Sergey Prigogin (Google) - [440283] Modify symlink tests to run on Windows with or without administrator privileges
 *******************************************************************************/
package org.eclipse.core.tests.harness;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import junit.framework.AssertionFailedError;
import junit.framework.TestCase;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.ILog;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.junit.Assume;

/**
 * @since 3.1
 */
public class CoreTest extends TestCase {
	private static Boolean canCreateSymLinks;

	/** counter for generating unique random file system locations */
	protected static int nextLocationCounter = 0;

	// plug-in identified for the core.tests.harness plug-in.
	public static final String PI_HARNESS = "org.eclipse.core.tests.harness";

	public static void debug(String message) {
		String id = "org.eclipse.core.tests.harness/debug";
		String option = Platform.getDebugOption(id);
		if (Boolean.TRUE.toString().equalsIgnoreCase(option)) {
			System.out.println(message);
		}
	}

	/**
	 * Fails the test due to the given throwable.
	 */
	public static void fail(String message, Throwable e) {
		// If the exception is a CoreException with a multistatus
		// then print out the multistatus so we can see all the info.
		if (e instanceof CoreException) {
			IStatus status = ((CoreException) e).getStatus();
			//if the status does not have an exception, print the stack for this one
			if (status.getException() == null) {
				e.printStackTrace();
			}
			write(status, 0);
		} else {
			e.printStackTrace();
		}
		AssertionFailedError assertFail = new AssertionFailedError(message + ": " + e);
		assertFail.initCause(e);
		throw assertFail;
	}

	private static void indent(OutputStream output, int indent) {
		for (int i = 0; i < indent; i++) {
			try {
				output.write("\t".getBytes());
			} catch (IOException e) {
				// ignore
			}
		}
	}

	public static void log(String pluginID, IStatus status) {
		ILog.of(Platform.getBundle(pluginID)).log(status);
	}

	public static void log(String pluginID, Throwable e) {
		log(pluginID, new Status(IStatus.ERROR, pluginID, IStatus.ERROR, "Error", e)); //$NON-NLS-1$
	}

	private static void write(IStatus status, int indent) {
		PrintStream output = System.out;
		indent(output, indent);
		output.println("Severity: " + status.getSeverity());

		indent(output, indent);
		output.println("Plugin ID: " + status.getPlugin());

		indent(output, indent);
		output.println("Code: " + status.getCode());

		indent(output, indent);
		output.println("Message: " + status.getMessage());

		if (status.getException() != null) {
			indent(output, indent);
			output.print("Exception: ");
			status.getException().printStackTrace(output);
		}

		if (status.isMultiStatus()) {
			IStatus[] children = status.getChildren();
			for (IStatus element : children) {
				write(element, indent + 1);
			}
		}
	}

	public CoreTest() {
		super();
	}

	public CoreTest(String name) {
		super(name);
	}

	/**
	 * Asserts that a stream closes successfully. Null streams
	 * are ignored, but failure to close the stream is reported as
	 * an assertion failure.
	 * @since 3.2
	 */
	protected void assertClose(InputStream stream) {
		if (stream == null) {
			return;
		}
		try {
			stream.close();
		} catch (IOException e) {
			fail("Failed close in assertClose", e);
		}
	}

	/**
	 * Asserts that a stream closes successfully. Null streams
	 * are ignored, but failure to close the stream is reported as
	 * an assertion failure.
	 * @since 3.2
	 */
	protected void assertClose(OutputStream stream) {
		if (stream == null) {
			return;
		}
		try {
			stream.close();
		} catch (IOException e) {
			fail("Failed close in assertClose", e);
		}
	}

	protected void assertEquals(String message, Object[] expected, Object[] actual) {
		if (expected == null && actual == null) {
			return;
		}
		if (expected == null || actual == null) {
			fail(message);
		}
		if (expected.length != actual.length) {
			fail(message);
		}
		for (int i = 0; i < expected.length; i++) {
			assertEquals(message, expected[i], actual[i]);
		}
	}

	protected void assertEquals(String message, Object[] expected, Object[] actual, boolean orderImportant) {
		// if the order in the array must match exactly, then call the other method
		if (orderImportant) {
			assertEquals(message, expected, actual);
			return;
		}
		// otherwise use this method and check that the arrays are equal in any order
		if (expected == null && actual == null) {
			return;
		}
		if (expected == actual) {
			return;
		}
		if (expected == null || actual == null) {
			assertTrue(message + ".1", false);
		}
		if (expected.length != actual.length) {
			assertTrue(message + ".2", false);
		}
		boolean[] found = new boolean[expected.length];
		for (Object element : expected) {
			for (int j = 0; j < expected.length; j++) {
				if (!found[j] && element.equals(actual[j])) {
					found[j] = true;
				}
			}
		}
		for (int i = 0; i < found.length; i++) {
			if (!found[i]) {
				assertTrue(message + ".3." + i, false);
			}
		}
	}

	/**
	 * Create the given file in the file system.
	 */
	public void createFileInFileSystem(File file, InputStream contents) throws IOException {
		file.getParentFile().mkdirs();
		FileOutputStream output = new FileOutputStream(file);
		transferData(contents, output);
	}

	/**
	 * Creates a symbolic link.
	 * Should only be called on platforms where symbolic links can actually
	 * be created, i.e. an "ln" command is available.
	 * @param basedir folder in which the symbolic link should be created
	 * @param linkName name of the symbolic link
	 * @param linkTarget target to which the symbolic link should point
	 * @param isDir <code>true</code> if the link should point to a folder
	 * @throws AssertionFailedError if creation of the symbolic link failed
	 */
	protected void createSymLink(File basedir, String linkName, String linkTarget, boolean isDir) {
		// The following code creates even a link if
		// Files.createSymbolicLink(new File(basedir, linkName).toPath(), new
		// File(basedir, linkTarget).toPath());
		// would throw java.nio.file.FileSystemException "missing rights"
		//
		// Deliberately use an empty environment to make the test reproducible.
		String[] envp = {};
		try {
			Process p;
			if (Platform.getOS().equals(Platform.OS_WIN32)) {
				// use File.getPath to avoid 'Illegal argument - ".."' for using "../"
				// instead of "..\"
				if (isDir) {
					String[] cmd = { "cmd", "/c", "mklink", "/d", new File(linkName).getPath(),
							new File(linkTarget).getPath() };
					p = Runtime.getRuntime().exec(cmd, envp, basedir);
				} else {
					String[] cmd = { "cmd", "/c", "mklink", new File(linkName).getPath(),
							new File(linkTarget).getPath() };
					p = Runtime.getRuntime().exec(cmd, envp, basedir);
				}
			} else {
				String[] cmd = {"ln", "-s", linkTarget, linkName};
				p = Runtime.getRuntime().exec(cmd, envp, basedir);
			}
			int exitcode = p.waitFor();
			if (exitcode != 0) {
				// xxx wrong charset. from jdk17+ we could use Console.charset()
				String result = new BufferedReader(new InputStreamReader(p.getErrorStream())).readLine();
				assertEquals("createSymLink: " + result + ", exitcode", 0, exitcode);
			}
		} catch (IOException | InterruptedException e) {
			fail("createSymLink", e);
		}
	}

	protected void assumeCanCreateSymLinks() {
		Assume.assumeTrue("Can't create symbolic links in this platform: " + Platform.getOS(), canCreateSymLinks());
	}

	/**
	 * Checks whether it is possible for a test to create a symbolic link.
	 *
	 * @return <code>true</code> if symbolic links can be created by a test
	 */
	protected boolean canCreateSymLinks() {
		if (canCreateSymLinks == null) {
			if (Platform.getOS().equals(Platform.OS_WIN32)) {
				// Creation of a symbolic link on Windows requires administrator privileges,
				// so it may or may not be possible.
				IPath tempDir = getTempDir();
				String linkName = FileSystemHelper.getRandomLocation(tempDir).lastSegment();
				try {
					// Try to create a symlink.
					createSymLink(tempDir.toFile(), linkName, "testTarget", false);
					// Clean up if the link was created.
					new File(tempDir.toFile(), linkName).delete();
					canCreateSymLinks = Boolean.TRUE;
				} catch (AssertionFailedError e) {
					// This exception indicates that creation of the symlink failed.
					canCreateSymLinks = Boolean.FALSE;
				}
			} else {
				canCreateSymLinks = Boolean.TRUE;
			}
		}
		return canCreateSymLinks.booleanValue();
	}

	protected void ensureDoesNotExistInFileSystem(java.io.File file) {
		FileSystemHelper.clear(file);
	}

	public InputStream getContents(java.io.File target, String errorCode) {
		try {
			return new FileInputStream(target);
		} catch (IOException e) {
			fail(errorCode, e);
		}
		return null; // never happens
	}

	/**
	 * Return an input stream with some the specified text to use
	 * as contents for a file resource.
	 */
	public InputStream getContents(String text) {
		return new ByteArrayInputStream(text.getBytes());
	}

	public IProgressMonitor getMonitor() {
		return new FussyProgressMonitor();
	}

	/**
	 * Return an input stream with some random text to use
	 * as contents for a file resource.
	 */
	public InputStream getRandomContents() {
		return new ByteArrayInputStream(getRandomString().getBytes());
	}

	/**
	 * Returns a unique location on disk.  It is guaranteed that no file currently
	 * exists at that location.  The returned location will be unique with respect
	 * to all other locations generated by this method in the current session.
	 * If the caller creates a folder or file at this location, they are responsible for
	 * deleting it when finished.
	 */
	public IPath getRandomLocation() {
		return FileSystemHelper.getRandomLocation(getTempDir());
	}

	/**
	 * Return String with some random text to use
	 * as contents for a file resource.
	 */
	public String getRandomString() {
		switch ((int) Math.round(Math.random() * 10)) {
			case 0 :
				return "este e' o meu conteudo (portuguese)";
			case 1 :
				return "ho ho ho";
			case 2 :
				return "I'll be back";
			case 3 :
				return "don't worry, be happy";
			case 4 :
				return "there is no imagination for more sentences";
			case 5 :
				return "Alexandre Bilodeau, Canada's first home gold. 14/02/2010";
			case 6 :
				return "foo";
			case 7 :
				return "bar";
			case 8 :
				return "foobar";
			case 9 :
				return "case 9";
			default :
				return "these are my contents";
		}
	}

	public IPath getTempDir() {
		return FileSystemHelper.getTempDir();
	}

	public String getUniqueString() {
		return System.currentTimeMillis() + "-" + Math.random();
	}

	/**
	 * Copy the data from the input stream to the output stream.
	 * Close both streams when finished.
	 */
	public void transferData(InputStream input, OutputStream output) {
		try {
			try {
				int c = 0;
				while ((c = input.read()) != -1) {
					output.write(c);
				}
			} finally {
				input.close();
				output.close();
			}
		} catch (IOException e) {
			e.printStackTrace();
			assertTrue(e.toString(), false);
		}
	}

	/**
	 * Copy the data from the input stream to the output stream.
	 * Do not close either of the streams.
	 */
	public void transferDataWithoutClose(InputStream input, OutputStream output) {
		try {
			int c = 0;
			while ((c = input.read()) != -1) {
				output.write(c);
			}
		} catch (IOException e) {
			e.printStackTrace();
			assertTrue(e.toString(), false);
		}
	}

	public static void assertSame(String message, int expected, int actual) {
		if (expected == actual) {
			return;
		}
		failNotSame(message, expected, actual);
	}

	public static void failNotSame(String message, int expected, int actual) {
		StringBuilder formatted = new StringBuilder();
		if (message != null) {
			formatted.append(message).append(' ');
		}
		formatted.append("expected same:<").append(expected).append("> was not:<").append(actual).append(">");
		fail(String.valueOf(formatted));
	}

	public static void assertSame(String message, boolean expected, boolean actual) {
		if (expected == actual) {
			return;
		}
		failNotSame(message, expected, actual);
	}

	public static void failNotSame(String message, boolean expected, boolean actual) {
		StringBuilder formatted = new StringBuilder();
		if (message != null) {
			formatted.append(message).append(' ');
		}
		formatted.append("expected same:<").append(expected).append("> was not:<").append(actual).append(">");
		fail(String.valueOf(formatted));
	}

	public static void assertSame(String message, float expected, float actual) {
		if (expected == actual) {
			return;
		}
		failNotSame(message, expected, actual);
	}

	public static void failNotSame(String message, float expected, float actual) {
		StringBuilder formatted = new StringBuilder();
		if (message != null) {
			formatted.append(message).append(' ');
		}
		formatted.append("expected same:<").append(expected).append("> was not:<").append(actual).append(">");
		fail(String.valueOf(formatted));
	}

	public static void assertSame(String message, double expected, double actual) {
		if (expected == actual) {
			return;
		}
		failNotSame(message, expected, actual);
	}

	public static void failNotSame(String message, double expected, double actual) {
		StringBuilder formatted = new StringBuilder();
		if (message != null) {
			formatted.append(message).append(' ');
		}
		formatted.append("expected same:<").append(expected).append("> was not:<").append(actual).append(">");
		fail(String.valueOf(formatted));
	}

	public static void assertSame(String message, long expected, long actual) {
		if (expected == actual) {
			return;
		}
		failNotSame(message, expected, actual);
	}

	public static void failNotSame(String message, long expected, long actual) {
		StringBuilder formatted = new StringBuilder();
		if (message != null) {
			formatted.append(message).append(' ');
		}
		formatted.append("expected same:<").append(expected).append("> was not:<").append(actual).append(">");
		fail(String.valueOf(formatted));
	}
}
