/*******************************************************************************
 *  Copyright (c) 2000, 2015 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
 *******************************************************************************/
package org.eclipse.jdt.debug.tests.core;

import static java.util.stream.Collectors.joining;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchConfigurationType;
import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
import org.eclipse.debug.core.ILaunchManager;
import org.eclipse.debug.core.model.IProcess;
import org.eclipse.debug.ui.console.IConsole;
import org.eclipse.debug.ui.console.IConsoleLineTrackerExtension;
import org.eclipse.jdt.debug.testplugin.ConsoleLineTracker;
import org.eclipse.jdt.debug.tests.AbstractDebugTest;
import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
import org.eclipse.jdt.launching.IVMInstall;
import org.eclipse.jdt.launching.JavaRuntime;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;

/**
 * Tests for program and VM arguments
 */
public class ArgumentTests extends AbstractDebugTest {

    private Object fLock = new Object();
	protected boolean fUseArgfile = false;

	private class ConsoleArgumentOutputRetriever implements IConsoleLineTrackerExtension {

		StringBuilder buffer;
		IDocument document;
		boolean closed = false;

		/**
		 * @see org.eclipse.debug.ui.console.IConsoleLineTracker#dispose()
		 */
		@Override
		public void dispose() {}

		/**
		 * @see org.eclipse.debug.ui.console.IConsoleLineTracker#init(org.eclipse.debug.ui.console.IConsole)
		 */
		@Override
		public void init(IConsole console) {
			buffer = new StringBuilder();
			document = console.getDocument();
		}

		/**
		 * @see org.eclipse.debug.ui.console.IConsoleLineTracker#lineAppended(org.eclipse.jface.text.IRegion)
		 */
		@Override
		public void lineAppended(IRegion line) {
			try {
                assertNotNull("received notification of invalid line", line);
                assertNotNull("buffer is null", buffer);
                String text = document.get(line.getOffset(), line.getLength());
				if (!JavaOutputHelpers.isKnownExtraneousOutput(text)) {
					buffer.append(text);
				}
			} catch (BadLocationException e) {
				e.printStackTrace();
			}
		}

		/**
		 * @see org.eclipse.debug.ui.console.IConsoleLineTrackerExtension#consoleClosed()
		 */
		@Override
		public void consoleClosed() {
			synchronized (fLock) {
				closed = true;
			    fLock.notifyAll();
            }
		}

		/**
		 * @return the output
		 */
		public String getOutput() {
			// wait to be closed
		    synchronized (fLock) {
		    	if (!closed) {
			        try {
	                    fLock.wait(DEFAULT_TIMEOUT);
	                } catch (InterruptedException e) {
	                }
		    	}
		    }
		    if (!closed) {
				// output contents to console in case of error
				if (buffer != null) {
				    System.out.println();
				    System.out.println(ArgumentTests.this.getName());
				    System.out.println("\treceived " + buffer.length() + " chars: " + buffer.toString());
				}
		    }
		    assertNotNull("Line tracker did not receive init notification", buffer);
		    assertTrue("Line tracker did not receive close notification", closed);
			return buffer.toString();
		}

}

	/**
	 * Constructor
	 * @param name the name of the test
	 */
	public ArgumentTests(String name) {
		super(name);
	}

	/**
	 * Creates and returns a new launch config the given name
	 */
	protected ILaunchConfigurationWorkingCopy newConfiguration(IContainer container, String name) throws CoreException {
		ILaunchConfigurationType type = getLaunchManager().getLaunchConfigurationType(IJavaLaunchConfigurationConstants.ID_JAVA_APPLICATION);
		return type.newInstance(container, name);
	}

