/*******************************************************************************
 * Copyright (c) 2000, 2022 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
 *     Johannes Utzig <mail@jutzig.de> - [JUnit] Update test suite wizard for JUnit 4: @RunWith(Suite.class)... - https://bugs.eclipse.org/155828
 *******************************************************************************/
package org.eclipse.jdt.internal.junit.wizards;

import java.util.Set;

import org.eclipse.jdt.junit.wizards.NewTestSuiteWizardPage;

import org.eclipse.swt.widgets.Shell;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.SubProgressMonitor;

import org.eclipse.core.resources.IFile;

import org.eclipse.jface.action.IAction;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.window.Window;

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

import org.eclipse.ui.IObjectActionDelegate;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.dialogs.ISelectionStatusValidator;

import org.eclipse.jdt.core.IAnnotation;
import org.eclipse.jdt.core.IBuffer;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.ISourceRange;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;

import org.eclipse.jdt.internal.junit.Messages;
import org.eclipse.jdt.internal.junit.ui.JUnitPlugin;
import org.eclipse.jdt.internal.junit.util.CheckedTableSelectionDialog;
import org.eclipse.jdt.internal.junit.util.ExceptionHandler;
import org.eclipse.jdt.internal.junit.util.JUnitStatus;
import org.eclipse.jdt.internal.junit.util.JUnitStubUtility;
import org.eclipse.jdt.internal.junit.util.Resources;

import org.eclipse.jdt.ui.JavaElementLabelProvider;

/**
 * An object contribution action that updates existing AllTests classes.
 */
public class UpdateTestSuite implements IObjectActionDelegate {

	private Shell fShell;
	private IPackageFragment fPack;
	private ICompilationUnit fTestSuite;
	private IMethod fSuiteMethod;
	private static boolean fEmptySelectionAllowed= false;
	private Object[] fSelectedTestCases;
	private boolean fIsJunit4;
	private boolean fIsJunit5;
	private IAnnotation fSuiteClasses;

	private class UpdateAllTestsValidator implements ISelectionStatusValidator {
		/*
		 * @see ISelectionValidator#validate(Object[])
		 */
		@Override
		public IStatus validate(Object[] selection) {
			int count= 0;
			for (Object element : selection) {
				if (element instanceof IType) {
					count++;
				}
			}
			if (count == 0 && !fEmptySelectionAllowed) {
				return new JUnitStatus(IStatus.ERROR, ""); //$NON-NLS-1$
			}

			IStatus recursiveInclusionStatus= checkRecursiveSuiteInclusion(selection);
			if (recursiveInclusionStatus != null && ! recursiveInclusionStatus.isOK())
				return recursiveInclusionStatus;

			String message;
			if (count == 1) {
				message= Messages.format(WizardMessages.UpdateAllTests_selected_methods_label_one, Integer.valueOf(count));
			} else {
				message= Messages.format(WizardMessages.UpdateAllTests_selected_methods_label_many, Integer.valueOf(count));
			}
			return new JUnitStatus(IStatus.INFO, message);
		}

		private IStatus checkRecursiveSuiteInclusion(Object[] selection){
			IType suiteClass= null;
			if (fIsJunit4 || fIsJunit5) {
				suiteClass= (IType) fSuiteClasses.getParent();
			} else {
				suiteClass= fSuiteMethod.getDeclaringType();
			}
			for (Object element : selection) {
				if (element instanceof IType){
					if (((IType)element).equals(suiteClass)){
						return new JUnitStatus(IStatus.WARNING, WizardMessages.UpdateTestSuite_infinite_recursion);
					}
				}
			}
			return null;
		}
	}

	public UpdateTestSuite() {
		super();
	}

	/*
	 * @see IObjectActionDelegate#setActivePart(IAction, IWorkbenchPart)
	 */
	@Override
	public void setActivePart(IAction action, IWorkbenchPart targetPart) {
	}

