/*******************************************************************************
 * Copyright (c) 2018, 2020 Cedric Chabanois 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:
 *     Cedric Chabanois (cchabanois@gmail.com) - initial implementation
 *******************************************************************************/
package org.eclipse.jdt.debug.tests.launching;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertNotEquals;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.Attributes;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import java.util.stream.Collectors;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.URIUtil;
import org.eclipse.jdt.debug.tests.AbstractDebugTest;
import org.eclipse.jdt.debug.tests.connectors.MockLaunch;
import org.eclipse.jdt.internal.launching.ClasspathShortener;

public class ClasspathShortenerTests extends AbstractDebugTest {
	private static final String MAIN_CLASS = "my.package.MainClass";
	private static final String ENCODING_ARG = "-Dfile.encoding=UTF-8";
	private static final String JAVA_10_PATH = "/usr/lib/jvm/java-10-jdk-amd64/bin/java";
	private static final String JAVA_8_PATH = "/usr/lib/jvm/java-8-openjdk-amd64/bin/java";
	private ClasspathShortenerForTest classpathShortener;
	private String userHome;

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

	@Override
	protected void setUp() throws Exception {
		super.setUp();
		userHome = System.getProperty("user.home");
	}

	@Override
	protected void tearDown() throws Exception {
		if (classpathShortener != null) {
			classpathShortener.getProcessTempFiles().forEach(file -> file.delete());
		}
		super.tearDown();
	}

	public void testNoNeedToShortenShortClasspath() {
		// Given
		String classpath = getClasspathOrModulePath(userHomePath("/workspace/myProject/bin"), userHomePath("/workspace/myProject/lib/lib 1.jar"));
		String[] cmdLine = new String[] { JAVA_10_PATH, ENCODING_ARG, "-classpath", classpath, MAIN_CLASS, "-arg1", "arg2" };
		classpathShortener = new ClasspathShortenerForTest(Platform.OS_LINUX, "10.0.1", cmdLine, 4, null);

		// When
		boolean result = classpathShortener.shortenCommandLineIfNecessary();

		// Then
		assertFalse(result);
	}

	public void testShortenClasspathWhenCommandLineIsTooLong() {
		// Given
		String classpath = getClasspathOrModulePath(userHomePath("/workspace/myProject/bin"), userHomePath("/workspace/myProject/lib/lib 1.jar"));
		String[] cmdLine = new String[] { JAVA_10_PATH, ENCODING_ARG, "-classpath", classpath, MAIN_CLASS, "-arg1", "arg2" };
		classpathShortener = new ClasspathShortenerForTest(Platform.OS_LINUX, "10.0.1", cmdLine, 4, null);

		// When
		classpathShortener.setMaxCommandLineLength(100);
		boolean result = classpathShortener.shortenCommandLineIfNecessary();

		// Then
		assertTrue(result);
	}

	public void testShortenClasspathWhenClasspathArgumentIsTooLong() {
		// Given
		String classpath = getClasspathOrModulePath(userHomePath("/workspace/myProject/bin"), userHomePath("/workspace/myProject/lib/lib 1.jar"));
		String[] cmdLine = new String[] { JAVA_10_PATH, ENCODING_ARG, "-classpath", classpath, MAIN_CLASS, "-arg1", "arg2" };
		classpathShortener = new ClasspathShortenerForTest(Platform.OS_LINUX, "10.0.1", cmdLine, 4, null);

		// When
		classpathShortener.setMaxArgLength(40);
		boolean result = classpathShortener.shortenCommandLineIfNecessary();

		// Then
		assertTrue(result);
	}

