/*******************************************************************************
 * 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.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.util.Iterator;
import java.util.List;

import org.eclipse.core.filebuffers.FileBuffers;
import org.eclipse.core.filebuffers.ITextFileBuffer;
import org.eclipse.core.filebuffers.ITextFileBufferManager;
import org.eclipse.core.filebuffers.LocationKind;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.JavaCore;
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.BodyDeclaration;
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.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ListRewrite;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.pde.api.tools.builder.tests.ApiTestingEnvironment;
import org.eclipse.pde.api.tools.internal.ApiBaselineManager;
import org.eclipse.pde.api.tools.internal.model.ApiModelFactory;
import org.eclipse.pde.api.tools.internal.provisional.ApiPlugin;
import org.eclipse.pde.api.tools.internal.provisional.Factory;
import org.eclipse.pde.api.tools.internal.provisional.IApiAnnotations;
import org.eclipse.pde.api.tools.internal.provisional.IApiBaselineManager;
import org.eclipse.pde.api.tools.internal.provisional.IApiDescription;
import org.eclipse.pde.api.tools.internal.provisional.RestrictionModifiers;
import org.eclipse.pde.api.tools.internal.provisional.VisibilityModifiers;
import org.eclipse.pde.api.tools.internal.provisional.model.IApiBaseline;
import org.eclipse.pde.api.tools.internal.provisional.model.IApiComponent;
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.core.project.IBundleClasspathEntry;
import org.eclipse.pde.core.project.IBundleProjectDescription;
import org.eclipse.pde.core.project.IBundleProjectService;
import org.eclipse.text.edits.MalformedTreeException;
import org.eclipse.text.edits.TextEdit;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

/**
 * Tests the {@link ApiBaselineManager} without the framework running
 */
@SuppressWarnings("unchecked")
public class ApiBaselineManagerTests extends AbstractApiTest {

	static final String THREE = "three"; //$NON-NLS-1$
	static final String TESTDEFAULT = "testdefault"; //$NON-NLS-1$
	static final String ADDTEST = "addtest"; //$NON-NLS-1$

	static class SourceChangeVisitor extends ASTVisitor {
		String name = null;
		String signature = null;
		String tagname = null;
		ASTRewrite rewrite = null;
		boolean remove = false;

		public SourceChangeVisitor(String name, String signature, String tagname, boolean remove, ASTRewrite rewrite) {
			this.name = name;
			this.signature = signature;
			this.tagname = tagname;
			this.rewrite = rewrite;
			this.remove = remove;
		}

		@Override
		public boolean visit(FieldDeclaration node) {
			if (signature != null) {
				return false;
			}
			List<VariableDeclarationFragment> fields = node.fragments();
			VariableDeclarationFragment fragment = null;
			for (Iterator<VariableDeclarationFragment> iter = fields.iterator(); iter.hasNext();) {
				fragment = iter.next();
				if (fragment.getName().getFullyQualifiedName().equals(name)) {
					break;
				}
			}
			if (fragment != null) {
				updateTag(node);
			}
			return false;
		}

		@Override
		public boolean visit(MethodDeclaration node) {
			if (name.equals(node.getName().getFullyQualifiedName())) {
				if (signature.equals(Signatures.getMethodSignatureFromNode(node, true))) {
					updateTag(node);
				}
			}
			return false;
		}

		@Override
		public boolean visit(TypeDeclaration node) {
			if (name.equals(node.getName().getFullyQualifiedName())) {
				updateTag(node);
				return false;
			}
			return true;
		}

		/**
		 * Updates a javadoc tag, by either adding a new one or removing an
		 * existing one
		 */
		private void updateTag(BodyDeclaration body) {
			Javadoc docnode = body.getJavadoc();
			AST ast = body.getAST();
			if (docnode == null) {
				docnode = ast.newJavadoc();
				rewrite.set(body, body.getJavadocProperty(), docnode, null);
			}
			ListRewrite lrewrite = rewrite.getListRewrite(docnode, Javadoc.TAGS_PROPERTY);
			if (remove) {
				List<TagElement> tags = (List<TagElement>) docnode.getStructuralProperty(Javadoc.TAGS_PROPERTY);
				if (tags != null) {
					TagElement tag = null;
					for (TagElement tag2 : tags) {
						tag = tag2;
						if (tagname.equals(tag.getTagName())) {
							lrewrite.remove(tag, null);
						}
					}
				}
			} else {
				TagElement newtag = ast.newTagElement();
				newtag.setTagName(tagname);
				lrewrite.insertLast(newtag, null);
			}
		}
	}

