/*
 * Copyright (C) 2022, Simeon Andreev <simeon.danailov.andreev@gmail.com> and others.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Distribution License v. 1.0 which is available at
 * https://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */
package org.eclipse.jgit.pgm;

import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGETOOL_SECTION;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGE_SECTION;
import static org.junit.Assert.fail;

import java.io.InputStream;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import org.eclipse.jgit.internal.diffmergetool.ExternalMergeTool;
import org.eclipse.jgit.internal.diffmergetool.MergeTools;
import org.eclipse.jgit.lib.StoredConfig;
import org.junit.Before;
import org.junit.Test;

/**
 * Testing the {@code mergetool} command.
 */
public class MergeToolTest extends ToolTestCase {

	private static final String MERGE_TOOL = CONFIG_MERGETOOL_SECTION;

	@Override
	@Before
	public void setUp() throws Exception {
		super.setUp();
		configureEchoTool(TOOL_NAME);
	}

	@Test
	public void testUndefinedTool() throws Exception {
		String toolName = "undefined";
		String[] conflictingFilenames = createMergeConflict();

		List<String> expectedErrors = new ArrayList<>();
		for (String conflictingFilename : conflictingFilenames) {
			expectedErrors.add("External merge tool is not defined: " + toolName);
			expectedErrors.add("merge of " + conflictingFilename + " failed");
		}

		runAndCaptureUsingInitRaw(expectedErrors, MERGE_TOOL,
				"--no-prompt", "--tool", toolName);
	}

	@Test(expected = Die.class)
	public void testUserToolWithCommandNotFoundError() throws Exception {
		String toolName = "customTool";

		int errorReturnCode = 127; // command not found
		String command = "exit " + errorReturnCode;

		StoredConfig config = db.getConfig();
		config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD,
				command);

		createMergeConflict();
		runAndCaptureUsingInitRaw(MERGE_TOOL, "--no-prompt", "--tool",
				toolName);