	/*
	 * @see IActionDelegate#run(IAction)
	 */
	@Override
	public void run(IAction action) {
		ILabelProvider lprovider= new JavaElementLabelProvider(JavaElementLabelProvider.SHOW_DEFAULT);
		SuiteClassesContentProvider cprovider= new SuiteClassesContentProvider(fIsJunit4, fIsJunit5);

		if (fIsJunit4) {
			/* find TestClasses already in Test Suite */
			IType testSuiteType= fTestSuite.findPrimaryType();
			fSuiteClasses= testSuiteType.getAnnotation("SuiteClasses"); //$NON-NLS-1$
			if (fSuiteClasses.exists()) {
				openTestSelectionDialog(lprovider, cprovider, testSuiteType);
			} else {
				noSuiteError();
			}

		} else if (fIsJunit5) {
			/* find TestClasses already in Test Suite */
			IType testSuiteType= fTestSuite.findPrimaryType();
			fSuiteClasses= testSuiteType.getAnnotation("SelectClasses"); //$NON-NLS-1$
			if (fSuiteClasses.exists()) {
				openTestSelectionDialog(lprovider, cprovider, testSuiteType);
			} else {
				noSuiteError();
			}
		} else {
			/* find TestClasses already in Test Suite */
			IType testSuiteType= fTestSuite.findPrimaryType();
			fSuiteMethod= testSuiteType.getMethod("suite", new String[] {}); //$NON-NLS-1$
			if (fSuiteMethod.exists()) {
				try {
					ISourceRange range= fSuiteMethod.getSourceRange();
					IBuffer buf= fTestSuite.getBuffer();
					String originalContent= buf.getText(range.getOffset(), range.getLength());
					buf.close();
					if (getTestSuiteClassListRange(originalContent) != null) {
						openTestSelectionDialog(lprovider, cprovider, testSuiteType);
					} else {
						cannotUpdateSuiteError();
					}
				} catch (JavaModelException e) {
					JUnitPlugin.log(e);
				}
			} else {
				noSuiteError();
			}
		}
	}

	private void openTestSelectionDialog(ILabelProvider lprovider, SuiteClassesContentProvider cprovider, IType testSuiteType) {
		CheckedTableSelectionDialog dialog= new CheckedTableSelectionDialog(fShell, lprovider, cprovider);
		dialog.setValidator(new UpdateAllTestsValidator());
		dialog.setTitle(WizardMessages.UpdateAllTests_title);
		dialog.setMessage(WizardMessages.UpdateAllTests_message);
		Set<IType> elements= cprovider.getTests(fPack);
		elements.remove(testSuiteType);
		dialog.setInitialSelections(elements.toArray());
		dialog.setSize(60, 25);
		dialog.setInput(fPack);
		if (dialog.open() == Window.OK) {
			fSelectedTestCases= dialog.getResult();
			try {
				PlatformUI.getWorkbench().getProgressService().busyCursorWhile(getRunnable());
			} catch (Exception e) {
				JUnitPlugin.log(e);
			}
		}
	}

	/*
	 * @see IActionDelegate#selectionChanged(IAction, ISelection)
	 */
	@Override
	public void selectionChanged(IAction action, ISelection selection) {
		fShell= JUnitPlugin.getActiveWorkbenchShell();
		if (selection instanceof IStructuredSelection) {
			Object testSuiteObj= ((IStructuredSelection) selection).getFirstElement();
			if (testSuiteObj instanceof ICompilationUnit) {
				fTestSuite= (ICompilationUnit) testSuiteObj;
				IJavaElement packIJE= fTestSuite.getParent();
				if (packIJE instanceof IPackageFragment) {
					fPack= (IPackageFragment) packIJE;
				}
				IType primaryType = fTestSuite.findPrimaryType();
				if (primaryType != null) {
					fIsJunit4 = primaryType.getAnnotation("RunWith").exists(); //$NON-NLS-1$
					fIsJunit5 = primaryType.getAnnotation("Test").exists() //$NON-NLS-1$
							&& primaryType.getAnnotation("Test").getElementName().startsWith("org.junit.jupiter"); //$NON-NLS-1$ //$NON-NLS-2$
				}

			}
		}
	}

	public static void updateTestCasesInJunit4Suite(IProgressMonitor monitor, ICompilationUnit testSuite, IAnnotation testClassesAnnotation, Object[] selectedTestCases) throws JavaModelException {
		try {
			monitor.beginTask(WizardMessages.UpdateAllTests_beginTask, 5);

			ISourceRange range= testClassesAnnotation.getSourceRange();
			IDocument fullSource= new Document(testSuite.getBuffer().getContents());
			StringBuilder source= new StringBuilder();
			monitor.worked(1);
			source.append(getUpdatableAnnotations(selectedTestCases));
			fullSource.replace(range.getOffset(), range.getLength(), source.toString());
			monitor.worked(1);
			String formattedContent= JUnitStubUtility.formatCompilationUnit(testSuite.getJavaProject(), fullSource.get(), testSuite.findRecommendedLineSeparator());
			IBuffer buf= testSuite.getBuffer();
			buf.replace(0, buf.getLength(), formattedContent);
			monitor.worked(1);
			testSuite.save(new SubProgressMonitor(monitor, 1), true);
			monitor.worked(1);


		}  catch (BadLocationException e) {
			Assert.isTrue(false, "Should never happen"); //$NON-NLS-1$
		} finally{
			monitor.done();
		}
	}