	/*
	 * VM argument tests
	 */
	/**
	 * Test a single VM argument.
	 * Program output should be: foo
	 * @throws CoreException
	 */
	public void testVMArgSingle() throws CoreException {
		testWithVMArg("-Dfoo=foo", "foo");
	}
	/**
	 * Test a VM argument with quotes in a valid location.
	 * Program output should be: foo
	 * @throws CoreException
	 */
	public void testVMArgSimpleQuotes() throws CoreException {
		testWithVMArg("-Dfoo=\"foo\"", "foo");
	}
	/**
	 * Test a VM argument with the standard style quoting for arguments with
	 * spaces.
	 * Program output should be: foo bar
	 * @throws CoreException
	 */
	public void testVMArgStandardQuotes() throws CoreException {
		testWithVMArg("-Dfoo=\"foo bar\"", "foo bar");
	}
	/**
	 * Test a VM argument with quotes in a standard location.
	 * Program output should be: "foo"
	 * @throws CoreException
	 */
	public void testVMArgStandardEmbeddedQuotes() throws CoreException {
		testWithVMArg("-Dfoo=\"\\\"foo\\\"\"", "\"foo\"");
	}
	/**
	 * Test a VM argument with the quoting style we recommended as a workaround
	 * to a bug (now fixed) that we suggested in the past.
	 * Program output should be: foo bar
	 * @throws CoreException
	 */
	public void testVMArgWorkaroundQuotes() throws CoreException {
		testWithVMArg("\"-Dfoo=foo bar\"", "foo bar");
	}
	/**
	 * Test a VM argument with quotes placed in a creative (non-standard, but
	 * valid) location
	 * Program output should be: foo bar
	 * @throws CoreException
	 */
	public void testVMArgCreativeQuotes() throws CoreException {
		testWithVMArg("-Dfoo=fo\"o b\"ar", "foo bar");
	}
	/**
	 * Test a VM argument with embedded quotes.
	 * Program output should be: "foo bar"
	 * @throws CoreException
	 */
	public void testVMArgEmbeddedQuotes() throws CoreException {
		testWithVMArg("-Dfoo=\"\\\"foo bar\\\"\"", "\"foo bar\"");
	}
	/**
	 * Test a VM argument with quotes placed in a creative (non-standard, but
	 * valid) location
	 * Program output should be: fo"o b"ar
	 * @throws CoreException
	 */
	public void testVMArgEmbeddedCreativeQuotes() throws CoreException {
		testWithVMArg("-Dfoo=fo\"\\\"o b\\\"\"ar", "fo\"o b\"ar");
	}

	/*
	 * Program argument tests
	 */
	/**
	 * Test a single program argument.
	 * Program output should be: foo
	 * @throws CoreException
	 */
	public void testProgramArgSingle() throws CoreException {
		testWithProgramArg("foo", "foo");
	}
	/**
	 * Test multiple program arguments.
	 * Program output should be: foo\nbar
	 * @throws CoreException
	 */
	public void testProgramArgMultiple() throws CoreException {
		testWithProgramArg("foo bar", "foobar");
	}
	/**
	 * Test a program argument with quotes in a valid location.
	 * Program output should be: foo
	 * @throws CoreException
	 */
	public void testProgramArgSimpleQuotes() throws CoreException {
		testWithProgramArg("\"foo\"", "foo");
	}
	/**
	 * Test a program argument with quotes in a standard location.
	 * Program output should be: foo bar
	 * @throws CoreException
	 */
	public void testProgramArgStandardQuotes() throws CoreException {
		testWithProgramArg("\"foo bar\"", "foo bar");
	}
	/**
	 * Test a program argument with quotes placed in a creative (non-standard,
	 * but valid) location.
	 * Program output should be: foo bar
	 * @throws CoreException
	 */
	public void testProgramArgCreativeQuotes() throws CoreException {
		testWithProgramArg("fo\"o b\"ar", "foo bar");
	}
	/**
	 * Test a program argument with embedded quotes in a standard location.
	 * Program output should be: "blah"
	 * @throws CoreException
	 */
	public void testProgramArgEmbeddedQuotes() throws CoreException {
		testWithProgramArg("\\\"blah\\\"", "\"blah\"");
	}
	/**
	 * Test a program argument with embedded quotes in a creative (non-standard,
	 * but valie) location.
	 * Program output should be: f"o"o
	 * @throws CoreException
	 */
	public void testProgramArgCreativeEmbeddedQuotes() throws CoreException {
		testWithProgramArg("f\\\"o\\\"o", "f\"o\"o");
	}