		fail("Expected exception to be thrown due to external tool exiting with error code: "
				+ errorReturnCode);
	}

	@Test
	public void testEmptyToolName() throws Exception {
		assumeLinuxPlatform();

		String emptyToolName = "";

		StoredConfig config = db.getConfig();
		// the default merge tool is configured without a subsection
		String subsection = null;
		config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_TOOL,
				emptyToolName);

		createMergeConflict();

		String araxisErrorLine = "compare: unrecognized option `-wait' @ error/compare.c/CompareImageCommand/1123.";
		String[] expectedErrorOutput = { araxisErrorLine, araxisErrorLine, };
		runAndCaptureUsingInitRaw(Arrays.asList(expectedErrorOutput),
				MERGE_TOOL, "--no-prompt");
	}

	@Test
	public void testAbortMerge() throws Exception {
		String[] inputLines = {
				"y", // start tool for merge resolution
				"n", // don't accept merge tool result
				"n", // don't continue resolution
		};
		String[] conflictingFilenames = createMergeConflict();
		int abortIndex = 1;
		String[] expectedOutput = getExpectedAbortMergeOutput(
				conflictingFilenames,
				abortIndex);

		String option = "--tool";

		InputStream inputStream = createInputStream(inputLines);
		assertArrayOfLinesEquals("Incorrect output for option: " + option,
				expectedOutput, runAndCaptureUsingInitRaw(inputStream,
						MERGE_TOOL, "--prompt", option, TOOL_NAME));
	}

	@Test
	public void testAbortLaunch() throws Exception {
		String[] inputLines = {
				"n", // abort merge tool launch
		};
		String[] conflictingFilenames = createMergeConflict();
		String[] expectedOutput = getExpectedAbortLaunchOutput(
				conflictingFilenames);

		String option = "--tool";

		InputStream inputStream = createInputStream(inputLines);
		assertArrayOfLinesEquals("Incorrect output for option: " + option,
				expectedOutput, runAndCaptureUsingInitRaw(inputStream,
						MERGE_TOOL, "--prompt", option, TOOL_NAME));
	}

	@Test
	public void testMergeConflict() throws Exception {
		String[] inputLines = {
				"y", // start tool for merge resolution
				"y", // accept merge result as successful
				"y", // start tool for merge resolution
				"y", // accept merge result as successful
		};
		String[] conflictingFilenames = createMergeConflict();
		String[] expectedOutput = getExpectedMergeConflictOutput(
				conflictingFilenames);

		String option = "--tool";

		InputStream inputStream = createInputStream(inputLines);
		assertArrayOfLinesEquals("Incorrect output for option: " + option,
				expectedOutput, runAndCaptureUsingInitRaw(inputStream,
						MERGE_TOOL, "--prompt", option, TOOL_NAME));
	}

	@Test
	public void testDeletedConflict() throws Exception {
		String[] inputLines = {
				"d", // choose delete option to resolve conflict
				"m", // choose merge option to resolve conflict
		};
		String[] conflictingFilenames = createDeletedConflict();
		String[] expectedOutput = getExpectedDeletedConflictOutput(
				conflictingFilenames);

		String option = "--tool";

		InputStream inputStream = createInputStream(inputLines);
		assertArrayOfLinesEquals("Incorrect output for option: " + option,
				expectedOutput, runAndCaptureUsingInitRaw(inputStream,
						MERGE_TOOL, "--prompt", option, TOOL_NAME));
	}

	@Test
	public void testNoConflict() throws Exception {
		createStagedChanges();
		String[] expectedOutput = { "No files need merging" };

		String[] options = { "--tool", "-t", };

		for (String option : options) {
			assertArrayOfLinesEquals("Incorrect output for option: " + option,
					expectedOutput,
					runAndCaptureUsingInitRaw(MERGE_TOOL, option, TOOL_NAME));
		}
	}

	@Test
	public void testMergeConflictNoPrompt() throws Exception {
		String[] conflictingFilenames = createMergeConflict();
		String[] expectedOutput = getExpectedMergeConflictOutputNoPrompt(
				conflictingFilenames);

		String option = "--tool";

		assertArrayOfLinesEquals("Incorrect output for option: " + option,
				expectedOutput,
				runAndCaptureUsingInitRaw(MERGE_TOOL, option, TOOL_NAME));
	}

	@Test
	public void testMergeConflictNoGuiNoPrompt() throws Exception {
		String[] conflictingFilenames = createMergeConflict();
		String[] expectedOutput = getExpectedMergeConflictOutputNoPrompt(
				conflictingFilenames);

		String option = "--tool";

		assertArrayOfLinesEquals("Incorrect output for option: " + option,
				expectedOutput, runAndCaptureUsingInitRaw(MERGE_TOOL,
						"--no-gui", "--no-prompt", option, TOOL_NAME));
	}

	@Test
	public void testToolHelp() throws Exception {
		List<String> expectedOutput = new ArrayList<>();

		MergeTools diffTools = new MergeTools(db);
		Map<String, ExternalMergeTool> predefinedTools = diffTools
				.getPredefinedTools(true);
		List<ExternalMergeTool> availableTools = new ArrayList<>();
		List<ExternalMergeTool> notAvailableTools = new ArrayList<>();
		for (ExternalMergeTool tool : predefinedTools.values()) {
			if (tool.isAvailable()) {
				availableTools.add(tool);
			} else {
				notAvailableTools.add(tool);
			}
		}

		expectedOutput.add(
				"'git mergetool --tool=<tool>' may be set to one of the following:");
		for (ExternalMergeTool tool : availableTools) {
			String toolName = tool.getName();
			expectedOutput.add(toolName);
		}
		String customToolHelpLine = TOOL_NAME + "." + CONFIG_KEY_CMD + " "
				+ getEchoCommand();
		expectedOutput.add("user-defined:");
		expectedOutput.add(customToolHelpLine);
		expectedOutput.add(
				"The following tools are valid, but not currently available:");
		for (ExternalMergeTool tool : notAvailableTools) {
			String toolName = tool.getName();
			expectedOutput.add(toolName);
		}
		String[] userDefinedToolsHelp = {
				"Some of the tools listed above only work in a windowed",
				"environment. If run in a terminal-only session, they will fail.", };
		expectedOutput.addAll(Arrays.asList(userDefinedToolsHelp));

		String option = "--tool-help";
		assertArrayOfLinesEquals("Incorrect output for option: " + option,
				expectedOutput.toArray(new String[0]),
				runAndCaptureUsingInitRaw(MERGE_TOOL, option));
	}

	private void configureEchoTool(String toolName) {
		StoredConfig config = db.getConfig();
		// the default merge tool is configured without a subsection
		String subsection = null;
		config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_TOOL,
				toolName);

		String command = getEchoCommand();

		config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD,
				command);
		/*
		 * prevent prompts as we are running in tests and there is no user to
		 * interact with on the command line
		 */
		config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_PROMPT,
				String.valueOf(false));
	}

	private String[] getExpectedMergeConflictOutputNoPrompt(
			String[] conflictFilenames) {
		List<String> expected = new ArrayList<>();
		expected.add("Merging:");
		for (String conflictFilename : conflictFilenames) {
			expected.add(conflictFilename);
		}
		for (String conflictFilename : conflictFilenames) {
			expected.add("Normal merge conflict for '" + conflictFilename
					+ "':");
			expected.add("{local}: modified file");
			expected.add("{remote}: modified file");
			Path filePath = getFullPath(conflictFilename);
			expected.add(filePath.toString());
			expected.add(conflictFilename + " seems unchanged.");
		}
		return expected.toArray(new String[0]);
	}

	private static String[] getExpectedAbortLaunchOutput(
			String[] conflictFilenames) {
		List<String> expected = new ArrayList<>();
		expected.add("Merging:");
		for (String conflictFilename : conflictFilenames) {
			expected.add(conflictFilename);
		}
		if (conflictFilenames.length > 1) {
			String conflictFilename = conflictFilenames[0];
			expected.add(
					"Normal merge conflict for '" + conflictFilename + "':");
			expected.add("{local}: modified file");
			expected.add("{remote}: modified file");
			expected.add("Hit return to start merge resolution tool ("
					+ TOOL_NAME + "):");
		}
		return expected.toArray(new String[0]);
	}

	private String[] getExpectedAbortMergeOutput(
			String[] conflictFilenames, int abortIndex) {
		List<String> expected = new ArrayList<>();
		expected.add("Merging:");
		for (String conflictFilename : conflictFilenames) {
			expected.add(conflictFilename);
		}
		for (int i = 0; i < conflictFilenames.length; ++i) {
			if (i == abortIndex) {
				break;
			}

			String conflictFilename = conflictFilenames[i];
			expected.add(
					"Normal merge conflict for '" + conflictFilename + "':");
			expected.add("{local}: modified file");
			expected.add("{remote}: modified file");
			Path fullPath = getFullPath(conflictFilename);
			expected.add("Hit return to start merge resolution tool ("
					+ TOOL_NAME + "): " + fullPath);
			expected.add(conflictFilename + " seems unchanged.");
			expected.add("Was the merge successful [y/n]?");
			if (i < conflictFilenames.length - 1) {
				expected.add(
						"\tContinue merging other unresolved paths [y/n]?");
			}
		}
		return expected.toArray(new String[0]);
	}

	private String[] getExpectedMergeConflictOutput(
			String[] conflictFilenames) {
		List<String> expected = new ArrayList<>();
		expected.add("Merging:");
		for (String conflictFilename : conflictFilenames) {
			expected.add(conflictFilename);
		}
		for (int i = 0; i < conflictFilenames.length; ++i) {
			String conflictFilename = conflictFilenames[i];
			expected.add("Normal merge conflict for '" + conflictFilename
					+ "':");
			expected.add("{local}: modified file");
			expected.add("{remote}: modified file");
			Path filePath = getFullPath(conflictFilename);
			expected.add("Hit return to start merge resolution tool ("
					+ TOOL_NAME + "): " + filePath);
			expected.add(conflictFilename + " seems unchanged.");
			expected.add("Was the merge successful [y/n]?");
			if (i < conflictFilenames.length - 1) {
				// expected.add(
				// "\tContinue merging other unresolved paths [y/n]?");
			}
		}
		return expected.toArray(new String[0]);
	}

	private static String[] getExpectedDeletedConflictOutput(
			String[] conflictFilenames) {
		List<String> expected = new ArrayList<>();
		expected.add("Merging:");
		for (String mergeConflictFilename : conflictFilenames) {
			expected.add(mergeConflictFilename);
		}
		for (int i = 0; i < conflictFilenames.length; ++i) {
			String conflictFilename = conflictFilenames[i];
			expected.add(conflictFilename + " seems unchanged.");
			expected.add("{local}: deleted");
			expected.add("{remote}: modified file");
			expected.add("Use (m)odified or (d)eleted file, or (a)bort?");
		}
		return expected.toArray(new String[0]);
	}

	private static String getEchoCommand() {
		/*
		 * use 'MERGED' placeholder, as both 'LOCAL' and 'REMOTE' will be
		 * replaced with full paths to a temporary file during some of the tests
		 */
		return "(echo \"$MERGED\")";
	}
}
