/*******************************************************************************
 * Copyright (c) 2007, 2020 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.pde.api.tools.util.tests;

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

import java.io.File;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.Javadoc;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.TagElement;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.ltk.core.refactoring.CompositeChange;
import org.eclipse.ltk.core.refactoring.TextFileChange;
import org.eclipse.pde.api.tools.internal.ApiDescriptionProcessor;
import org.eclipse.pde.api.tools.internal.util.Signatures;
import org.eclipse.pde.api.tools.model.tests.TestSuiteHelper;
import org.eclipse.pde.api.tools.tests.AbstractApiTest;
import org.eclipse.pde.api.tools.tests.util.FileUtils;
import org.eclipse.pde.api.tools.tests.util.ProjectUtils;
import org.eclipse.pde.api.tools.ui.internal.ApiUIPlugin;
import org.eclipse.pde.api.tools.ui.internal.wizards.ApiToolingSetupRefactoring;
import org.eclipse.pde.api.tools.ui.internal.wizards.WizardMessages;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.TextEdit;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

/**
 * This class tests the {@link ApiDescriptionProcessor}
 *
 * @since 1.0.0
 */
public class ApiDescriptionProcessorTests extends AbstractApiTest {

	/**
	 * The source directory for the javadoc updating test source
	 */
	private static String JAVADOC_SRC_DIR = null;
	static {
		JAVADOC_SRC_DIR = TestSuiteHelper.getPluginDirectoryPath().append("test-source").append("javadoc").toOSString(); //$NON-NLS-1$ //$NON-NLS-2$
	}

	/**
	 * Visitor used to inspect the 'after' class files once they have had tags
	 * added to ensure the tags as specified in the component.xml file were
	 * added.
	 */
	static class ChangeVisitor extends ASTVisitor {
		String type, membername, signature, innertypename;

		String[] expectedtags = null;
		boolean processed = false;

		public ChangeVisitor(String type, String innertypename, String membername, String signature, String[] expectedtags) {
			this.type = type;
			this.membername = membername;
			this.signature = signature;
			this.expectedtags = expectedtags;
			this.innertypename = innertypename;
		}

		@Override
		public boolean visit(FieldDeclaration node) {
			if (signature != null) {
				// means we are looking for a method
				return false;
			}
			@SuppressWarnings("unchecked")
			List<VariableDeclarationFragment> fields = node.fragments();
			VariableDeclarationFragment fragment = null;
			for (Iterator<VariableDeclarationFragment> iter = fields.iterator(); iter.hasNext();) {
				fragment = iter.next();
				if (fragment.getName().getFullyQualifiedName().equals(membername)) {
					Javadoc docnode = node.getJavadoc();
					assertNotNull("the field: " + membername + " must have a javadoc node", docnode); //$NON-NLS-1$ //$NON-NLS-2$
					assertTrue("the field: " + membername + " should contain all of the tags: " + getStringValue(expectedtags), containsAllTags(docnode)); //$NON-NLS-1$ //$NON-NLS-2$
					processed = true;
				}
			}
			return false;
		}

		@Override
		public boolean visit(MethodDeclaration node) {
			if (node.getName().getFullyQualifiedName().equals(membername)) {
				String sig = Signatures.getMethodSignatureFromNode(node, true);
				if (signature.equals(sig)) {
					Javadoc docnode = node.getJavadoc();
					assertNotNull("the method: " + membername + " [" + signature + "] must have a javadoc node", docnode); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
					assertTrue("the method: " + membername + " [" + signature + "] should contain all of the tags: " + getStringValue(expectedtags), containsAllTags(docnode)); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
					processed = true;
				}
			}
			return false;
		}

		@Override
		public boolean visit(TypeDeclaration node) {
			if (membername == null && signature == null) {
				String name = node.getName().getFullyQualifiedName();
				if ((innertypename == null && name.equals(type)) || name.equals(innertypename)) {
					Javadoc docnode = node.getJavadoc();
					assertNotNull("the type: " + name + " must have a javadoc node", docnode); //$NON-NLS-1$ //$NON-NLS-2$
					assertTrue("the type: " + name + " should contain all of the tags: " + getStringValue(expectedtags), containsAllTags(docnode)); //$NON-NLS-1$ //$NON-NLS-2$
					processed = true;
				}
			}
			return membername != null || innertypename != null;
		}

		/**
		 * Determines if the listing of doc tags from the declaration contains
		 * all of the expected tags
		 *
		 * @return true if the tag list contains all of the expected tags, false
		 *         otherwise
		 */
		private boolean containsAllTags(Javadoc docnode) {
			@SuppressWarnings("unchecked")
			List<TagElement> tags = docnode.tags();
			boolean allfound = true;
			TagElement element = null;
			for (String expectedtag : expectedtags) {
				boolean contained = false;
				for (TagElement tag : tags) {
					element = tag;
					if (expectedtag.equals(element.getTagName())) {
						contained = true;
					}
				}
				allfound &= contained;
			}
			return allfound;
		}

