/*******************************************************************************
 * Copyright (c) 2017 Microsoft Corporation and others.
 * All rights reserved. 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:
 *     Microsoft Corporation. - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdt.ls.core.internal.correction;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.compiler.IProblem;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.manipulation.CoreASTProvider;
import org.eclipse.jdt.ls.core.internal.JDTUtils;
import org.eclipse.jdt.ls.core.internal.TextEditUtil;
import org.eclipse.jdt.ls.core.internal.handlers.CodeActionHandler;
import org.eclipse.jdt.ls.core.internal.handlers.DiagnosticsHandler;
import org.eclipse.jdt.ls.core.internal.managers.AbstractProjectsManagerBasedTest;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.CodeActionContext;
import org.eclipse.lsp4j.CodeActionKind;
import org.eclipse.lsp4j.CodeActionParams;
import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.ResourceOperation;
import org.eclipse.lsp4j.TextDocumentEdit;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.TextEdit;
import org.eclipse.lsp4j.WorkspaceEdit;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.junit.Assert;

public class AbstractQuickFixTest extends AbstractProjectsManagerBasedTest {

	private List<String> ignoredCommands;

	private List<String> ignoredKinds = Arrays.asList(CodeActionKind.Source + ".*");

	private List<String> onlyKinds;

	protected void assertCodeActionExists(ICompilationUnit cu, Expected expected) throws Exception {
		List<Either<Command, CodeAction>> codeActions = evaluateCodeActions(cu);
		for (Either<Command, CodeAction> c : codeActions) {
			if (Objects.equals(expected.name, getTitle(c))) {
				expected.assertEquivalent(c);
				return;
			}
		}
		String allCommands = codeActions.stream().map(a -> getTitle(a)).collect(Collectors.joining("\n"));
		fail(expected.name + " not found in " + allCommands);
	}

	protected void assertCodeActionNotExists(ICompilationUnit cu, String label) throws Exception {
		List<Either<Command, CodeAction>> codeActionCommands = evaluateCodeActions(cu);
		assertFalse("'" + label + "' should not be added to the code actions", codeActionCommands.stream().filter(ca -> getTitle(ca).equals(label)).findAny().isPresent());
	}

	protected void assertCodeActions(ICompilationUnit cu, Collection<Expected> expected) throws Exception {
		assertCodeActions(cu, expected.toArray(new Expected[expected.size()]));
	}

	protected void assertCodeActions(ICompilationUnit cu, Range range, Collection<Expected> expected) throws Exception {
		assertCodeActions(cu, range, expected.toArray(new Expected[expected.size()]));
	}

	protected void assertCodeActions(ICompilationUnit cu, Expected... expecteds) throws Exception {
		List<Either<Command, CodeAction>> codeActions = evaluateCodeActions(cu);
		assertCodeActions(codeActions, expecteds);
	}

	protected void assertCodeActions(ICompilationUnit cu, Range range, Expected... expecteds) throws Exception {
		List<Either<Command, CodeAction>> codeActions = evaluateCodeActions(cu, range);
		assertCodeActions(codeActions, expecteds);
	}

	protected void assertCodeActions(List<Either<Command, CodeAction>> codeActions, Expected... expecteds) throws Exception {
		if (codeActions.size() < expecteds.length) {
			String res = codeActions.stream().map(a -> ("'" + getTitle(a) + "'")).collect(Collectors.joining(","));
			assertEquals("Number of code actions: " + res, expecteds.length, codeActions.size());
		}

		Map<String, Expected> expectedActions = Stream.of(expecteds).collect(Collectors.toMap(Expected::getName, Function.identity()));
		Map<String, Either<Command, CodeAction>> actualActions = codeActions.stream().collect(Collectors.toMap(this::getTitle, Function.identity()));

		for (Expected expected : expecteds) {
			Either<Command, CodeAction> action = actualActions.get(expected.name);
			assertNotNull("Should prompt code action: " + expected.name, action);
			expected.assertEquivalent(action);
		}

		int k = 0;
		String aStr = "", eStr = "", testContent = "";
		for (Either<Command, CodeAction> c : codeActions) {
			String title = getTitle(c);
			Expected e = expectedActions.get(title);
			if (e != null) {
				String actual = evaluateCodeActionCommand(c);
				if (!Objects.equals(e.content, actual)) {
					aStr += '\n' + title + '\n' + actual;
					eStr += '\n' + e.name + '\n' + e.content;
				}
				testContent += generateTest(actual, getTitle(c), k);
				k++;
			}
		}
		if (aStr.length() > 0) {
			aStr += '\n' + testContent;
		}
		assertEquals(eStr, aStr);
	}

	protected String generateTest(String actual, String name, int k) {
		StringBuilder builder = new StringBuilder();
		String[] lines = actual.split("\n");
		builder.append("		buf = new StringBuilder();\n");
		for (String line : lines) {
			wrapInBufAppend(line, builder);
		}
		builder.append("		Expected e" + k + " = new Expected(\"" + name + "\", buf.toString());\n");
		builder.append("\n");
		return builder.toString();
	}

	private static void wrapInBufAppend(String curr, StringBuilder buf) {
		buf.append("		buf.append(\"");

		int last = curr.length() - 1;
		for (int k = 0; k <= last; k++) {
			char ch = curr.charAt(k);
			if (ch == '\n') {
				buf.append("\\n\");\n");
				if (k < last) {
					buf.append("buf.append(\"");
				}
			} else if (ch == '\r') {
				// ignore
			} else if (ch == '\t') {
				buf.append("    "); // 4 spaces
			} else if (ch == '"' || ch == '\\') {
				buf.append('\\').append(ch);
			} else {
				buf.append(ch);
			}
		}
		if (buf.length() > 0 && buf.charAt(buf.length() - 1) != '\n') {
			buf.append("\\n\");\n");
		}
	}

	public class Expected {
		String name;
		String content;
		String kind;
		private static final String ALL_KINDS = "*";

		public Expected(String name, String content) {
			this(name, content, ALL_KINDS);
		}

		public Expected(String name, String content, String kind) {
			this.content = content;
			this.name = name;
			this.kind = kind;
		}

		public String getName() {
			return name;
		}

		/**
		 * Checks if the action has the same title as this. If it has, then assert that
		 * that action is equivalent to this in kind and content.
		 */
		public void assertEquivalent(Either<Command, CodeAction> action) throws Exception {
			String title = getTitle(action);
			assertEquals("Unexpected command :", name, title);
			if (!ALL_KINDS.equals(kind) && action.isRight()) {
				assertEquals(title + " has the wrong kind ", kind, action.getRight().getKind());
			}
			String actionContent = evaluateCodeActionCommand(action);
			assertEquals(title + " has the wrong content ", content, actionContent);
		}
	}

	protected Range getRange(ICompilationUnit cu, IProblem[] problems) throws JavaModelException {
		if (problems.length == 0) {
			return new Range(new Position(), new Position());
		}
		IProblem problem = problems[0];
		return JDTUtils.toRange(cu, problem.getSourceStart(), 0);
	}

	protected void setIgnoredCommands(String... ignoredCommands) {
		this.ignoredCommands = Arrays.asList(ignoredCommands);
	}

	protected void setIgnoredKind(String... ignoredKind) {
		this.ignoredKinds = Arrays.asList(ignoredKind);
	}

	protected void setOnly(String... onlyKinds) {
		this.onlyKinds = Arrays.asList(onlyKinds);
	}

	protected List<Either<Command, CodeAction>> evaluateCodeActions(ICompilationUnit cu) throws JavaModelException {

		CompilationUnit astRoot = CoreASTProvider.getInstance().getAST(cu, CoreASTProvider.WAIT_YES, null);
		IProblem[] problems = astRoot.getProblems();

		Range range = getRange(cu, problems);
		return evaluateCodeActions(cu, range);
	}

	protected List<Either<Command, CodeAction>> evaluateCodeActions(ICompilationUnit cu, Range range) throws JavaModelException {

		CompilationUnit astRoot = CoreASTProvider.getInstance().getAST(cu, CoreASTProvider.WAIT_YES, null);
		IProblem[] problems = astRoot.getProblems();

		CodeActionParams parms = new CodeActionParams();

		TextDocumentIdentifier textDocument = new TextDocumentIdentifier();
		textDocument.setUri(JDTUtils.toURI(cu));
		parms.setTextDocument(textDocument);
		parms.setRange(range);
		CodeActionContext context = new CodeActionContext();
		context.setDiagnostics(DiagnosticsHandler.toDiagnosticsArray(cu, Arrays.asList(problems), true));
		context.setOnly(onlyKinds);
		parms.setContext(context);

		List<Either<Command, CodeAction>> codeActions = new CodeActionHandler(this.preferenceManager).getCodeActionCommands(parms, new NullProgressMonitor());
		if (onlyKinds != null && !onlyKinds.isEmpty()) {
			for (Either<Command, CodeAction> codeAction : codeActions) {
				Stream<String> acceptedActionKinds = onlyKinds.stream();
				String kind = codeAction.getRight().getKind();
				assertTrue(codeAction.getRight().getTitle() + " has kind " + kind + " but only " + onlyKinds + " are accepted", acceptedActionKinds.filter(k -> kind != null && kind.startsWith(k)).findFirst().isPresent());
			}
		}

		if (this.ignoredKinds != null) {
			List<Either<Command, CodeAction>> filteredList = codeActions.stream().filter(Either::isRight).filter(codeAction -> {
				for (String str : this.ignoredKinds) {
					if (codeAction.getRight().getKind().matches(str)) {
						return true;
					}
				}
				return false;
			}).collect(Collectors.toList());
			codeActions.removeAll(filteredList);
		}

		if (this.ignoredCommands != null) {
			List<Either<Command, CodeAction>> filteredList = new ArrayList<>();
			for (Either<Command, CodeAction> codeAction : codeActions) {
				for (String str : this.ignoredCommands) {
					if (getTitle(codeAction).matches(str)) {
						filteredList.add(codeAction);
						break;
					}
				}
			}
			codeActions.removeAll(filteredList);
		}
		return codeActions;
	}

	protected String evaluateCodeActionCommand(Either<Command, CodeAction> codeAction)
			throws BadLocationException, JavaModelException {

		Command c = codeAction.isLeft() ? codeAction.getLeft() : codeAction.getRight().getCommand();
		Assert.assertEquals(CodeActionHandler.COMMAND_ID_APPLY_EDIT, c.getCommand());
		Assert.assertNotNull(c.getArguments());
		Assert.assertTrue(c.getArguments().get(0) instanceof WorkspaceEdit);
		WorkspaceEdit we = (WorkspaceEdit) c.getArguments().get(0);
		if (we.getDocumentChanges() != null) {
			return evaluateChanges(we.getDocumentChanges());
		}
		return evaluateChanges(we.getChanges());
	}

	private String evaluateChanges(List<Either<TextDocumentEdit, ResourceOperation>> documentChanges) throws BadLocationException, JavaModelException {
		List<TextDocumentEdit> changes = documentChanges.stream().filter(e -> e.isLeft()).map(e -> e.getLeft()).collect(Collectors.toList());
		assertFalse("No edits generated", changes.isEmpty());
		Set<String> uris = changes.stream().map(tde -> tde.getTextDocument().getUri()).distinct().collect(Collectors.toSet());
		assertEquals("Only one resource should be modified", 1, uris.size());
		String uri = uris.iterator().next();
		List<TextEdit> edits = changes.stream().flatMap(e -> e.getEdits().stream()).collect(Collectors.toList());
		return evaluateChanges(uri, edits);
	}

	protected String evaluateChanges(Map<String, List<TextEdit>> changes) throws BadLocationException, JavaModelException {
		Iterator<Entry<String, List<TextEdit>>> editEntries = changes.entrySet().iterator();
		Entry<String, List<TextEdit>> entry = editEntries.next();
		assertNotNull("No edits generated", entry);
		assertEquals("More than one resource modified", false, editEntries.hasNext());
		return evaluateChanges(entry.getKey(), entry.getValue());
	}

	private String evaluateChanges(String uri, List<TextEdit> edits) throws BadLocationException, JavaModelException {
		assertFalse("No edits generated: " + edits, edits == null || edits.isEmpty());
		ICompilationUnit cu = JDTUtils.resolveCompilationUnit(uri);
		assertNotNull("CU not found: " + uri, cu);
		Document doc = new Document();
		if (cu.exists()) {
			doc.set(cu.getSource());
		}
		return TextEditUtil.apply(doc, edits);
	}

	public Command getCommand(Either<Command, CodeAction> codeAction) {
		return codeAction.isLeft() ? codeAction.getLeft() : codeAction.getRight().getCommand();
	}

	public String getTitle(Either<Command, CodeAction> codeAction) {
		return getCommand(codeAction).getTitle();
	}

}