/*******************************************************************************
 * Copyright (c) 2020 Red Hat Inc. 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:
 *     Red Hat Inc. - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdt.ui.unittest.junit.ui;

import java.lang.reflect.InvocationTargetException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.eclipse.unittest.model.ITestCaseElement;
import org.eclipse.unittest.model.ITestElement.FailureTrace;
import org.eclipse.unittest.model.ITestRunSession;

import org.eclipse.swt.widgets.Shell;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.OperationCanceledException;

import org.eclipse.jface.dialogs.MessageDialog;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;

import org.eclipse.ui.PlatformUI;

import org.eclipse.ui.texteditor.ITextEditor;

import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.ISourceRange;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeHierarchy;
import org.eclipse.jdt.core.JavaConventions;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.Signature;
import org.eclipse.jdt.core.search.IJavaSearchConstants;
import org.eclipse.jdt.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.core.search.SearchMatch;
import org.eclipse.jdt.core.search.SearchParticipant;
import org.eclipse.jdt.core.search.SearchPattern;
import org.eclipse.jdt.core.search.SearchRequestor;

import org.eclipse.jdt.internal.corext.util.JavaConventionsUtil;

import org.eclipse.jdt.ui.unittest.junit.JUnitTestPlugin;

import org.eclipse.jdt.internal.ui.actions.SelectionConverter;

/**
 * Open a class on a Test method.
 */
public class OpenTestAction extends OpenEditorAction {

	private String fMethodName;
	private String[] fMethodParamTypes;
	private IMethod fMethod;
	private int fLineNumber = -1;

	private IType fType;

	public OpenTestAction(Shell shell, ITestCaseElement testCase, String[] methodParamTypes) {
		this(shell, JUnitTestViewSupport.getClassName(testCase), extractRealMethodName(testCase), methodParamTypes,
				true, testCase.getTestRunSession());
		FailureTrace trace = testCase.getFailureTrace();
		if (trace != null) {
			String rawClassName = JUnitTestViewSupport.extractRawClassName(testCase.getTestName());
			rawClassName = rawClassName.replaceAll("\\.", "\\\\."); //$NON-NLS-1$//$NON-NLS-2$
			rawClassName = rawClassName.replaceAll("\\$", "\\\\\\$"); //$NON-NLS-1$//$NON-NLS-2$
			Pattern pattern = Pattern.compile(
					JUnitTestViewSupport.FRAME_LINE_PREFIX + rawClassName + '.' + fMethodName + "\\(.*:(\\d+)\\)" //$NON-NLS-1$
			);
			Matcher matcher = pattern.matcher(trace.getTrace());
			if (matcher.find()) {
				try {
					fLineNumber = Integer.parseInt(matcher.group(1));
				} catch (NumberFormatException e) {
					// continue
				}
			}
		}
	}

	public OpenTestAction(Shell shell, String className, ITestRunSession session) {
		this(shell, className, null, null, true, session);
	}

	public OpenTestAction(Shell shell, String className, String method, String[] methodParamTypes, boolean activate,
			ITestRunSession session) {
		super(shell, className, activate, session);
		PlatformUI.getWorkbench().getHelpSystem().setHelp(this, IJUnitHelpContextIds.OPENTEST_ACTION);
		fMethodName = method;
		fMethodParamTypes = methodParamTypes;
	}

	private static String extractRealMethodName(ITestCaseElement testCase) {
		// workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=334864 :
		if (testCase.isIgnored() && JavaConventions
				.validateJavaTypeName(testCase.getTestName(), JavaCore.VERSION_1_5, JavaCore.VERSION_1_5, null)
				.getSeverity() != IStatus.ERROR) {
			return null;
		}

		// workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=275308 :
		String testMethodName = JUnitTestViewSupport.getTestMethodName(testCase);
		for (int i = 0; i < testMethodName.length(); i++) {
			if (!Character.isJavaIdentifierPart(testMethodName.charAt(i))) {
				return testMethodName.substring(0, i);
			}
		}
		return testMethodName;
	}

	@Override
	protected IJavaElement findElement(IJavaProject project, String className) throws JavaModelException {
		IType type = findType(project, className);
		if (type == null)
			return null;

		if (fMethodName == null) {
			fType = type;
			return type;
		}

		IMethod method = null;
		try {
			method = findMethod(type);
			if (method == null) {
				ITypeHierarchy typeHierarchy = type.newSupertypeHierarchy(null);
				IType[] supertypes = typeHierarchy.getAllSupertypes(type);
				for (IType supertype : supertypes) {
					method = findMethod(supertype);
					if (method != null)
						break;
				}
			}
		} catch (OperationCanceledException e) {
			// user cancelled the selection dialog - ignore and proceed
		}
		if (method == null) {
			if (fLineNumber < 0) {
				String title = JUnitMessages.OpenTestAction_dialog_title;
				String message = MessageFormat.format(JUnitMessages.OpenTestAction_error_methodNoFound,
						BasicElementLabels.getJavaElementName(fMethodName));
				MessageDialog.openInformation(getShell(), title, message);
			}
			return type;
		}

		fMethod = method;
		return method;
	}