	public static void updateTestCasesInJunit5Suite(IProgressMonitor monitor, ICompilationUnit testSuite, IAnnotation testClassesAnnotation, Object[] selectedTestCases) throws JavaModelException {
		try {
			monitor.beginTask(WizardMessages.UpdateAllTests_beginTask, 5);

			ISourceRange range= testClassesAnnotation.getSourceRange();
			IDocument fullSource= new Document(testSuite.getBuffer().getContents());
			StringBuilder source= new StringBuilder();
			monitor.worked(1);
			source.append(getUpdatableJUnit5Annotations(selectedTestCases));
			fullSource.replace(range.getOffset(), range.getLength(), source.toString());
			monitor.worked(1);
			String formattedContent= JUnitStubUtility.formatCompilationUnit(testSuite.getJavaProject(), fullSource.get(), testSuite.findRecommendedLineSeparator());
			IBuffer buf= testSuite.getBuffer();
			buf.replace(0, buf.getLength(), formattedContent);
			monitor.worked(1);
			testSuite.save(new SubProgressMonitor(monitor, 1), true);
			monitor.worked(1);


		}  catch (BadLocationException e) {
			Assert.isTrue(false, "Should never happen"); //$NON-NLS-1$
		} finally{
			monitor.done();
		}
	}

	public static void updateTestCasesInSuite(IProgressMonitor monitor, ICompilationUnit testSuite, IMethod suiteMethod, Object[] selectedTestCases) throws JavaModelException {
		try {
			monitor.beginTask(WizardMessages.UpdateAllTests_beginTask, 5);

			ISourceRange range= suiteMethod.getSourceRange();
			IDocument fullSource= new Document(testSuite.getBuffer().getContents());
			String originalContent= fullSource.get(range.getOffset(), range.getLength());
			StringBuilder source= new StringBuilder(originalContent);
			TestSuiteClassListRange classRange= getTestSuiteClassListRange(source.toString());
			if (classRange != null) {
				monitor.worked(1);
				//					String updatableCode= source.substring(start,end+NewTestSuiteCreationWizardPage.endMarker.length());
				source.replace(classRange.getStart(), classRange.getEnd(), getUpdatableString(selectedTestCases));
				fullSource.replace(range.getOffset(), range.getLength(), source.toString());
				monitor.worked(1);
				String formattedContent= JUnitStubUtility.formatCompilationUnit(testSuite.getJavaProject(), fullSource.get(), testSuite.findRecommendedLineSeparator());
				//buf.replace(range.getOffset(), range.getLength(), formattedContent);
				IBuffer buf= testSuite.getBuffer();
				buf.replace(0, buf.getLength(), formattedContent);
				monitor.worked(1);
				testSuite.save(new SubProgressMonitor(monitor, 1), true);
				monitor.worked(1);
			}

		} catch (BadLocationException e) {
			Assert.isTrue(false, "Should never happen"); //$NON-NLS-1$
		} finally {
			monitor.done();
		}
	}

	public static TestSuiteClassListRange getTestSuiteClassListRange(String source) {
		int start= source.indexOf(NewTestSuiteWizardPage.NON_COMMENT_START_MARKER);
		if (start <= -1)
			return null;
		start = source.lastIndexOf(NewTestSuiteWizardPage.COMMENT_START, start);
		if (start <= -1)
			return null;
		int end= source.indexOf(NewTestSuiteWizardPage.NON_COMMENT_END_MARKER, start);
		if (end <= -1)
			return null;
		end += NewTestSuiteWizardPage.NON_COMMENT_END_MARKER.length();
		return new TestSuiteClassListRange(start, end);
	}



	/*
	 * Returns the new code to be included in a new suite() or which replaces old code in an existing suite().
	 */
	public static String getUpdatableString(Object[] selectedClasses) {
		StringBuilder suite= new StringBuilder();
		suite.append(NewTestSuiteWizardPage.START_MARKER+"\n"); //$NON-NLS-1$
		for (Object selectedClasse : selectedClasses) {
			if (selectedClasse instanceof IType) {
				IType testType= (IType) selectedClasse;
				IMethod suiteMethod= testType.getMethod("suite", new String[] {}); //$NON-NLS-1$
				if (!suiteMethod.exists()) {
					suite.append("suite.addTestSuite("+testType.getElementName()+".class);"); //$NON-NLS-1$ //$NON-NLS-2$
				} else {
					suite.append("suite.addTest("+testType.getElementName()+".suite());"); //$NON-NLS-1$ //$NON-NLS-2$
				}
			}
		}
		suite.append("\n"+NewTestSuiteWizardPage.END_MARKER); //$NON-NLS-1$
		return suite.toString();
	}