	public void testForceClasspathOnlyJar() throws Exception {
		// Given
		String classpath = getClasspathOrModulePath(userHomePath("/workspace/myProject/bin"), userHomePath("/workspace/myProject/lib/lib 1.jar"));
		String[] cmdLine = new String[] { JAVA_10_PATH, ENCODING_ARG, "-classpath", classpath, MAIN_CLASS, "-arg1", "arg2" };
		classpathShortener = new ClasspathShortenerForTest(Platform.OS_LINUX, "10.0.1", cmdLine, 4, null);

		// When
		classpathShortener.setForceUseClasspathOnlyJar(true);
		boolean result = classpathShortener.shortenCommandLineIfNecessary();

		// Then
		assertTrue(result);
		assertEquals(1, classpathShortener.getProcessTempFiles().size());
		assertArrayEquals(new String[] { JAVA_10_PATH, ENCODING_ARG, "-classpath", classpathShortener.getProcessTempFiles().get(0).getAbsolutePath(),
				MAIN_CLASS, "-arg1", "arg2" }, classpathShortener.getCmdLine());
		List<File> classpathJars = getClasspathJarsFromJarManifest(classpathShortener.getProcessTempFiles().get(0));
		String filePathSuffix = new File(userHomePath("/workspace/myProject/bin")).getPath();
		int index = classpathJars.get(0).getCanonicalFile().getPath().lastIndexOf(filePathSuffix);
		assertNotEquals("First Classpath jar file location not found", -1, index);
		filePathSuffix = new File(userHomePath("/workspace/myProject/lib/lib 1.jar")).getPath();
		index = classpathJars.get(1).getCanonicalFile().getPath().lastIndexOf(filePathSuffix);
		assertNotEquals("Second Classpath jar file location not found", -1, index);
	}

	public void testArgFileUsedForLongClasspathOnJava9() throws Exception {
		// Given
		String classpath = getClasspathOrModulePath(userHomePath("/workspace/myProject/bin"), userHomePath("/workspace/myProject/lib/lib 1.jar"));
		String[] cmdLine = new String[] { JAVA_10_PATH, ENCODING_ARG, "-cp", classpath, MAIN_CLASS, "-arg1", "arg2" };
		classpathShortener = new ClasspathShortenerForTest(Platform.OS_LINUX, "10.0.1", cmdLine, 4, null);
		classpathShortener.setMaxCommandLineLength(100);

		// When
		boolean result = classpathShortener.shortenCommandLineIfNecessary();

		// Then
		assertTrue(result);
		assertEquals(1, classpathShortener.getProcessTempFiles().size());
		assertArrayEquals(new String[] { JAVA_10_PATH, ENCODING_ARG, "@" + classpathShortener.getProcessTempFiles().get(0).getAbsolutePath(),
				MAIN_CLASS, "-arg1", "arg2" }, classpathShortener.getCmdLine());
		assertEquals("-classpath "
				+ classpathShortener.quoteWindowsPath(classpath), getFileContents(classpathShortener.getProcessTempFiles().get(0)));
	}

	public void testArgFileUsedForLongModulePath() throws Exception {
		// Given
		String modulepath = getClasspathOrModulePath(userHomePath("/workspace/myProject/bin"), userHomePath("/workspace/myProject/lib/lib 1.jar"));
		String[] cmdLine = new String[] { JAVA_10_PATH, ENCODING_ARG, "-p", modulepath, MAIN_CLASS, "-arg1", "arg2" };
		classpathShortener = new ClasspathShortenerForTest(Platform.OS_WIN32, "10.0.1", cmdLine, 4, null);
		classpathShortener.setMaxCommandLineLength(100);

		// When
		boolean result = classpathShortener.shortenCommandLineIfNecessary();

		// Then
		assertTrue(result);
		assertEquals(1, classpathShortener.getProcessTempFiles().size());
		assertArrayEquals(new String[] { JAVA_10_PATH, ENCODING_ARG, "@" + classpathShortener.getProcessTempFiles().get(0).getAbsolutePath(),
				MAIN_CLASS, "-arg1", "arg2" }, classpathShortener.getCmdLine());
		assertEquals("--module-path "
				+ classpathShortener.quoteWindowsPath(modulepath), getFileContents(classpathShortener.getProcessTempFiles().get(0)));
	}