	/**
	 * Test a program argument with one empty string
     *
	 * Program output should be: 1
	 * @throws CoreException
	 */
	public void testProgramArgEmptyString() throws CoreException {
		testProgramArgCount("\"\"", "1");
		// assert that it's really the empty string:
		testWithProgramArg("\"\"", "");
	}

	/**
	 * Test a program with an empty string among other args.
	 *
	 * Program output should be: 4
	 * @throws CoreException
	 */
	public void testProgramArgEmptyStringWithOthers() throws CoreException {
		testProgramArgCount("word1 \"\" \"part1 part2\" word2", "4");
	}

	/**
	 * Test a program argument with one double quote. We should pass in the
	 * empty string to match Java console behavior.
	 *
	 * Program output should be: 1
	 * @throws CoreException
	 */
	public void testProgramArgOneQuote() throws CoreException {
		testProgramArgCount("\"", "1");
	}

	/**
	 * Runs the FooPropertyPrinter with the given VM arguments and checks for
	 * the given output.
	 * @param argString the VM arguments
	 * @param argValue the expected output
	 */
	private void testWithVMArg(String argString, String outputValue) throws CoreException {
		testOutput("FooPropertyPrinter", argString, null, outputValue);
	}

	/**
	 * Runs the ArgumentPrinter with the given program arguments
	 * @param argString
	 * @param outputValue
	 * @throws CoreException
	 */
	private void testWithProgramArg(String argString, String outputValue) throws CoreException {
		testOutput("ArgumentPrinter", null, argString, outputValue);
	}

	/**
	 * Runs the ArgumentCounter with the given program arguments
	 * @param argString
	 * @param outputValue
	 * @throws CoreException
	 */
	private void testProgramArgCount(String argString, String outputValue) throws CoreException {
		testOutput("ArgumentCounter", null, argString, outputValue);
	}