		private String getStringValue(String[] tags) {
			StringBuilder buffer = new StringBuilder();
			for (int i = 0, max = tags.length; i < max; i++) {
				if (i > 0) {
					buffer.append(',');
				}
				buffer.append(tags[i]);
			}
			return String.valueOf(buffer);
		}
	}

	private static IPath ROOT_PATH = TestSuiteHelper.getPluginDirectoryPath().append("test-source").append("javadoc"); //$NON-NLS-1$ //$NON-NLS-2$
	private static File componentxml = new File(ROOT_PATH.append("component.xml").toOSString()); //$NON-NLS-1$

	@Override
	@Before
	public void setUp() throws Exception {
		super.setUp();
		createProject(TESTING_PROJECT_NAME, null);
		IJavaProject project = getTestingJavaProject(TESTING_PROJECT_NAME);
		assertNotNull("The java project must have been created", project); //$NON-NLS-1$
		IPackageFragmentRoot srcroot = ProjectUtils.addSourceContainer(project, ProjectUtils.SRC_FOLDER);
		assertNotNull("the src root must have been created", srcroot); //$NON-NLS-1$

		File src = new File(JAVADOC_SRC_DIR);
		assertTrue("the source dir must exist", src.exists()); //$NON-NLS-1$
		assertTrue("the source dir must be a directory", src.isDirectory()); //$NON-NLS-1$
		assertNotNull("the srcroot for the test java project must not be null", srcroot); //$NON-NLS-1$
		FileUtils.importFilesFromDirectory(src, srcroot.getPath().append("javadoc")); //$NON-NLS-1$

		ApiToolingSetupRefactoring refactoring = new ApiToolingSetupRefactoring();
		CompositeChange change = new CompositeChange("Test"); //$NON-NLS-1$
		createTagChanges(change, project, componentxml);
		refactoring.addChange(change);
		performRefactoring(refactoring);
	}

	@Override
	@After
	public void tearDown() throws Exception {
		deleteProject(TESTING_PROJECT_NAME);
		super.tearDown();
	}

	/**
	 * Tests that the component.xml file is parsed if it is provided in one of
	 * the doc'd three forms:
	 * <ol>
	 * <li>a directory containing the component.xml file</li>
	 * <li>a component.xml file</li>
	 * <li>a jar file containing the component.xml file</li>
	 * </ol>
	 */
	@Test
	public void testSerializeComponentXml() {
		String xml = ApiDescriptionProcessor.serializeComponentXml(new File(ROOT_PATH.toOSString()));
		assertNotNull("The component xml file must exist and be parsable from a root directory", xml); //$NON-NLS-1$
		xml = ApiDescriptionProcessor.serializeComponentXml(componentxml);
		assertNotNull("The component xml file must exist and be parsable from a component.xml file", xml); //$NON-NLS-1$
		xml = ApiDescriptionProcessor.serializeComponentXml(new File(ROOT_PATH.append("component.jar").toOSString())); //$NON-NLS-1$
		assertNotNull("The component xml file must exist and be parsable from a jar file", xml); //$NON-NLS-1$
	}

	/**
	 * Creates all of the text edit changes collected from the processor. The
	 * collected edits are arranged as multi-edits for the one file that they
	 * belong to
	 */
	private void createTagChanges(CompositeChange projectchange, IJavaProject project, File cxml) {
		try {
			HashMap<IFile, Set<TextEdit>> map = new HashMap<>();
			ApiDescriptionProcessor.collectTagUpdates(project, cxml, map);
			IFile file = null;
			TextFileChange change = null;
			MultiTextEdit multiedit = null;
			Set<TextEdit> alledits = null;
			TextEdit edit = null;
			for (Entry<IFile, Set<TextEdit>> entry : map.entrySet()) {
				file = entry.getKey();
				change = new TextFileChange(MessageFormat.format(WizardMessages.JavadocTagRefactoring_2, new Object[] { file.getName() }), file);
				multiedit = new MultiTextEdit();
				change.setEdit(multiedit);
				alledits = entry.getValue();
				if (alledits != null) {
					for (Iterator<TextEdit> iter2 = alledits.iterator(); iter2.hasNext();) {
						edit = iter2.next();
						multiedit.addChild(edit);
					}
				}
				if (change != null) {
					projectchange.add(change);
				}
			}
		} catch (CoreException e) {
			ApiUIPlugin.log(e);
		}
	}