	public void testLongClasspathAndLongModulePath() throws Exception {
		// Given
		String classpath = getClasspathOrModulePath(userHomePath("/workspace/myProject/lib/lib 2.jar"), userHomePath("/workspace/myProject/lib/lib 3.jar"));
		String modulepath = getClasspathOrModulePath(userHomePath("/workspace/myProject/bin"), userHomePath("/workspace/myProject/lib/lib 1.jar"));
		String[] cmdLine = new String[] { JAVA_10_PATH, ENCODING_ARG, "-cp", classpath, "-p", modulepath, MAIN_CLASS, "-arg1", "arg2" };
		classpathShortener = new ClasspathShortenerForTest(Platform.OS_WIN32, "10.0.1", cmdLine, 6, null);
		classpathShortener.setMaxCommandLineLength(100);

		// When
		boolean result = classpathShortener.shortenCommandLineIfNecessary();

		// Then
		assertTrue(result);
		assertEquals(2, classpathShortener.getProcessTempFiles().size());
		assertArrayEquals(new String[] { JAVA_10_PATH, ENCODING_ARG, "@" + classpathShortener.getProcessTempFiles().get(0).getAbsolutePath(),
				"@" + classpathShortener.getProcessTempFiles().get(1).getAbsolutePath(), MAIN_CLASS, "-arg1",
				"arg2" }, classpathShortener.getCmdLine());
		assertEquals("-classpath "
				+ classpathShortener.quoteWindowsPath(classpath), getFileContents(classpathShortener.getProcessTempFiles().get(0)));
		assertEquals("--module-path "
				+ classpathShortener.quoteWindowsPath(modulepath), getFileContents(classpathShortener.getProcessTempFiles().get(1)));
	}

	public void testClasspathOnlyJarUsedForLongClasspathOnJava8() throws Exception {
		// Given
		String classpath = getClasspathOrModulePath(userHomePath("/workspace/myProject/bin"), userHomePath("/workspace/myProject/lib/lib 1.jar"));
		String[] cmdLine = new String[] { JAVA_8_PATH, ENCODING_ARG, "-cp", classpath, MAIN_CLASS, "-arg1", "arg2" };
		classpathShortener = new ClasspathShortenerForTest(Platform.OS_LINUX, "1.8.0_171", cmdLine, 4, null);
		classpathShortener.setMaxCommandLineLength(100);

		// When
		classpathShortener.setAllowToUseClasspathOnlyJar(true);
		boolean result = classpathShortener.shortenCommandLineIfNecessary();

		// Then
		assertTrue(result);
		assertEquals(1, classpathShortener.getProcessTempFiles().size());
		assertArrayEquals(new String[] { JAVA_8_PATH, ENCODING_ARG, "-cp", classpathShortener.getProcessTempFiles().get(0).getAbsolutePath(),
				MAIN_CLASS, "-arg1", "arg2" }, classpathShortener.getCmdLine());
		List<File> classpathJars = getClasspathJarsFromJarManifest(classpathShortener.getProcessTempFiles().get(0));
		String filePathSuffix = new File(userHomePath("/workspace/myProject/bin")).getPath();
		int index = classpathJars.get(0).getCanonicalFile().getPath().lastIndexOf(filePathSuffix);
		assertNotEquals("First Classpath jar file location not found", -1, index);
		filePathSuffix = new File(userHomePath("/workspace/myProject/lib/lib 1.jar")).getPath();
		index = classpathJars.get(1).getCanonicalFile().getPath().lastIndexOf(filePathSuffix);
		assertNotEquals("Second Classpath jar file location not found", -1, index);
	}