	/**
	 * Runs the given program with the given VM arguments and the given program arguments and
	 * asserts that the output matches the given output.
	 * @param mainTypeName the type to execute
	 * @param vmArgs the VM arguments to specify
	 * @param programArgs the program arguments to specify
	 * @param outputValue the expected output
	 */
	private void testOutput(String mainTypeName, String vmArgs, String programArgs, String outputValue) throws CoreException {
		ILaunchConfigurationWorkingCopy workingCopy = newConfiguration(null, "config1");

		workingCopy.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, get14Project().getProject().getName());
		workingCopy.setAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, mainTypeName);
		workingCopy.setAttribute(IJavaLaunchConfigurationConstants.ATTR_STOP_IN_MAIN, true);
		workingCopy.setAttribute(IJavaLaunchConfigurationConstants.ATTR_VM_ARGUMENTS, vmArgs);
		workingCopy.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROGRAM_ARGUMENTS, programArgs);
		workingCopy.setAttribute(IJavaLaunchConfigurationConstants.ATTR_USE_ARGFILE, fUseArgfile);

		Map<String, String> env = getLaunchManager().getNativeEnvironment().entrySet().stream().filter(e -> !"JAVA_TOOL_OPTIONS".equals(e.getKey())).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
		workingCopy.setAttribute(ILaunchManager.ATTR_APPEND_ENVIRONMENT_VARIABLES, false);
		workingCopy.setAttribute(ILaunchManager.ATTR_ENVIRONMENT_VARIABLES, env);

		IVMInstall vm = JavaRuntime.getVMInstall(get14Project());
		assertNotNull("should be able to get a VM install from the 1.4 project", vm);
		if (fUseArgfile) {
			workingCopy.setAttribute(IJavaLaunchConfigurationConstants.ATTR_JRE_CONTAINER_PATH, JavaRuntime.newJREContainerPath(JavaRuntime.getExecutionEnvironmentsManager().getEnvironment("JavaSE-9")).toString());
		}
		//workingCopy.setAttribute(IJavaLaunchConfigurationConstants.ATTR_JRE_CONTAINER_PATH, JavaRuntime.newJREContainerPath(vm).toPortableString());

		// use 'java' instead of 'javaw' to launch tests (javaw is problematic on JDK1.4.2)
		Map<String, String> map = new HashMap<>(1);
		map.put(IJavaLaunchConfigurationConstants.ATTR_JAVA_COMMAND, "java");
		workingCopy.setAttribute(IJavaLaunchConfigurationConstants.ATTR_VM_INSTALL_TYPE_SPECIFIC_ATTRS_MAP, map);

		ConsoleArgumentOutputRetriever retriever = new ConsoleArgumentOutputRetriever();
		ConsoleLineTracker.setDelegate(retriever);
		IProcess process = null;
		ILaunch launch = null;
		String commandLine = null;
		try {
			HashSet<String> set = new HashSet<>();
			set.add(ILaunchManager.RUN_MODE);
			ensurePreferredDelegate(workingCopy, set);
			launch = workingCopy.launch(ILaunchManager.RUN_MODE, null);
			process = launch.getProcesses()[0];
			commandLine = process.getAttribute(IProcess.ATTR_CMDLINE);
		} catch (Exception e) {
			e.printStackTrace();
		}
		assertNotNull(commandLine);
		if (!fUseArgfile) {
			assertEquals("command line must not contain an @argfile", -1, commandLine.indexOf(" @"));
		} else {
			assertTrue("command line must contain an @argfile", commandLine.indexOf(" @") > -1);
		}
		try {
			String output = retriever.getOutput();
			// output if in error
			if (!outputValue.equals(output)) {
			    System.out.println();
			    System.out.println(getName());
				System.out.println("\tExpected:     " + outputValue);
				System.out.println("\tActual:       " + output);
				System.out.println("\tCommand Line: " + commandLine);
				if (fUseArgfile) {
					System.out.println("\tArgfile:      "
							+ readArgfile(commandLine).stream().collect(joining("\n\t              ")));
					System.out.println();
				}
			}
			assertEquals(outputValue, output);
		} finally {
			ConsoleLineTracker.setDelegate(null);
			if (process != null) {
				process.terminate();
			}
			if (launch != null) {
				getLaunchManager().removeLaunch(launch);
			}
		}
	}

	private List<String> readArgfile(String commandLine) {
		String[] arguments = DebugPlugin.parseArguments(commandLine);
		assertEquals("command line too long, only command @argfile expected", 2, arguments.length);
		String argfile = arguments[1];
		assertTrue("wrong command line, expected @argfile at index 1: " + commandLine, argfile != null && argfile.startsWith("@"));
		try {
			return Files.readAllLines(Path.of(argfile.substring(1)));
		} catch (IOException e) {
			throw new IllegalStateException("Error reading @argfile: " + argfile, e);
		}
	}

	/**
	 * Tests the default VM args
	 * @throws CoreException
	 */
	/*public void testDefaultVMArgs() throws CoreException {
	    IVMInstall install = JavaRuntime.getVMInstall(get14Project());
	    assertTrue("should be an IVMInstall2", install instanceof IVMInstall2);
	    IVMInstall2 vm2 = (IVMInstall2) install;
	    String prev = vm2.getVMArgs();
	    vm2.setVMArgs("-Dfoo=\"one two three\"");
	    try {
	        testWithVMArg(null, "one two three");
	    } finally {
	        vm2.setVMArgs(prev);
	    }
	}*/
}