	private IMethod findMethod(IType type) {
		IStatus status = JavaConventionsUtil.validateMethodName(fMethodName, type);
		if (!status.isOK())
			return null;

		List<IMethod> foundMethods = new ArrayList<>();
		try {
			PlatformUI.getWorkbench().getProgressService().busyCursorWhile(monitor -> {
				String methodPattern = type.getFullyQualifiedName('.') + '.' + fMethodName;
				if (fMethodParamTypes != null && fMethodParamTypes.length > 0) {
					String paramTypes = Arrays.stream(fMethodParamTypes).map(paramType -> {
						try {
							return paramType = Signature.toString(paramType);
						} catch (IllegalArgumentException e1) {
							// return the paramType as it is
						}
						return paramType.replace('$', '.'); // for nested classes... See OpenEditorAction#findType also.
					}).collect(Collectors.joining(", ", "(", ")")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
					methodPattern += paramTypes;
				} else {
					methodPattern += "()"; //$NON-NLS-1$
				}
				int matchRule = SearchPattern.R_ERASURE_MATCH | SearchPattern.R_EXACT_MATCH
						| SearchPattern.R_CASE_SENSITIVE;
				SearchPattern searchPattern = SearchPattern.createPattern(methodPattern, IJavaSearchConstants.METHOD,
						IJavaSearchConstants.DECLARATIONS, matchRule);
				if (searchPattern == null) {
					return;
				}
				SearchRequestor requestor = new SearchRequestor() {
					@Override
					public void acceptSearchMatch(SearchMatch match) throws CoreException {
						Object element = match.getElement();
						if (element instanceof IMethod) {
							foundMethods.add((IMethod) element);
						}
					}
				};
				SearchParticipant[] participants = new SearchParticipant[] {
						SearchEngine.getDefaultSearchParticipant() };
				IJavaSearchScope scope = SearchEngine.createJavaSearchScope(new IJavaElement[] { type });
				try {
					new SearchEngine().search(searchPattern, participants, scope, requestor, monitor);
				} catch (CoreException e2) {
					JUnitTestPlugin.log(e2);
				}
			});
		} catch (InvocationTargetException e) {
			JUnitTestPlugin.log(e);
		} catch (InterruptedException e) {
			// user cancelled
		}

		if (foundMethods.size() == 1) {
			return foundMethods.get(0);
		} else if (foundMethods.size() > 1) {
			IMethod method = openSelectionDialog(foundMethods);
			if (method == null) {
				throw new OperationCanceledException();
			}
			return method;
		}

		// search just by name and number of parameters, if method not found yet
		try {
			for (IMethod method : type.getMethods()) {
				String methodName = method.getElementName();
				if (fMethodName.equals(methodName)) {
					int numOfParams = method.getNumberOfParameters();
					int requiredNumOfParams = 0;
					if (fMethodParamTypes != null) {
						requiredNumOfParams = fMethodParamTypes.length;
					}
					if (numOfParams == requiredNumOfParams) {
						foundMethods.add(method);
					}
				}
			}
			if (foundMethods.isEmpty()) {
				return null;
			} else if (foundMethods.size() > 1) {
				IMethod method = openSelectionDialog(foundMethods);
				if (method == null) {
					throw new OperationCanceledException();
				}
				return method;
			} else {
				return foundMethods.get(0);
			}
		} catch (JavaModelException e) {
			// if type does not exist or if an exception occurs while accessing its resource
			// => ignore (no method found)
		}

		return null;
	}

	private IMethod openSelectionDialog(List<IMethod> foundMethods) {
		IMethod[] elements = foundMethods.toArray(new IMethod[foundMethods.size()]);
		String title = JUnitMessages.OpenTestAction_dialog_title;
		String message = JUnitMessages.OpenTestAction_select_element;
		return (IMethod) SelectionConverter.selectJavaElement(elements, getShell(), title, message);
	}

	@Override
	protected void reveal(ITextEditor textEditor) {
		if (fLineNumber >= 0) {
			try {
				IDocument document = textEditor.getDocumentProvider().getDocument(textEditor.getEditorInput());
				int lineOffset = document.getLineOffset(fLineNumber - 1);
				int lineLength = document.getLineLength(fLineNumber - 1);
				if (fMethod != null) {
					try {
						ISourceRange sr = fMethod.getSourceRange();
						if (sr == null || sr.getOffset() == -1 || lineOffset < sr.getOffset()
								|| sr.getOffset() + sr.getLength() < lineOffset + lineLength) {
							throw new BadLocationException();
						}
					} catch (JavaModelException e) {
						// not a problem
					}
				}
				textEditor.selectAndReveal(lineOffset, lineLength);
				return;
			} catch (BadLocationException x) {
				// marker refers to invalid text position -> do nothing
			}
		}
		if (fMethod != null) {
			try {
				ISourceRange range = fMethod.getNameRange();
				if (range != null && range.getOffset() >= 0)
					textEditor.selectAndReveal(range.getOffset(), range.getLength());
				return;
			} catch (JavaModelException e) {
				// not a problem
			}
		}
		if (fType != null) {
			try {
				ISourceRange range = fType.getNameRange();
				if (range != null && range.getOffset() >= 0)
					textEditor.selectAndReveal(range.getOffset(), range.getLength());
			} catch (JavaModelException e) {
				// not a problem
			}
		}
	}

}