	public void testClasspathEnvVariableUsedForLongClasspathOnJava8OnWindows() {
		// Given
		String classpath = getClasspathOrModulePath(userHomePath("/workspace/myProject/bin"), userHomePath("/workspace/myProject/lib/lib 1.jar"));
		String[] cmdLine = new String[] { JAVA_8_PATH, ENCODING_ARG, "-cp", classpath, MAIN_CLASS, "-arg1", "arg2" };
		classpathShortener = new ClasspathShortenerForTest(Platform.OS_WIN32, "1.8.0_171", cmdLine, 4, null);
		classpathShortener.setMaxCommandLineLength(100);

		// When
		Map<String, String> nativeEnvironment = new LinkedHashMap<>();
		nativeEnvironment.put("PATH", "C:\\WINDOWS\\System32;C:\\WINDOWS");
		classpathShortener.setNativeEnvironment(nativeEnvironment);
		boolean result = classpathShortener.shortenCommandLineIfNecessary();

		// Then
		assertTrue(result);
		assertEquals(0, classpathShortener.getProcessTempFiles().size());
		assertArrayEquals(new String[] { "PATH=C:\\WINDOWS\\System32;C:\\WINDOWS",
				"CLASSPATH=" + classpath }, classpathShortener.getEnvp());
		assertArrayEquals(new String[] { JAVA_8_PATH, ENCODING_ARG, MAIN_CLASS, "-arg1", "arg2" }, classpathShortener.getCmdLine());
	}

	public void testClasspathUsedOnWindowsWhenEnvp() {
		// Given
		String classpath = getClasspathOrModulePath(userHomePath("/workspace/myProject/bin"), userHomePath("/workspace/myProject/lib/lib 1.jar"));
		String[] cmdLine = new String[] { JAVA_8_PATH, ENCODING_ARG, "-cp", classpath, MAIN_CLASS, "-arg1", "arg2" };
		String[] envp = new String[] { "MYVAR1=value1", "MYVAR2=value2" };
		classpathShortener = new ClasspathShortenerForTest(Platform.OS_WIN32, "1.8.0_171", cmdLine, 4, envp);
		classpathShortener.setMaxCommandLineLength(100);

		// When
		boolean result = classpathShortener.shortenCommandLineIfNecessary();

		// Then
		assertTrue(result);
		assertEquals(0, classpathShortener.getProcessTempFiles().size());
		assertArrayEquals(new String[] { "MYVAR1=value1", "MYVAR2=value2",
				"CLASSPATH=" + classpath }, classpathShortener.getEnvp());
		assertArrayEquals(new String[] { JAVA_8_PATH, ENCODING_ARG, MAIN_CLASS, "-arg1", "arg2" }, classpathShortener.getCmdLine());
	}

	public void testClasspathInEnvironmentReplacedOnWindows() {
		// Given
		String classpath = getClasspathOrModulePath(userHomePath("/workspace/myProject/bin"), userHomePath("/workspace/myProject/lib/lib 1.jar"));
		String[] cmdLine = new String[] { JAVA_8_PATH, ENCODING_ARG, "-cp", classpath, MAIN_CLASS, "-arg1", "arg2" };
		classpathShortener = new ClasspathShortenerForTest(Platform.OS_WIN32, "1.8.0_171", cmdLine, 4, null);
		classpathShortener.setMaxCommandLineLength(100);

		// When
		Map<String, String> nativeEnvironment = new LinkedHashMap<>();
		nativeEnvironment.put("PATH", "C:\\WINDOWS\\System32;C:\\WINDOWS");
		nativeEnvironment.put("CLASSPATH", "C:\\myJars\\jar1.jar");
		classpathShortener.setNativeEnvironment(nativeEnvironment);
		boolean result = classpathShortener.shortenCommandLineIfNecessary();

		// Then
		assertTrue(result);
		assertEquals(0, classpathShortener.getProcessTempFiles().size());
		assertArrayEquals(new String[] { "PATH=C:\\WINDOWS\\System32;C:\\WINDOWS",
				"CLASSPATH=" + classpath }, classpathShortener.getEnvp());
		assertArrayEquals(new String[] { JAVA_8_PATH, ENCODING_ARG, MAIN_CLASS, "-arg1", "arg2" }, classpathShortener.getCmdLine());
	}