	/*
	 * Returns the new test suite annotations which replace old annotations in the existing suite
	 */
	public static String getUpdatableAnnotations(Object[] selectedClasses) {
		StringBuilder buffer = new StringBuilder("@SuiteClasses({"); //$NON-NLS-1$
		for (int i= 0; i < selectedClasses.length; i++) {
			if (selectedClasses[i] instanceof IType) {
				IType testType= (IType) selectedClasses[i];
				buffer.append(testType.getElementName());
				buffer.append(".class"); //$NON-NLS-1$
				if (i < selectedClasses.length - 1)
					buffer.append(',');
			}
		}
		buffer.append("})"); //$NON-NLS-1$
		buffer.append("\n"); //$NON-NLS-1$
		return buffer.toString();
	}

	/*
	 * Returns the new test suite annotations which replace old annotations in the existing suite
	 */
	public static String getUpdatableJUnit5Annotations(Object[] selectedClasses) {
		StringBuilder buffer = new StringBuilder("@SelectClasses({"); //$NON-NLS-1$
		for (int i= 0; i < selectedClasses.length; i++) {
			if (selectedClasses[i] instanceof IType) {
				IType testType= (IType) selectedClasses[i];
				buffer.append(testType.getElementName());
				buffer.append(".class"); //$NON-NLS-1$
				if (i < selectedClasses.length - 1)
					buffer.append(',');
			}
		}
		buffer.append("})"); //$NON-NLS-1$
		buffer.append("\n"); //$NON-NLS-1$
		return buffer.toString();
	}

	public static boolean checkValidateEditStatus(ICompilationUnit testSuiteCu, Shell shell){
		IStatus status= validateModifiesFiles(getTestSuiteFile(testSuiteCu));
		if (status.isOK())
			return true;
		ErrorDialog.openError(shell, WizardMessages.UpdateTestSuite_update, WizardMessages.UpdateTestSuite_could_not_update, status);
		return false;
	}

	private static IFile getTestSuiteFile(ICompilationUnit testSuiteCu) {
		return (IFile) testSuiteCu.getResource();
	}

	private static IStatus validateModifiesFiles(IFile fileToModify) {
		IFile[] filesToModify= {fileToModify};
		IStatus status= Resources.checkInSync(filesToModify);
		if (! status.isOK())
			return status;
		status= Resources.makeCommittable(filesToModify, null);
		if (! status.isOK())
			return status;
		return new JUnitStatus();
	}

	public IRunnableWithProgress getRunnable() {
		return monitor -> {
			if (monitor == null) {
				monitor= new NullProgressMonitor();
			}
			if (! checkValidateEditStatus(fTestSuite, fShell))
				return;
			try {
				if (fIsJunit4)
					updateTestCasesInJunit4Suite(monitor, fTestSuite, fSuiteClasses, fSelectedTestCases);
				else if (fIsJunit5)
					updateTestCasesInJunit5Suite(monitor, fTestSuite, fSuiteClasses, fSelectedTestCases);
				else
					updateTestCasesInSuite(monitor, fTestSuite, fSuiteMethod, fSelectedTestCases);
			} catch (JavaModelException e) {
				ExceptionHandler.handle(e, fShell, WizardMessages.UpdateTestSuite_update, WizardMessages.UpdateTestSuite_error);
			}
		};
	}

	private void cannotUpdateSuiteError() {
		MessageDialog.openError(fShell, WizardMessages.UpdateAllTests_cannotUpdate_errorDialog_title,
			Messages.format(WizardMessages.UpdateAllTests_cannotUpdate_errorDialog_message, new String[] {NewTestSuiteWizardPage.START_MARKER, NewTestSuiteWizardPage.END_MARKER}));

	}

	private void noSuiteError() {
		if (fIsJunit4) {
			MessageDialog.openError(fShell, WizardMessages.UpdateAllTests_cannotFind_annotation_errorDialog_title, WizardMessages.UpdateAllTests_cannotFind_annotation_errorDialog_message);
		} else if (fIsJunit5) {
			MessageDialog.openError(fShell, WizardMessages.UpdateAllTests_cannotFind_annotation5_errorDialog_title, WizardMessages.UpdateAllTests_cannotFind_annotation5_errorDialog_message);
		} else {
			MessageDialog.openError(fShell, WizardMessages.UpdateAllTests_cannotFind_errorDialog_title, WizardMessages.UpdateAllTests_cannotFind_errorDialog_message);
		}
	}
}