	private final IPath SRC_LOC = TestSuiteHelper.getPluginDirectoryPath().append("test-source").append("a").append("b").append("c"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
	private final IPath PLUGIN_LOC = TestSuiteHelper.getPluginDirectoryPath().append("test-plugins"); //$NON-NLS-1$
	private final IApiBaselineManager fPMmanager = ApiPlugin.getDefault().getApiBaselineManager();
	private final String TESTING_PACKAGE = "a.b.c"; //$NON-NLS-1$

	/**
	 * @return the {@link IApiDescription} for the testing project
	 */
	private IApiDescription getTestProjectApiDescription() throws CoreException {
		IApiBaseline baseline = getWorkspaceBaseline();
		assertNotNull("the workspace baseline must exist", baseline); //$NON-NLS-1$
		IApiComponent component = baseline.getApiComponent(TESTING_PLUGIN_PROJECT_NAME);
		if (component != null) {
			return component.getApiDescription();
		}
		return null;
	}

	/**
	 * Creates and returns a test baseline with the given id. Also adds it to
	 * the baseline manager
	 */
	protected IApiBaseline getTestBaseline(String id) {
		IApiBaseline baseline = ApiModelFactory.newApiBaseline(id);
		fPMmanager.addApiBaseline(baseline);
		return baseline;
	}

	/**
	 * Tests trying to get the workspace baseline without the framework running
	 */
	@Test
	public void testGetWorkspaceComponent() {
		IApiBaseline baseline = getWorkspaceBaseline();
		assertNotNull("the workspace baseline must not be null", baseline); //$NON-NLS-1$
	}

	/**
	 * Tests that an API baseline can be added and retrieved successfully
	 */
	@Test
	public void testAddBaseline() {
		IApiBaseline baseline = getTestBaseline(ADDTEST);
		assertNotNull("the test baseline must have been created", baseline); //$NON-NLS-1$
		assertTrue("the testadd baseline must be in the manager", fPMmanager.removeApiBaseline(ADDTEST)); //$NON-NLS-1$
	}

	/**
	 * Tests that an API baseline can be added/removed successfully
	 */
	@Test
	public void testRemoveBaseline() {
		IApiBaseline baseline = getTestBaseline("removetest"); //$NON-NLS-1$
		assertNotNull("the testremove baseline must exist", baseline); //$NON-NLS-1$
		baseline = fPMmanager.getApiBaseline("removetest"); //$NON-NLS-1$
		assertNotNull("the testremove baseline must be in the manager", baseline); //$NON-NLS-1$
		assertTrue("the testremove baseline should have been removed", fPMmanager.removeApiBaseline("removetest")); //$NON-NLS-1$ //$NON-NLS-2$
	}

	/**
	 * Tests that the default baseline can be set/retrieved
	 */
	@Test
	public void testSetDefaultBaseline() {
		try {
			IApiBaseline baseline = getTestBaseline(TESTDEFAULT);
			assertNotNull("the testdefault baseline must exist", baseline); //$NON-NLS-1$
			fPMmanager.setDefaultApiBaseline(TESTDEFAULT);
			baseline = fPMmanager.getDefaultApiBaseline();
			assertNotNull("the default baseline must be the testdefault baseline", baseline); //$NON-NLS-1$
		} finally {
			fPMmanager.removeApiBaseline(TESTDEFAULT);
		}
	}

	/**
	 * Tests that all baselines added to the manager can be retrieved
	 */
	@Test
	public void testGetAllBaselines() {
		try {
			fPMmanager.addApiBaseline(getTestBaseline(ADDTEST));
			fPMmanager.addApiBaseline(getTestBaseline(TESTDEFAULT));
			fPMmanager.addApiBaseline(getTestBaseline(THREE));
			IApiBaseline[] baselines = fPMmanager.getApiBaselines();
			assertEquals("there should be three baselines", 3, baselines.length); //$NON-NLS-1$
		} finally {
			fPMmanager.removeApiBaseline(ADDTEST);
			fPMmanager.removeApiBaseline(TESTDEFAULT);
			fPMmanager.removeApiBaseline(THREE);
		}
	}

	/**
	 * Tests that all of the baselines have been removed
	 */
	@Test
	public void testCleanUpMmanager() {
		try {
			fPMmanager.addApiBaseline(getTestBaseline(ADDTEST));
			fPMmanager.addApiBaseline(getTestBaseline(TESTDEFAULT));
			fPMmanager.addApiBaseline(getTestBaseline(THREE));
			IApiBaseline[] baselines = fPMmanager.getApiBaselines();
			assertEquals("there should be three baselines", 3, baselines.length); //$NON-NLS-1$
			assertTrue("the testadd baseline should have been removed", fPMmanager.removeApiBaseline(ADDTEST)); //$NON-NLS-1$
			assertTrue("the testdefault baseline should have been removed", fPMmanager.removeApiBaseline(TESTDEFAULT)); //$NON-NLS-1$
			assertTrue("the three baseline should have been removed", fPMmanager.removeApiBaseline(THREE)); //$NON-NLS-1$
			assertEquals("There sould be no more baselines", 0, fPMmanager.getApiBaselines().length); //$NON-NLS-1$
		} finally {
			fPMmanager.removeApiBaseline(ADDTEST);
			fPMmanager.removeApiBaseline(TESTDEFAULT);
			fPMmanager.removeApiBaseline(THREE);
		}
	}

	/**
	 * Adds the given source to the given package in the given fragment root
	 *
	 * @param root the root to add the source to
	 * @param packagename the name of the package e.g. a.b.c
	 * @param sourcename the name of the source file without an extension e.g.
	 *            TestClass1
	 */
	public void assertTestSource(IPackageFragmentRoot root, String packagename, String sourcename) {
		IPackageFragment fragment = root.getPackageFragment(packagename);
		FileUtils.importFileFromDirectory(SRC_LOC.append(sourcename + ".java").toFile(), fragment.getPath()); //$NON-NLS-1$
	}

	/**
	 * Adds the package with the given name to the given package fragment root
	 *
	 * @param project
	 *            the project to add the package to
	 * @param srcroot
	 *            the absolute path to the package fragment root to add the new
	 *            package to
	 * @param packagename
	 *            the name of the new package
	 * @return the new {@link IPackageFragment} or <code>null</code>
	 */
	public IPackageFragment assertTestPackage(IJavaProject project, IPath srcroot, String packagename) throws JavaModelException {
		IPackageFragment fragment = null;
		IPackageFragmentRoot root = project.findPackageFragmentRoot(srcroot);
		assertNotNull("the 'src' package fragment root must exist", root); //$NON-NLS-1$
		fragment = root.createPackageFragment(packagename, true, new NullProgressMonitor());
		assertNotNull("the new package '" + packagename + "' should have been created", fragment); //$NON-NLS-1$ //$NON-NLS-2$
		return fragment;
	}

	/**
	 * Adds a test library with the given name to the test projects' class path.
	 * The library is imported from the {@link #PLUGIN_LOC} location.
	 *
	 * @param project the project to add the library classpath entry to
	 * @param folderpath the path in the project where the library should be
	 *            imported to
	 * @param libname the name of the library
	 */
	public IFolder assertTestLibrary(IJavaProject project, IPath folderpath, String libname) throws CoreException {
		IFolder folder = null;
		// import library
		folder = project.getProject().getFolder(folderpath);
		if (!folder.exists()) {
			folder.create(false, true, null);
		}
		FileUtils.importFileFromDirectory(PLUGIN_LOC.append(libname).toFile(), folder.getFullPath());
		IPath libPath = folder.getFullPath().append(libname);

		// add to manifest bundle classpath
		ProjectUtils.addBundleClasspathEntry(project.getProject(), ProjectUtils.getBundleProjectService().newBundleClasspathEntry(null, null, libPath.removeFirstSegments(1)));
		waitForAutoBuild();
		return folder;
	}

	/**
	 * Asserts if the given restriction is on the specified source
	 */
	public void assertSourceResctriction(String packagename, String sourcename, int restriction) throws CoreException {
		IApiDescription desc = getTestProjectApiDescription();
		assertNotNull("the testing project api description must exist", desc); //$NON-NLS-1$
		IApiAnnotations annot = desc.resolveAnnotations(Factory.typeDescriptor(packagename + "." + sourcename)); //$NON-NLS-1$
		assertNotNull("the annotations for " + packagename + "." + sourcename + " cannot be null", annot); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		assertEquals("there must be a noinstantiate setting for TestClass1", annot.getRestrictions(), restriction); //$NON-NLS-1$
	}

	/**
	 * Tests that closing an API aware project causes the workspace description
	 * to be updated
	 */
	@Test
	public void testWPUpdateProjectClosed() throws Exception {
		IJavaProject project = getTestingProject();
		assertNotNull("The testing project must exist", project); //$NON-NLS-1$
		assertNotNull("the workspace baseline must not be null", getWorkspaceBaseline()); //$NON-NLS-1$
		IApiComponent component = getWorkspaceBaseline().getApiComponent(TESTING_PLUGIN_PROJECT_NAME);
		assertNotNull("the change project api component must exist in the workspace baseline", component); //$NON-NLS-1$
		project.getProject().close(new NullProgressMonitor());
		component = getWorkspaceBaseline().getApiComponent(TESTING_PLUGIN_PROJECT_NAME);
		assertNull("the test project api component should no longer exist in the workspace baseline", component); //$NON-NLS-1$
	}

	/**
	 * Tests that opening an API aware project causes the workspace description
	 * to be updated
	 */
	@Test
	public void testWPUpdateProjectOpen() throws Exception {
		IJavaProject project = getTestingProject();
		assertNotNull("The testing project must exist", project); //$NON-NLS-1$
		if (project.getProject().isAccessible()) {
			project.getProject().close(new NullProgressMonitor());
		}
		project.getProject().open(new NullProgressMonitor());
		IApiBaseline baseline = getWorkspaceBaseline();
		assertNotNull("the workspace baseline must not be null", baseline); //$NON-NLS-1$
		IApiComponent component = baseline.getApiComponent(TESTING_PLUGIN_PROJECT_NAME);
		assertNotNull("the test project api component must exist in the workspace baseline", component); //$NON-NLS-1$
	}

	/**
	 * Tests that adding a source file to an API aware project causes the
	 * workspace description to be updated This test adds
	 * <code>a.b.c.TestClass1</code> to the plug-in project
	 */
	@Test
	public void testWPUpdateSourceAdded() throws Exception {
		IJavaProject project = getTestingProject();
		assertNotNull("The testing project must exist", project); //$NON-NLS-1$
		IPackageFragmentRoot root = project.findPackageFragmentRoot(IPath.fromOSString(project.getElementName()).append(ProjectUtils.SRC_FOLDER).makeAbsolute().makeAbsolute());
		assertNotNull("the 'src' package fragment root must exist", root); //$NON-NLS-1$
		assertTestSource(root, TESTING_PACKAGE, "TestClass1"); //$NON-NLS-1$
		assertSourceResctriction(TESTING_PACKAGE, "TestClass1", RestrictionModifiers.NO_INSTANTIATE); //$NON-NLS-1$
	}

	/**
	 * Tests that removing a source file from an API aware project causes the
	 * workspace description to be updated
	 */
	@Test
	public void testWPUpdateSourceRemoved() throws Exception {
		IJavaProject project = getTestingProject();
		assertNotNull("The testing project must exist", project); //$NON-NLS-1$
		IPackageFragmentRoot root = project.findPackageFragmentRoot(IPath.fromOSString(project.getElementName()).append(ProjectUtils.SRC_FOLDER).makeAbsolute());
		assertNotNull("the 'src' package fragment root must exist", root); //$NON-NLS-1$
		assertTestSource(root, TESTING_PACKAGE, "TestClass1"); //$NON-NLS-1$
		IJavaElement element = project.findElement(IPath.fromOSString("a/b/c/TestClass1.java")); //$NON-NLS-1$
		assertNotNull("the class a.b.c.TestClass1 must exist in the project", element); //$NON-NLS-1$
		element.getResource().delete(true, new NullProgressMonitor());
		IApiDescription desc = getTestProjectApiDescription();
		assertNotNull("the testing project api description must exist", desc); //$NON-NLS-1$
		IApiAnnotations annot = desc.resolveAnnotations(Factory.typeDescriptor("a.b.c.TestClass1")); //$NON-NLS-1$
		assertNull("the annotations for a.b.c.TestClass1 should no longer be present", annot); //$NON-NLS-1$
	}

	/**
	 * Adds the specified tag to the source member defined by the member name
	 * and signature
	 */
	private void updateTagInSource(ICompilationUnit unit, String membername, String signature, String tagname, boolean remove) throws CoreException, MalformedTreeException, BadLocationException {
		ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
		parser.setSource(unit);
		CompilationUnit cunit = (CompilationUnit) parser.createAST(new NullProgressMonitor());
		assertNotNull("the ast compilation unit cannot be null", cunit); //$NON-NLS-1$
		cunit.recordModifications();
		ASTRewrite rewrite = ASTRewrite.create(cunit.getAST());
		cunit.accept(new SourceChangeVisitor(membername, signature, tagname, remove, rewrite));
		ITextFileBufferManager bm = FileBuffers.getTextFileBufferManager();
		IPath path = cunit.getJavaElement().getPath();
		try {
			bm.connect(path, LocationKind.IFILE, null);
			ITextFileBuffer tfb = bm.getTextFileBuffer(path, LocationKind.IFILE);
			IDocument document = tfb.getDocument();
			TextEdit edits = rewrite.rewriteAST(document, null);
			edits.apply(document);
			tfb.commit(new NullProgressMonitor(), true);
		} finally {
			bm.disconnect(path, LocationKind.IFILE, null);
		}
	}

	/**
	 * Tests that making Javadoc changes to the source file TestClass2 cause the
	 * workspace baseline to be updated.
	 *
	 * This test adds a @noinstantiate tag to the source file TestClass2
	 */
	@Test
	public void testWPUpdateSourceTypeChanged() throws Exception {
		IJavaProject project = getTestingProject();
		assertNotNull("The testing project must exist", project); //$NON-NLS-1$
		IPackageFragmentRoot root = project.findPackageFragmentRoot(IPath.fromOSString(project.getElementName()).append(ProjectUtils.SRC_FOLDER).makeAbsolute());
		assertNotNull("the 'src' package fragment root must exist", root); //$NON-NLS-1$
		IPackageFragment fragment = root.getPackageFragment("a.b.c"); //$NON-NLS-1$
		FileUtils.importFileFromDirectory(SRC_LOC.append("TestClass2.java").toFile(), fragment.getPath()); //$NON-NLS-1$
		ICompilationUnit element = (ICompilationUnit) project.findElement(IPath.fromOSString("a/b/c/TestClass2.java")); //$NON-NLS-1$
		assertNotNull("TestClass2 must exist in the test project", element); //$NON-NLS-1$
		updateTagInSource(element, "TestClass2", null, "@noinstantiate", false); //$NON-NLS-1$ //$NON-NLS-2$
		IApiDescription desc = getTestProjectApiDescription();
		assertNotNull("the testing project api description must exist", desc); //$NON-NLS-1$
		IApiAnnotations annot = desc.resolveAnnotations(Factory.typeDescriptor("a.b.c.TestClass2")); //$NON-NLS-1$
		assertNotNull("the annotations for a.b.c.TestClass2 cannot be null", annot); //$NON-NLS-1$
		assertNotEquals("there must be a noinstantiate setting for TestClass2", 0, //$NON-NLS-1$
				(annot.getRestrictions() & RestrictionModifiers.NO_INSTANTIATE));
		assertNotEquals("there must be a noextend setting for TestClass2", 0, //$NON-NLS-1$
				(annot.getRestrictions() & RestrictionModifiers.NO_EXTEND));
	}

	/**
	 * Tests that tags updated on an inner type are updated in the workspace
	 * description.
	 *
	 * This test adds a @noinstantiate tag to an inner class in TestClass3
	 */
	@Test
	public void testWPUpdateSourceInnerTypeChanged() throws Exception {
		IJavaProject project = getTestingProject();
		assertNotNull("The testing project must exist", project); //$NON-NLS-1$
		IPackageFragmentRoot root = project.findPackageFragmentRoot(IPath.fromOSString(project.getElementName()).append(ProjectUtils.SRC_FOLDER).makeAbsolute());
		assertNotNull("the 'src' package fragment root must exist", root); //$NON-NLS-1$
		assertTestSource(root, TESTING_PACKAGE, "TestClass3"); //$NON-NLS-1$
		ICompilationUnit element = (ICompilationUnit) project.findElement(IPath.fromOSString("a/b/c/TestClass3.java")); //$NON-NLS-1$
		assertNotNull("TestClass3 must exist in the test project", element); //$NON-NLS-1$
		updateTagInSource(element, "InnerTestClass3", null, "@noinstantiate", false); //$NON-NLS-1$ //$NON-NLS-2$
		IApiDescription desc = getTestProjectApiDescription();
		assertNotNull("the testing project api description must exist", desc); //$NON-NLS-1$
		IApiAnnotations annot = desc.resolveAnnotations(Factory.typeDescriptor("a.b.c.TestClass3$InnerTestClass3")); //$NON-NLS-1$
		assertNotNull("the annotations for a.b.c.TestClass3$InnerTestClass3 cannot be null", annot); //$NON-NLS-1$
		assertEquals("there must not be a noinstantiate setting for TestClass3$InnerTestClass3", 0, //$NON-NLS-1$
				(annot.getRestrictions() & RestrictionModifiers.NO_INSTANTIATE));
		assertEquals("there must not be a noextend setting for TestClass3$InnerTestClass3", 0, //$NON-NLS-1$
				(annot.getRestrictions() & RestrictionModifiers.NO_EXTEND));
	}

	/**
	 * Tests that changing the javadoc for a method updates the workspace
	 * baseline
	 *
	 * This test adds a @noextend tag to the method foo() in TestClass1
	 */
	@Test
	public void testWPUpdateSourceMethodChanged() throws Exception {
		IJavaProject project = getTestingProject();
		assertNotNull("The testing project must exist", project); //$NON-NLS-1$
		IPackageFragmentRoot root = project.findPackageFragmentRoot(IPath.fromOSString(project.getElementName()).append(ProjectUtils.SRC_FOLDER).makeAbsolute());
		assertNotNull("the 'src' package fragment root must exist", root); //$NON-NLS-1$
		assertTestSource(root, TESTING_PACKAGE, "TestClass1"); //$NON-NLS-1$
		ICompilationUnit element = (ICompilationUnit) project.findElement(IPath.fromOSString("a/b/c/TestClass1.java")); //$NON-NLS-1$
		assertNotNull("TestClass1 must exist in the test project", element); //$NON-NLS-1$
		updateTagInSource(element, "foo", "()V", "@nooverride", false); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		IApiDescription desc = getTestProjectApiDescription();
		assertNotNull("the testing project api description must exist", desc); //$NON-NLS-1$
		IApiAnnotations annot = desc.resolveAnnotations(Factory.methodDescriptor("a.b.c.TestClass1", "foo", "()V")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		assertNotNull("the annotations for foo() cannot be null", annot); //$NON-NLS-1$
		assertNotEquals("there must be a nooverride setting for foo()", 0, //$NON-NLS-1$
				(annot.getRestrictions() & RestrictionModifiers.NO_OVERRIDE));
	}

	/**
	 * Tests that changing the javadoc for a field updates the workspace
	 * baseline
	 *
	 * This test adds a @noextend tag to the field 'field' in TestField9
	 */
	@Test
	public void testWPUpdateSourceFieldChanged() throws Exception {
		IJavaProject project = getTestingProject();
		assertNotNull("The testing project must exist", project); //$NON-NLS-1$
		IPackageFragmentRoot root = project.findPackageFragmentRoot(IPath.fromOSString(project.getElementName()).append(ProjectUtils.SRC_FOLDER).makeAbsolute());
		assertNotNull("the 'src' package fragment root must exist", root); //$NON-NLS-1$
		assertTestSource(root, TESTING_PACKAGE, "TestField9"); //$NON-NLS-1$
		ICompilationUnit element = (ICompilationUnit) project.findElement(IPath.fromOSString("a/b/c/TestField9.java")); //$NON-NLS-1$
		assertNotNull("TestField9 must exist in the test project", element); //$NON-NLS-1$
		updateTagInSource(element, "field", null, "@noreference", false); //$NON-NLS-1$ //$NON-NLS-2$
		IApiDescription desc = getTestProjectApiDescription();
		assertNotNull("the testing project api description must exist", desc); //$NON-NLS-1$
		IApiAnnotations annot = desc.resolveAnnotations(Factory.fieldDescriptor("a.b.c.TestField9", "field")); //$NON-NLS-1$ //$NON-NLS-2$
		assertNotNull("the annotations for 'field' cannot be null", annot); //$NON-NLS-1$
		assertNotEquals("there must be a noreference setting for 'field'", 0, //$NON-NLS-1$
				(annot.getRestrictions() & RestrictionModifiers.NO_REFERENCE));
	}

	/**
	 * Tests that removing a tag from a method updates the workspace baseline
	 *
	 * This test removes a @noextend tag to the method foo() in TestClass1
	 */
	@Test
	public void testWPUpdateSourceMethodRemoveTag() throws Exception {
		IJavaProject project = getTestingProject();
		assertNotNull("The testing project must exist", project); //$NON-NLS-1$
		IPackageFragmentRoot root = project.findPackageFragmentRoot(IPath.fromOSString(project.getElementName()).append(ProjectUtils.SRC_FOLDER).makeAbsolute());
		assertNotNull("the 'src' package fragment root must exist", root); //$NON-NLS-1$
		assertTestSource(root, TESTING_PACKAGE, "TestClass1"); //$NON-NLS-1$
		ICompilationUnit element = (ICompilationUnit) project.findElement(IPath.fromOSString("a/b/c/TestClass1.java")); //$NON-NLS-1$
		assertNotNull("TestClass1 must exist in the test project", element); //$NON-NLS-1$
		updateTagInSource(element, "foo", "()V", "@nooverride", true); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		IApiDescription desc = getTestProjectApiDescription();
		assertNotNull("the testing project api description must exist", desc); //$NON-NLS-1$
		IApiAnnotations annot = desc.resolveAnnotations(Factory.methodDescriptor("a.b.c.TestClass1", "foo", "()V")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		assertNotNull("the annotations for foo() cannot be null", annot); //$NON-NLS-1$
		assertEquals("there must be no restrictions for foo()", 0, annot.getRestrictions()); //$NON-NLS-1$
	}

	/**
	 * Tests that removing a tag from a type updates the workspace baseline
	 *
	 * This test removes a @noinstantiate tag to an inner class in TestClass3
	 */
	@Test
	public void testWPUpdateSourceTypeRemoveTag() throws Exception {
		IJavaProject project = getTestingProject();
		assertNotNull("The testing project must exist", project); //$NON-NLS-1$
		IPackageFragmentRoot root = project.findPackageFragmentRoot(IPath.fromOSString(project.getElementName()).append(ProjectUtils.SRC_FOLDER).makeAbsolute());
		assertNotNull("the 'src' package fragment root must exist", root); //$NON-NLS-1$
		assertTestSource(root, TESTING_PACKAGE, "TestClass3"); //$NON-NLS-1$
		ICompilationUnit element = (ICompilationUnit) project.findElement(IPath.fromOSString("a/b/c/TestClass3.java")); //$NON-NLS-1$
		assertNotNull("TestClass3 must exist in the test project", element); //$NON-NLS-1$
		updateTagInSource(element, "InnerTestClass3", null, "@noextend", true); //$NON-NLS-1$ //$NON-NLS-2$
		IApiDescription desc = getTestProjectApiDescription();
		assertNotNull("the testing project api description must exist", desc); //$NON-NLS-1$
		IApiAnnotations annot = desc.resolveAnnotations(Factory.typeDescriptor("a.b.c.TestClass3$InnerTestClass3")); //$NON-NLS-1$
		assertNotNull("the annotations for 'InnerTestClass3' cannot be null", annot); //$NON-NLS-1$
		assertEquals("there must be a no restrictions for 'InnerTestClass3'", 0, //$NON-NLS-1$
				(annot.getRestrictions() & RestrictionModifiers.NO_INSTANTIATE));
	}

	/**
	 * Tests that removing a tag from a field updates the workspace baseline
	 *
	 * This test adds a @noextend tag to the field 'field' in TestField9
	 */
	@Test
	public void testWPUpdateSourceFieldRemoveTag() throws Exception {
		IJavaProject project = getTestingProject();
		assertNotNull("The testing project must exist", project); //$NON-NLS-1$
		IPackageFragmentRoot root = project.findPackageFragmentRoot(IPath.fromOSString(project.getElementName()).append(ProjectUtils.SRC_FOLDER).makeAbsolute());
		assertNotNull("the 'src' package fragment root must exist", root); //$NON-NLS-1$
		assertTestSource(root, TESTING_PACKAGE, "TestField9"); //$NON-NLS-1$
		ICompilationUnit element = (ICompilationUnit) project.findElement(IPath.fromOSString("a/b/c/TestField9.java")); //$NON-NLS-1$
		assertNotNull("TestField9 must exist in the test project", element); //$NON-NLS-1$
		updateTagInSource(element, "field1", null, "@noreference", true); //$NON-NLS-1$ //$NON-NLS-2$
		IApiDescription desc = getTestProjectApiDescription();
		assertNotNull("the testing project api description must exist", desc); //$NON-NLS-1$
		IApiAnnotations annot = desc.resolveAnnotations(Factory.fieldDescriptor("a.b.c.TestField9", "field")); //$NON-NLS-1$ //$NON-NLS-2$
		assertNotNull("the annotations for 'field' cannot be null", annot); //$NON-NLS-1$
		assertEquals("there must be a no restrictions for 'field'", 0, annot.getRestrictions()); //$NON-NLS-1$
	}

	/**
	 * Tests that a library added to the build and bundle class path of a
	 * project causes the class file containers for the project to need to be
	 * recomputed
	 */
	@Test
	public void testWPUpdateLibraryAddedToClasspath() throws Exception {
		IFolder folder = null;
		try {
			IJavaProject project = getTestingProject();
			assertNotNull("The testing project must exist", project); //$NON-NLS-1$
			IApiComponent component = getWorkspaceBaseline().getApiComponent(project.getElementName());
			assertNotNull("the workspace component must exist", component); //$NON-NLS-1$
			int before = component.getApiTypeContainers().length;

			// add to classpath
			folder = assertTestLibrary(project, IPath.fromOSString("libx"), "component.a_1.0.0.jar"); //$NON-NLS-1$ //$NON-NLS-2$
			assertNotNull("The new library path should not be null", folder); //$NON-NLS-1$

			// re-retrieve updated component
			component = getWorkspaceBaseline().getApiComponent(project.getElementName());
			assertTrue("there must be more containers after the addition", before < component.getApiTypeContainers().length); //$NON-NLS-1$
		} finally {
			if (folder != null) {
				FileUtils.delete(folder);
			}
		}
	}

	/**
	 * Tests removing a library from the classpath of a project
	 */
	@Test
	public void testWPUpdateLibraryRemovedFromClasspath() throws Exception {
		IPath libPath = null;
		try {
			IJavaProject project = getTestingProject();
			assertNotNull("The testing project must exist", project); //$NON-NLS-1$

			// add to classpath
			IFolder folder = assertTestLibrary(project, IPath.fromOSString("libx"), "component.a_1.0.0.jar"); //$NON-NLS-1$ //$NON-NLS-2$
			IApiComponent component = getWorkspaceBaseline().getApiComponent(project.getElementName());
			assertNotNull("the workspace component must exist", component); //$NON-NLS-1$
			int before = component.getApiTypeContainers().length;
			libPath = folder.getFullPath().append("component.a_1.0.0.jar"); //$NON-NLS-1$

			// remove classpath entry
			ProjectUtils.removeFromClasspath(project, JavaCore.newLibraryEntry(libPath, null, null));
			waitForAutoBuild();

			// remove from bundle class path
			IBundleProjectService service = ProjectUtils.getBundleProjectService();
			IBundleProjectDescription description = service.getDescription(project.getProject());
			description.setBundleClasspath(new IBundleClasspathEntry[] { service.newBundleClasspathEntry(IPath.fromOSString(ProjectUtils.SRC_FOLDER), null, null) });
			description.apply(null);
			waitForAutoBuild();

			// retrieve updated component
			component = getWorkspaceBaseline().getApiComponent(project.getElementName());
			assertTrue("there must be less containers after the removal", before > component.getApiTypeContainers().length); //$NON-NLS-1$
		} finally {
			if (libPath != null) {
				FileUtils.delete(libPath.toOSString());
			}
		}
	}

	/**
	 * Tests that changing the output folder settings for a project cause the
	 * class file containers to be updated
	 */
	@Test
	public void testWPUpdateDefaultOutputFolderChanged() throws Exception {
		IJavaProject project = getTestingProject();
		assertNotNull("The testing project must exist", project); //$NON-NLS-1$
		IContainer container = ProjectUtils.addFolderToProject(project.getProject(), "bin2"); //$NON-NLS-1$
		assertNotNull("the new output folder cannot be null", container); //$NON-NLS-1$
		IApiComponent component = getWorkspaceBaseline().getApiComponent(project.getElementName());
		assertNotNull("the workspace component must exist", component); //$NON-NLS-1$
		int before = component.getApiTypeContainers().length;
		project.setOutputLocation(container.getFullPath(), new NullProgressMonitor());
		waitForAutoBuild();
		assertEquals("there must be the same number of containers after the change", before, //$NON-NLS-1$
				component.getApiTypeContainers().length);
		assertTrue("the new output location should be 'bin2'", "bin2".equalsIgnoreCase(project.getOutputLocation().toFile().getName())); //$NON-NLS-1$ //$NON-NLS-2$
	}

	/**
	 * Tests that the output folder settings for a source folder cause the class
	 * file containers to be updated
	 */
	@Test
	public void testWPUpdateOutputFolderSrcFolderChanged() throws Exception {
		IJavaProject project = getTestingProject();
		IApiComponent component = getWorkspaceBaseline().getApiComponent(project.getElementName());
		assertNotNull("the workspace component must exist", component); //$NON-NLS-1$
		int before = component.getApiTypeContainers().length;

		ProjectUtils.addFolderToProject(project.getProject(), "bin3"); //$NON-NLS-1$
		IContainer container = ProjectUtils.addFolderToProject(project.getProject(), "src2"); //$NON-NLS-1$
		// add to bundle class path
		IBundleProjectService service = ProjectUtils.getBundleProjectService();
		IBundleClasspathEntry next = service.newBundleClasspathEntry(IPath.fromOSString("src2"), IPath.fromOSString("bin3"), IPath.fromOSString("next.jar")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		ProjectUtils.addBundleClasspathEntry(project.getProject(), next);
		waitForAutoBuild();

		// retrieve updated component
		component = getWorkspaceBaseline().getApiComponent(project.getElementName());
		assertTrue("there must be one more container after the change", before < component.getApiTypeContainers().length); //$NON-NLS-1$
		IPackageFragmentRoot root = project.getPackageFragmentRoot(container);
		assertEquals("the class file container for src2 must be 'bin3'", "bin3", //$NON-NLS-1$ //$NON-NLS-2$
				root.getRawClasspathEntry().getOutputLocation().toFile().getName());
	}

	/**
	 * Tests that adding a package does not update the workspace baseline
	 */
	@Test
	public void testWPUpdatePackageAdded() throws Exception {
		IJavaProject project = getTestingProject();
		assertNotNull("The testing project must exist", project); //$NON-NLS-1$

		// add the package
		assertTestPackage(project, IPath.fromOSString(project.getElementName()).append(ProjectUtils.SRC_FOLDER).makeAbsolute(), "a.test1.c.d"); //$NON-NLS-1$

		IApiDescription desc = getTestProjectApiDescription();
		assertNotNull("the testing project api description must exist", desc); //$NON-NLS-1$
		IApiAnnotations annot = desc.resolveAnnotations(Factory.packageDescriptor("a.test1.c.d")); //$NON-NLS-1$
		assertNotNull("the annotations for package " + TESTING_PACKAGE + " should exist", annot); //$NON-NLS-1$ //$NON-NLS-2$
	}

	/**
	 * Tests that removing a package updates the workspace baseline This test
	 * removes the a.b.c package being used in all tests thus far, and should be
	 * run last
	 */
	@Test
	public void testWPUpdatePackageRemoved() throws Exception {
		IJavaProject project = getTestingProject();
		assertNotNull("The testing project must exist", project); //$NON-NLS-1$

		// add the package
		IPath srcroot = IPath.fromOSString(project.getElementName()).append(ProjectUtils.SRC_FOLDER).makeAbsolute();
		IPackageFragment fragment = assertTestPackage(project, srcroot, "a.test2"); //$NON-NLS-1$
		assertNotNull("the package " + TESTING_PACKAGE + " must exist", fragment); //$NON-NLS-1$ //$NON-NLS-2$

		// remove the package
		fragment.delete(true, new NullProgressMonitor());

		IApiDescription desc = getTestProjectApiDescription();
		assertNotNull("the testing project api description must exist", desc); //$NON-NLS-1$
		IApiAnnotations annot = desc.resolveAnnotations(Factory.packageDescriptor("a.test2")); //$NON-NLS-1$
		assertNull("the annotations for package " + TESTING_PACKAGE + " should not exist", annot); //$NON-NLS-1$ //$NON-NLS-2$
	}

	/**
	 * Tests that an exported package addition in the PDE model is reflected in
	 * the workspace api baseline
	 */
	@Test
	public void testWPUpdateExportPackageAdded() throws Exception {
		IJavaProject project = getTestingProject();
		assertNotNull("The testing project must exist", project); //$NON-NLS-1$

		// add package
		assertTestPackage(project, IPath.fromOSString(project.getElementName()).append(ProjectUtils.SRC_FOLDER).makeAbsolute(), "export1"); //$NON-NLS-1$

		// update
		setPackageToApi(project, "export1"); //$NON-NLS-1$
		IApiAnnotations annot = getTestProjectApiDescription().resolveAnnotations(Factory.packageDescriptor("export1")); //$NON-NLS-1$
		assertNotNull("there must be an annotation for the new exported package", annot); //$NON-NLS-1$
		assertEquals("the newly exported package must be API visibility", VisibilityModifiers.API, //$NON-NLS-1$
				annot.getVisibility());
	}

	/**
	 * Tests that changing a directive to x-internal on an exported package
	 * causes the workspace api baseline to be updated
	 */
	@Test
	public void testWPUPdateExportPackageDirectiveChangedToInternal() throws Exception {
		IJavaProject project = getTestingProject();
		assertNotNull("The testing project must exist", project); //$NON-NLS-1$

		// add package
		assertTestPackage(project, IPath.fromOSString(project.getElementName()).append(ProjectUtils.SRC_FOLDER).makeAbsolute(), "export1"); //$NON-NLS-1$

		// export the package
		ProjectUtils.addExportedPackage(project.getProject(), "export1", true, List.of()); //$NON-NLS-1$

		// check the description
		IApiAnnotations annot = getTestProjectApiDescription().resolveAnnotations(Factory.packageDescriptor("export1")); //$NON-NLS-1$
		assertNotNull("there must be an annotation for the new exported package", annot); //$NON-NLS-1$
		assertEquals("the changed exported package must be PRIVATE visibility", VisibilityModifiers.PRIVATE, //$NON-NLS-1$
				annot.getVisibility());
	}

	/**
	 * Tests that an exported package removal in the PDE model is reflected in
	 * the workspace api baseline
	 */
	@Test
	public void testWPUpdateExportPackageRemoved() throws Exception {
		IJavaProject project = getTestingProject();
		assertNotNull("The testing project must exist", project); //$NON-NLS-1$

		// add package
		assertTestPackage(project, IPath.fromOSString(project.getElementName()).append(ProjectUtils.SRC_FOLDER).makeAbsolute(), "export1"); //$NON-NLS-1$

		setPackageToApi(project, "export1"); //$NON-NLS-1$
		IApiAnnotations annot = getTestProjectApiDescription().resolveAnnotations(Factory.packageDescriptor("export1")); //$NON-NLS-1$
		assertNotNull("there must be an annotation for the new exported package", annot); //$NON-NLS-1$
		assertEquals("the newly exported package must be API visibility", VisibilityModifiers.API, //$NON-NLS-1$
				annot.getVisibility());

		// remove exported packages
		IBundleProjectService service = ProjectUtils.getBundleProjectService();
		IBundleProjectDescription description = service.getDescription(project.getProject());
		description.setPackageExports(null);
		description.apply(null);

		// check the API description
		annot = getTestProjectApiDescription().resolveAnnotations(Factory.packageDescriptor("export1")); //$NON-NLS-1$
		assertNotNull("should still be an annotation for the package", annot); //$NON-NLS-1$
		assertTrue("unexported package must be private", VisibilityModifiers.isPrivate(annot.getVisibility())); //$NON-NLS-1$
	}

	/**
	 * sets the given package name to be an Exported-Package
	 */
	private void setPackageToApi(IJavaProject project, String name) throws CoreException {
		ProjectUtils.addExportedPackage(project.getProject(), name, false, List.of());
	}

	IJavaProject getTestingProject() {
		return JavaCore.create(ResourcesPlugin.getWorkspace().getRoot().getProject(TESTING_PLUGIN_PROJECT_NAME));
	}

	@Override
	@Before
	public void setUp() throws Exception {
		super.setUp();
		createProject(TESTING_PLUGIN_PROJECT_NAME, new String[] { TESTING_PACKAGE });
		setPackageToApi(getTestingProject(), TESTING_PACKAGE);
	}

	@Override
	@After
	public void tearDown() throws Exception {
		deleteProject(TESTING_PLUGIN_PROJECT_NAME);
		ApiTestingEnvironment.dispose(getWorkspaceBaseline());
		super.tearDown();
	}
}