	private String getFileContents(File file) throws UnsupportedEncodingException, IOException {
		return new String(Files.readAllBytes(file.toPath()), "UTF-8");
	}

	private String getClasspathAttributeFromJarManifest(File jarFile) throws FileNotFoundException, IOException {
		try (JarInputStream jarStream = new JarInputStream(new FileInputStream(jarFile))) {
			Manifest mf = jarStream.getManifest();
			return mf.getMainAttributes().getValue(Attributes.Name.CLASS_PATH);
		}
	}

	private List<File> getClasspathJarsFromJarManifest(File jarFile) throws FileNotFoundException, IOException {
		File parentDir = jarFile.getParentFile();
		String classpath = getClasspathAttributeFromJarManifest(jarFile);
		return Arrays.stream(classpath.split(" ")).map(relativePath -> {
			try {
				return new File(URIUtil.makeAbsolute(new URI(relativePath), parentDir.toURI()));
			} catch (URISyntaxException e) {
				throw new RuntimeException(e);
			}
		}).collect(Collectors.toList());
	}

	private String userHomePath(String path) {
		return userHome + path;
	}

	private String getClasspathOrModulePath(String... classpathElements) {
		return String.join(";", classpathElements);
	}

	private static class ClasspathShortenerForTest extends ClasspathShortener {
		private boolean forceUseClasspathOnlyJar = false;
		private boolean allowToUseClasspathOnlyJar = false;
		private int maxArgLength = Integer.MAX_VALUE;
		private int maxCommandLineLength = Integer.MAX_VALUE;
		private Map<String, String> nativeEnvironment = new HashMap<>();

		public ClasspathShortenerForTest(String os, String javaVersion, String[] cmdLine, int classpathArgumentIndex, String[] envp) {
			super(os, javaVersion, new MockLaunch(), cmdLine, classpathArgumentIndex, null, envp);
		}

		public void setAllowToUseClasspathOnlyJar(boolean allowToUseClasspathOnlyJar) {
			this.allowToUseClasspathOnlyJar = allowToUseClasspathOnlyJar;
		}

		public void setForceUseClasspathOnlyJar(boolean forceUseClasspathOnlyJar) {
			this.forceUseClasspathOnlyJar = forceUseClasspathOnlyJar;
		}

		@Override
		protected String getLaunchConfigurationName() {
			return "launch";
		}

		@Override
		protected String getLaunchTimeStamp() {
			return Long.toString(System.currentTimeMillis());
		}

		@Override
		protected boolean handleClasspathTooLongStatus() throws CoreException {
			return allowToUseClasspathOnlyJar;
		}

		@Override
		protected boolean getLaunchConfigurationUseClasspathOnlyJarAttribute() throws CoreException {
			return forceUseClasspathOnlyJar;
		}

		@Override
		protected int getMaxArgLength() {
			return maxArgLength;
		}

		@Override
		protected int getMaxCommandLineLength() {
			return maxCommandLineLength;
		}

		public void setMaxArgLength(int maxArgLength) {
			this.maxArgLength = maxArgLength;
		}

		public void setMaxCommandLineLength(int maxCommandLineLength) {
			this.maxCommandLineLength = maxCommandLineLength;
		}

		public void setNativeEnvironment(Map<String, String> nativeEnvironment) {
			this.nativeEnvironment = nativeEnvironment;
		}

		@Override
		protected Map<String, String> getNativeEnvironment() {
			return nativeEnvironment;
		}

		@Override
		protected char getPathSeparatorChar() {
			// always use ';' as separator (tests would fail on windows otherwise with paths c:\ ...)
			return ';';
		}

	}

}