	/**
	 * Processes the change from original to updated
	 *
	 * @param typename
	 *            the name of the type to query
	 * @param membername
	 *            the name of the member
	 * @param signature
	 *            the signature of the member
	 * @param expectedtags
	 *            the tags we expect to see
	 */
	protected void processUpdatedItem(String typename, String innertypename, String membername, String signature, String[] expectedtags) throws Exception {
		try {
			IJavaProject project = getTestingJavaProject(TESTING_PROJECT_NAME);
			IType type = project.findType("javadoc", typename); //$NON-NLS-1$
			assertNotNull("the type for javadoc." + typename + " must exist", type); //$NON-NLS-1$ //$NON-NLS-2$
			ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
			parser.setSource(type.getCompilationUnit());
			CompilationUnit cunit = (CompilationUnit) parser.createAST(new NullProgressMonitor());
			ChangeVisitor visitor = new ChangeVisitor(typename, innertypename, membername, signature, expectedtags);
			cunit.accept(visitor);
			assertTrue("the specified node should have been processed", visitor.processed); //$NON-NLS-1$
		} catch (JavaModelException jme) {
			fail("the test class javadoc." + typename + " had problems loading"); //$NON-NLS-1$ //$NON-NLS-2$
		}
	}

	/**
	 * Tests the addition of a javadoc tag to a class. Uses
	 * <code>JavadocTestClass1</code>
	 */
	@Test
	public void testProcessClassAddition() throws Exception {
		processUpdatedItem("JavadocTestClass1", null, null, null, new String[] { "@noinstantiate" }); //$NON-NLS-1$ //$NON-NLS-2$
	}

	/**
	 * Tests the addition of a javadoc tag to a class that does not have a
	 * javadoc section yet Uses <code>JavadocTestClass7</code>
	 */
	@Test
	public void testProcessClassAdditionNoDocElement() throws Exception {
		processUpdatedItem("JavadocTestClass7", null, null, null, new String[] { //$NON-NLS-1$
		"@noextend", "@noinstantiate" }); //$NON-NLS-1$ //$NON-NLS-2$
	}

	/**
	 * Tests the addition of a javadoc tag to a method that does not have a
	 * javadoc section yet Uses <code>JavadocTestClass7</code>
	 */
	@Test
	public void testProcessMethodAdditionNoDocElement() throws Exception {
		processUpdatedItem("JavadocTestClass7", null, "m1", "()V", new String[] { "@nooverride" }); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
	}

	/**
	 * Tests the addition of a javadoc tag to a field that does not have a
	 * javadoc section yet Uses <code>JavadocTestClass7</code>
	 */
	@Test
	public void testProcessFieldAdditionNoDocElement() throws Exception {
		processUpdatedItem("JavadocTestClass7", null, "f1", null, new String[] { "@noreference" }); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
	}

	/**
	 * Tests the addition of a javadoc tag to an inner class. Uses
	 * <code>JavadocTestClass2</code>
	 */
	@Test
	public void testProcessInnerClassAddition() throws Exception {
		processUpdatedItem("JavadocTestClass2", "Inner", null, null, new String[] { "@noinstantiate" }); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
	}

	/**
	 * Tests the addition of a javadoc tags to methods. Uses
	 * <code>JavadocTestClass3</code>
	 */
	@Test
	public void testProcessMethodAddition() throws Exception {
		processUpdatedItem("JavadocTestClass3", null, "m1", "()V", new String[] { "@nooverride" }); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
		processUpdatedItem("JavadocTestClass3", null, "m2", "()V", new String[] { "@noreference" }); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
	}

	/**
	 * Tests the addition of a javadoc tags to fields. Uses
	 * <code>JavadocTestClass4</code>
	 */
	@Test
	public void testProcessFieldAddition() throws Exception {
		processUpdatedItem("JavadocTestClass4", null, "f1", null, new String[] { "@noreference" }); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		processUpdatedItem("JavadocTestClass4", null, "f2", null, new String[] { "@noreference" }); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
	}

	/**
	 * Tests the addition of a javadoc tags to methods in inner classes. Uses
	 * <code>JavadocTestClass6</code>
	 */
	@Test
	public void testProcessInnerMethodAddition() throws Exception {
		processUpdatedItem("JavadocTestClass6", "Inner2", "m1", "()V", new String[] { "@nooverride" }); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
		processUpdatedItem("JavadocTestClass6", "Inner2", "m2", "()V", new String[] { "@noreference" }); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
	}

	/**
	 * Tests the addition of a javadoc tags to fields in inner classes. Uses
	 * <code>JavadocTestClass5</code>
	 */
	@Test
	public void testProcessInnerFieldAddition() throws Exception {
		processUpdatedItem("JavadocTestClass5", "Inner2", "f1", null, new String[] { "@noreference" }); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
		processUpdatedItem("JavadocTestClass5", "Inner2", "f2", null, new String[] { "@noreference" }); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
	}

	/**
	 * Tests that the old 'subclass' attribute is correctly resolved to
	 * 'noextend' restriction for tag updating. Tests the case of bug 210786
	 * (https://bugs.eclipse.org/bugs/show_bug.cgi?id=210786) Uses
	 * <code>JavadocTestClass8</code>
	 */
	@Test
	public void testProcessSubclassAttribute() throws Exception {
		processUpdatedItem("JavadocTestClass8", null, null, null, new String[] { "@noextend" }); //$NON-NLS-1$ //$NON-NLS-2$
	}
}
