/*******************************************************************************
 * Copyright (c) 2000, 2013 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.team.tests.ccvs.core.mappings;

import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.internal.resources.mapping.SimpleResourceMapping;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceVisitor;
import org.eclipse.core.resources.mapping.RemoteResourceMappingContext;
import org.eclipse.core.resources.mapping.ResourceMapping;
import org.eclipse.core.resources.mapping.ResourceMappingContext;
import org.eclipse.core.resources.mapping.ResourceTraversal;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.jface.util.Util;
import org.eclipse.osgi.util.NLS;
import org.eclipse.team.core.TeamException;
import org.eclipse.team.core.TeamStatus;
import org.eclipse.team.core.diff.IDiff;
import org.eclipse.team.core.diff.IThreeWayDiff;
import org.eclipse.team.core.mapping.IResourceDiffTree;
import org.eclipse.team.core.mapping.provider.ResourceDiffTree;
import org.eclipse.team.core.synchronize.FastSyncInfoFilter;
import org.eclipse.team.core.synchronize.SyncInfo;
import org.eclipse.team.core.synchronize.SyncInfoTree;
import org.eclipse.team.core.variants.CachedResourceVariant;
import org.eclipse.team.core.variants.IResourceVariant;
import org.eclipse.team.internal.ccvs.core.CVSException;
import org.eclipse.team.internal.ccvs.core.CVSProviderPlugin;
import org.eclipse.team.internal.ccvs.core.CVSTag;
import org.eclipse.team.internal.ccvs.core.ICVSFile;
import org.eclipse.team.internal.ccvs.core.ICVSFolder;
import org.eclipse.team.internal.ccvs.core.ICVSRemoteFile;
import org.eclipse.team.internal.ccvs.core.ICVSResource;
import org.eclipse.team.internal.ccvs.core.client.Command.LocalOption;
import org.eclipse.team.internal.ccvs.core.connection.CVSCommunicationException;
import org.eclipse.team.internal.ccvs.core.resources.RemoteFolderTree;
import org.eclipse.team.internal.ccvs.core.resources.RemoteFolderTreeBuilder;
import org.eclipse.team.internal.ccvs.core.syncinfo.ResourceSyncInfo;
import org.eclipse.team.internal.ccvs.ui.operations.CacheBaseContentsOperation;
import org.eclipse.team.internal.ccvs.ui.operations.CacheRemoteContentsOperation;
import org.eclipse.team.internal.core.ResourceVariantCache;
import org.eclipse.team.internal.core.ResourceVariantCacheEntry;
import org.eclipse.team.internal.core.mapping.SyncInfoToDiffConverter;
import org.eclipse.team.tests.ccvs.core.EclipseTest;
import org.eclipse.team.tests.ccvs.core.TeamCVSTestPlugin;

import junit.framework.Test;

/**
 * Tests for using CVS operations with deep and shallow resource mappings.
 */
public class ResourceMapperTests extends EclipseTest {


	public ResourceMapperTests() {
		super();
	}

	public ResourceMapperTests(String name) {
		super(name);
	}

	public static Test suite() {
		return suite(ResourceMapperTests.class);
	}
	
	/**
	 * Update the resources contained in the given mappers and ensure that the
	 * update was performed properly by comparing the result with the reference projects.
	 * @throws Exception 
	 */
	protected void update(ResourceMapping mapper, LocalOption[] options) throws Exception {
		SyncInfoTree incomingSet = getIncoming(mapper.getProjects());
		update(new ResourceMapping[] { mapper }, options);
		assertUpdate(mapper, incomingSet);
	}
	
	/**
	 * Replace the resources contained in the given mappers and ensure that the
	 * update was performed properly by comparing the result with the reference projects.
	 * @throws Exception 
	 */
	protected void replace(ResourceMapping mapper) throws Exception {
		SyncInfoTree incomingSet = getIncoming(mapper.getProjects());
		replace(new ResourceMapping[] { mapper });
		assertUpdate(mapper, incomingSet);
	} 

	/**
	 * Commit and check that all resources in containing project that should have been committed were and
	 * that any not contained by the mappers were not.
	 * @throws CoreException 
	 * @see org.eclipse.team.tests.ccvs.core.EclipseTest#commit(org.eclipse.core.resources.mapping.IResourceMapper[], java.lang.String)
	 */
	protected void commit(ResourceMapping mapper, String message) throws CoreException {
		SyncInfoTree set = getOutgoing(mapper.getProjects());
		commit(new ResourceMapping[] { mapper }, message);
		assertCommit(mapper, set);
	}
	
	/**
	 * Tag the given resource mappings and assert that only the resources
	 * within the mapping were tagged.
	 * @throws CoreException 
	 */
	protected void tag(ResourceMapping mapping, CVSTag tag) throws CoreException {
		tag(new ResourceMapping[] { mapping }, tag, false);
		assertTagged(mapping, tag);
	}
	
	/**
	 * Branch the resources in the given mapping.
	 * @throws CoreException 
	 * @throws IOException 
	 */
	protected void branch(ResourceMapping mapping, CVSTag branch) throws CoreException, IOException {
		CVSTag version = new CVSTag("Root_" + branch.getName(), CVSTag.VERSION);
		branch(new ResourceMapping[] { mapping }, version, branch, true /* update */);
		assertTagged(mapping, version);
		assertBranched(mapping, branch);
	}
	
	/**
	 * Add any resources contained by the mapping
	 * @param mapping
	 * @throws CoreException 
	 */
	protected void add(ResourceMapping mapping) throws CoreException {
		SyncInfoTree set = getUnaddedResource(mapping);
		add(new ResourceMapping[] { mapping });
		assertAdded(mapping, set);
	}

	private void assertAdded(ResourceMapping mapping, final SyncInfoTree set) throws CoreException {
		// Assert that all resources covered by the mapping are now under version control (i.e. are in-sync)
		// Remove the resources contained in the mapping from the set of unadded resources.
		visit(mapping, ResourceMappingContext.LOCAL_CONTEXT, (IResourceVisitor) resource -> {
			ICVSResource cvsResource = getCVSResource(resource);
			assertTrue("Resource was not added but should have been: " + resource.getFullPath(), 
					(cvsResource.isManaged() 
							|| (cvsResource.isFolder() 
									&& ((ICVSFolder)cvsResource).isCVSFolder())));
			set.remove(resource);
			return true;
		});
		// Assert that the remaining unadded resources are still unadded
		SyncInfo[] infos = set.getSyncInfos();
		for (SyncInfo info : infos) {
			ICVSResource cvsResource = getCVSResource(info.getLocal());
			assertTrue("Resource was added but should not have been: " + info.getLocal().getFullPath(), !cvsResource.isManaged());
		}
	}

	/*
	 * Need to ensure that only the resources contained in the mapping
	 * have the branch tag associated with them.
	 */
	private void assertBranched(ResourceMapping mapping, CVSTag branch) throws CoreException {
		// First, make sure the proper resources are tagged in the repo
		assertTagged(mapping, branch);
		// Now make sure the proper local files are tagged
		final Map<String, ICVSResource> remotes = getTaggedRemoteFilesByPath(mapping, branch);
		final Map<String, ICVSFile> locals = getTaggedLocalFilesByPath(mapping, branch);
		for (Iterator<String> iter = remotes.keySet().iterator(); iter.hasNext();) {
			String key = iter.next();
			ICVSRemoteFile remote = (ICVSRemoteFile)remotes.get(key);
			ICVSFile local = locals.get(key);
			assertNotNull("Remotely tagged resource was not tagged locally: " + remote.getRepositoryRelativePath(), local);
			assertEquals(local.getIResource().getParent().getFullPath(), remote, local, false, false /* include tags */);
			assertEquals("Remotely tagged resource was not tagged locally: " + remote.getRepositoryRelativePath(), branch, local.getSyncInfo().getTag());
			locals.remove(key);
			iter.remove();
		}
		// The remote map should be empty after traversal
		for (Object element : remotes.keySet()) {
			String path = (String) element;
			fail("Remote file " + path + " was tagged remotely but not locally.");
		}
		// The local map should be empty after traversal
		for (Object element : locals.keySet()) {
			String path = (String) element;
			fail("Local file " + path + " was tagged locally but not remotely.");
		}
	}

	private void assertTagged(ResourceMapping mapping, final CVSTag tag) throws CoreException {
		final Map<String, ICVSResource> tagged = getTaggedRemoteFilesByPath(mapping, tag);
		// Visit all the resources in the traversal and ensure that they are tagged
		visit(mapping, ResourceMappingContext.LOCAL_CONTEXT, (IResourceVisitor) resource -> {
			if (resource.getType() == IResource.FILE) {
				ICVSRemoteFile file = popRemote(resource, tagged);
				assertNotNull("Resource was not tagged: " + resource.getFullPath(), file);
			}
			return true;
		});
		
		for (String path : tagged.keySet()) {
			fail("Remote file " + path + " was tagged but should not have been.");
		}
	}

	private Map<String, ICVSFile> getTaggedLocalFilesByPath(ResourceMapping mapping, final CVSTag branch)
			throws CoreException {
		final Map<String, ICVSFile> tagged = new HashMap<>();
		IProject[] projects = mapping.getProjects();
		for (IProject project : projects) {
			project.accept(resource -> {
				if (resource.getType() == IResource.FILE) {
					ICVSFile file = (ICVSFile)getCVSResource(resource);
					ResourceSyncInfo info = file.getSyncInfo();
					if (info != null && info.getTag() != null && info.getTag().equals(branch)) {
						tagged.put(file.getRepositoryRelativePath(), file);
					}
				}
				return true;
			});
		}
		return tagged;
	}
	
	private Map<String, ICVSResource> getTaggedRemoteFilesByPath(ResourceMapping mapping, final CVSTag tag)
			throws CVSException {
		IProject[] projects = mapping.getProjects();
		ICVSResource[] remotes = getRemoteTrees(projects, tag);
		final Map<String, ICVSResource> tagged = getFilesByPath(remotes);
		return tagged;
	}

	private ICVSResource[] getRemoteTrees(IProject[] projects, CVSTag tag) throws CVSException {
		List<ICVSResource> result = new ArrayList<>();
		for (IProject project : projects) {
			RemoteFolderTree tree = RemoteFolderTreeBuilder.buildRemoteTree(getRepository(), project, tag, DEFAULT_MONITOR);
			result.add(tree);
		}
		return result.toArray(new ICVSResource[result.size()]);
	}

	private Map<String, ICVSResource> getFilesByPath(ICVSResource[] remotes) throws CVSException {
		Map<String, ICVSResource> result = new HashMap<>();
		for (ICVSResource resource : remotes) {
			collectFiles(resource, result);
		}
		return result;
	}

	private void collectFiles(ICVSResource resource, Map<String, ICVSResource> result) throws CVSException {
		if (resource.isFolder()) {
			ICVSResource[] members = ((ICVSFolder)resource).members(ICVSFolder.ALL_EXISTING_MEMBERS);
			for (ICVSResource member : members) {
				collectFiles(member, result);
			}
		} else {
			result.put(resource.getRepositoryRelativePath(), resource);
		} 
	}

	private ICVSRemoteFile popRemote(IResource resource, Map<String, ICVSResource> tagged) throws CVSException {
		ICVSResource cvsResource = getCVSResource(resource);
		ICVSRemoteFile remote = (ICVSRemoteFile)tagged.get(cvsResource.getRepositoryRelativePath());
		if (remote != null) {
			tagged.remove(remote.getRepositoryRelativePath());
		}
		return remote;
	}
	
	private ResourceMapping asResourceMapping(final IResource[] resources, final int depth) {
		return new ResourceMapping() {
			private Object object = new Object();
			@Override
			public Object getModelObject() {
				return object;
			}
			@Override
			public IProject[] getProjects() {
				return getProjects(resources);
			}
			private IProject[] getProjects(IResource[] resources) {
				Set<IProject> projects = new HashSet<>();
				for (IResource resource : resources) {
					projects.add(resource.getProject());
				}
				return projects.toArray(new IProject[projects.size()]);
			}
			@Override
			public ResourceTraversal[] getTraversals(ResourceMappingContext context, IProgressMonitor monitor) throws CoreException {
				return new ResourceTraversal[] {
						new ResourceTraversal(resources, depth, IResource.NONE)
					};
			}
			@Override
			public String getModelProviderId() {
				return "org.eclipse.team.tests.cvs.core.modelProvider";
			}
		};
	}
	
	private void assertUpdate(ResourceMapping mapper, final SyncInfoTree set) throws Exception {
		final Exception[] exception = new Exception[] { null };
		visit(mapper, new SyncInfoSetTraveralContext(set), (IResourceVisitor) resource -> {
			SyncInfo info = set.getSyncInfo(resource);
			if (info != null) {
				set.remove(resource);
				try {
					// Assert that the local sync info matches the remote info
					assertEquals(resource.getParent().getFullPath(), getCVSResource(resource), (ICVSResource)info.getRemote(), false, false);
				} catch (CVSException e1) {
					exception[0] = e1;
				} catch (CoreException e2) {
					exception[0] = e2;
				} catch (IOException e3) {
					exception[0] = e3;
				}
			}
			return true;
		});
		if (exception[0] != null) throw exception[0];
		
		// check the the state of the remaining resources has not changed
		assertUnchanged(set);
	}

	private void assertCommit(ResourceMapping mapper, final SyncInfoTree set) throws CoreException {
		visit(mapper, new SyncInfoSetTraveralContext(set), (IResourceVisitor) resource -> {
			SyncInfo info = set.getSyncInfo(resource);
			if (info != null) {
				set.remove(resource);
				assertTrue("Committed resource is not in-sync: " + resource.getFullPath(), getSyncInfo(resource).getKind() == SyncInfo.IN_SYNC);
			}
			return true;
		});
		// check the the state of the remaining resources has not changed
		assertUnchanged(set);
	}

	/*
	 * Assert that the state of the resources in the set have not changed
	 */
	private void assertUnchanged(SyncInfoTree set) throws TeamException {
		//TODO: Need to refresh the subscriber since flush of remote state is deep
		CVSProviderPlugin.getPlugin().getCVSWorkspaceSubscriber().refresh(set.getResources(), IResource.DEPTH_ZERO, DEFAULT_MONITOR);
		SyncInfo[] infos = set.getSyncInfos();
		for (SyncInfo info : infos) {
			assertUnchanged(info);
		}
	}

	private void assertUnchanged(SyncInfo info) throws TeamException {
		SyncInfo current = getSyncInfo(info.getLocal());
		assertEquals("The sync info changed for " + info.getLocal().getFullPath(), info, current);
	}

	private SyncInfo getSyncInfo(IResource local) throws TeamException {
		return CVSProviderPlugin.getPlugin().getCVSWorkspaceSubscriber().getSyncInfo(local);
	}

	private SyncInfoTree getIncoming(IProject[] projects) throws TeamException {
		CVSProviderPlugin.getPlugin().getCVSWorkspaceSubscriber().refresh(projects, IResource.DEPTH_INFINITE, DEFAULT_MONITOR);
		SyncInfoTree set = getAllOutOfSync(projects);
		set.removeOutgoingNodes();
		set.removeConflictingNodes();
		return set;
	}
	
	private SyncInfoTree getOutgoing(IProject[] projects) {
		SyncInfoTree set = getAllOutOfSync(projects);
		set.removeIncomingNodes();
		set.removeConflictingNodes();
		return set;
	}

	private SyncInfoTree getUnaddedResource(ResourceMapping mapping) {
		SyncInfoTree set = getAllOutOfSync(mapping.getProjects());
		set.selectNodes(new FastSyncInfoFilter() {
			@Override
			public boolean select(SyncInfo info) {
				try {
					if (info.getLocal().getType() != IResource.PROJECT && info.getRemote() == null && info.getBase() == null) {
						ICVSResource resource = getCVSResource(info.getLocal());
						return !resource.isManaged();
					}
				} catch (CVSException e) {
					fail(e.getMessage());
				}
				return false;
			}
		});
		return set;
	}
	
	private SyncInfoTree getAllOutOfSync(IProject[] projects) {
		SyncInfoTree set = new SyncInfoTree();
		CVSProviderPlugin.getPlugin().getCVSWorkspaceSubscriber().collectOutOfSync(projects, IResource.DEPTH_INFINITE, set, DEFAULT_MONITOR);
		return set;
	}
	
	private IResourceDiffTree getAllDiffs(IProject[] projects) throws CoreException {
		final ResourceDiffTree tree = new ResourceDiffTree();
		CVSProviderPlugin.getPlugin().getCVSWorkspaceSubscriber().accept(projects, IResource.DEPTH_INFINITE, delta -> {
			tree.add(delta);
			return true;
		});
		return tree;
	}
	
	private void visit(ResourceMapping mapper, ResourceMappingContext context, IResourceVisitor visitor) throws CoreException {
		ResourceTraversal[] traversals = mapper.getTraversals(context, null);
		for (ResourceTraversal traversal : traversals) {
			visit(traversal, context, visitor);
		}
	}

	private void visit(ResourceTraversal traversal, ResourceMappingContext context, IResourceVisitor visitor) throws CoreException {
		IResource[] resources = traversal.getResources();
		for (IResource resource : resources) {
			visit(resource, visitor, context, traversal.getDepth());
		}
	}

	private void visit(IResource resource, IResourceVisitor visitor, ResourceMappingContext context, int depth)
			throws CoreException {
		if (!visitor.visit(resource) || depth == IResource.DEPTH_ZERO || resource.getType() == IResource.FILE)
			return;
		Set<IResource> members = new HashSet<>();
		members.addAll(Arrays.asList(((IContainer) resource).members(false)));
		if (context instanceof RemoteResourceMappingContext) {
			RemoteResourceMappingContext remoteContext = (RemoteResourceMappingContext) context;
			members.addAll(Arrays.asList(remoteContext.fetchMembers((IContainer) resource, DEFAULT_MONITOR)));
		}
		for (IResource member : members) {
			visit(member, visitor, context,
					depth == IResource.DEPTH_ONE ? IResource.DEPTH_ZERO : IResource.DEPTH_INFINITE);
		}
	}
	
	private boolean isTimeout(Throwable e) {
		if (e == null) {
			return false;
		}

		if (e instanceof InterruptedIOException
				&& e.getMessage() != null
				&& e.getMessage().contains("Timeout while writing to output stream")) {
			return true;
		}

		if (e instanceof CoreException) {
			CoreException ce = (CoreException) e;
			if (ce.getStatus() != null && ce.getStatus().isMultiStatus()) {
				MultiStatus multistatus = (MultiStatus) ce.getStatus();
				for (int i = 0; i < multistatus.getChildren().length; i++) {
					if (isTimeout(multistatus.getChildren()[i].getException())) {
						return true;
					}
				}
			}

		}

		return isTimeout(e.getCause());
	}

	public void testUpdate() throws Exception {
		try{
			// Create a test project, import it into cvs and check it out
			IProject project = createProject("testUpdate", new String[] { "changed.txt", "deleted.txt", "folder1/", "folder1/a.txt", "folder1/b.txt", "folder1/subfolder1/c.txt" });
	
			// Check the project out under a different name
			IProject copy = checkoutCopy(project, "-copy");
			
			// Perform some operations on the copy and commit them all
			addResources(copy, new String[] { "added.txt", "folder2/", "folder2/added.txt" }, false);
			setContentsAndEnsureModified(copy.getFile("changed.txt"));
			deleteResources(new IResource[] {copy.getFile("deleted.txt")});
			setContentsAndEnsureModified(copy.getFile("folder1/a.txt"));
			setContentsAndEnsureModified(copy.getFile("folder1/subfolder1/c.txt"));
			commit(asResourceMapping(new IResource[] { copy }, IResource.DEPTH_INFINITE), "A commit message");
			
			// Update the project using depth one and ensure we got only what was asked for
			update(asResourceMapping(new IResource[] { project }, IResource.DEPTH_ONE), null);
			
			// Update a subfolder using depth one and ensure we got only what was asked for
			update(asResourceMapping(new IResource[] { project.getFolder("folder1") }, IResource.DEPTH_ONE), null);
			
			// Update the specific file
			update(asResourceMapping(new IResource[] { project.getFile("folder1/subfolder1/c.txt") }, IResource.DEPTH_ZERO), null);
			
			// Update the remaining resources
			update(asResourceMapping(new IResource[] { project.getFolder("folder2") }, IResource.DEPTH_INFINITE), null);
			assertEquals(project, copy);
		} catch (Exception e) {
			if (isTimeout(e)) {
				//TODO see Bug 399375
				System.err.println("Timeout while testUpdate");
				e.printStackTrace();
				return;
			}
			throw e;
		}
	}
	
	public void testReplace() throws Exception {
		try{
			// Create a test project, import it into cvs and check it out
			IProject project = createProject("testReplace", new String[] { "changed.txt", "deleted.txt", "folder1/", "folder1/a.txt", "folder1/b.txt", "folder1/subfolder1/c.txt" });
	
			// Check the project out under a different name
			IProject copy = checkoutCopy(project, "-copy");
			
			// Perform some operations on the copy and commit them all
			addResources(copy, new String[] { "added.txt", "folder2/", "folder2/added.txt" }, false);
			setContentsAndEnsureModified(copy.getFile("changed.txt"));
			deleteResources(new IResource[] {copy.getFile("deleted.txt")});
			setContentsAndEnsureModified(copy.getFile("folder1/a.txt"));
			setContentsAndEnsureModified(copy.getFile("folder1/subfolder1/c.txt"));
			commit(asResourceMapping(new IResource[] { copy }, IResource.DEPTH_INFINITE), "A commit message");
			
			// Update the project using depth one and ensure we got only what was asked for
			replace(asResourceMapping(new IResource[] { project }, IResource.DEPTH_ONE));
			
			// Update a subfolder using depth one and ensure we got only what was asked for
			deleteResources(new IResource[] {project.getFile("folder1/b.txt")});
			replace(asResourceMapping(new IResource[] { project.getFolder("folder1") }, IResource.DEPTH_ONE));
			
			// Update the specific file
			replace(asResourceMapping(new IResource[] { project.getFile("folder1/subfolder1/c.txt") }, IResource.DEPTH_ZERO));
			
			// Update the remaining resources
			replace(asResourceMapping(new IResource[] { project.getFolder("folder2") }, IResource.DEPTH_INFINITE));
			assertEquals(project, copy);
		} catch (Exception e) {
			if (isTimeout(e)) {
				//TODO see Bug 399375
				System.err.println("Timeout while testReplace");
				e.printStackTrace();
				return;
			}
			throw e;
		}
	}

	public void testCommit() throws Exception {
		if (TeamCVSTestPlugin.IS_UNSTABLE_TEST && Util.isMac())
			return;

		// Create a test project, import it into cvs and check it out
		IProject project = createProject("testCommit", new String[] { "changed.txt", "deleted.txt", "folder1/", "folder1/a.txt", "folder1/b.txt", "folder1/subfolder1/c.txt" });
		
		// Perform some operations on the copy and commit only the top level
		addResources(project, new String[] { "added.txt", "folder2/", "folder2/added.txt" }, false);
		setContentsAndEnsureModified(project.getFile("changed.txt"));
		deleteResources(new IResource[] {project.getFile("deleted.txt")});
		setContentsAndEnsureModified(project.getFile("folder1/a.txt"));
		setContentsAndEnsureModified(project.getFile("folder1/subfolder1/c.txt"));
		
		// Commit the project shallow
		commit(asResourceMapping(new IResource[] { project }, IResource.DEPTH_ONE), "A commit message");
		
		// Commit a subfolder shallow
		commit(asResourceMapping(new IResource[] { project.getFolder("folder1") }, IResource.DEPTH_ONE), "A commit message");
		
		// Now commit the file specifically
		commit(asResourceMapping(new IResource[] { project.getFile("folder1/subfolder1/c.txt") }, IResource.DEPTH_ZERO), "A commit message");
		
		// Now commit the rest
		commit(asResourceMapping(new IResource[] { project.getFolder("folder2") }, IResource.DEPTH_INFINITE), "A commit message");
		
		// Check the project out under a different name
		IProject copy = checkoutCopy(project, "-copy");
		assertEquals(project, copy);
	}
	
	public void testTag() throws Exception {
		// Create a test project, import it into cvs and check it out
		IProject project = createProject("testTag", new String[] { "changed.txt", "deleted.txt", "folder1/", "folder1/a.txt", "folder1/b.txt", "folder1/subfolder1/c.txt" });

		tag(asResourceMapping(new IResource[] { project }, IResource.DEPTH_ONE), new CVSTag("v1", CVSTag.VERSION));
		tag(asResourceMapping(new IResource[] { project.getFolder("folder1") }, IResource.DEPTH_ONE), new CVSTag("v2", CVSTag.VERSION));
		tag(asResourceMapping(new IResource[] { project.getFile("folder1/subfolder1/c.txt") }, IResource.DEPTH_ZERO), new CVSTag("v3", CVSTag.VERSION));
		tag(asResourceMapping(new IResource[] { project}, IResource.DEPTH_INFINITE), new CVSTag("v4", CVSTag.VERSION));
	}
	
	public void testBranch() throws Exception {

		if (TeamCVSTestPlugin.IS_UNSTABLE_TEST)
			return;

		// Create a test project, import it into cvs and check it out
		IProject project = createProject("testBranch", new String[] { "changed.txt", "deleted.txt", "folder1/", "folder1/a.txt", "folder1/b.txt", "folder1/subfolder1/c.txt"  });

		branch(asResourceMapping(new IResource[] { project }, IResource.DEPTH_ONE), new CVSTag("b1", CVSTag.BRANCH));
		branch(asResourceMapping(new IResource[] { project.getFolder("folder1") }, IResource.DEPTH_ONE), new CVSTag("b2", CVSTag.BRANCH));
		branch(asResourceMapping(new IResource[] { project.getFile("folder1/subfolder1/c.txt") }, IResource.DEPTH_ZERO), new CVSTag("b3", CVSTag.BRANCH));
		branch(asResourceMapping(new IResource[] { project }, IResource.DEPTH_INFINITE), new CVSTag("b4", CVSTag.BRANCH));
	}
	
	public void testAdd() throws TeamException, CoreException {
		// Create an empty project
		IProject project = createProject("testAdd", new String[] { });
		// add some resources
		buildResources(project, new String[] { "changed.txt", "deleted.txt", "folder1/", "folder1/a.txt", "folder1/b.txt", "folder1/subfolder1/c.txt"  }, false);
		// add them to CVS
		add(asResourceMapping(new IResource[] { project }, IResource.DEPTH_ONE));
		add(asResourceMapping(new IResource[] { project.getFolder("folder1") }, IResource.DEPTH_ONE));
		add(asResourceMapping(new IResource[] { project.getFile("folder1/subfolder1/c.txt") }, IResource.DEPTH_ZERO));
		add(asResourceMapping(new IResource[] { project }, IResource.DEPTH_INFINITE));
	}

	public void testCacheBase() throws TeamException, CoreException {
		if (TeamCVSTestPlugin.IS_UNSTABLE_TEST && Util.isMac())
			return;

		IProject project = createProject("testCacheBase", new String[] { "changed.txt", "deleted.txt", "folder1/", "folder1/a.txt", "folder1/b.txt", "folder1/subfolder1/c.txt"  });
		IProject copy = checkoutCopy(project, "-copy");

		// First, make some local changes and then cache the bases
		setContentsAndEnsureModified(project.getFile("changed.txt"), "Uncommitted text");
		setContentsAndEnsureModified(project.getFile("folder1/b.txt"));
		project.getFile("deleted.txt").delete(false, true, null);
		try {
			cacheBase(project, true /* cache for outgoing and conflicting */);
			cacheBase(project, false /* cache for conflicting only*/);

			// Next, retry after releasing some changes (to ensure proper contents are fetched)
			setContentsAndEnsureModified(copy.getFile("changed.txt"), "Text comited from the copy");
			commitProject(copy);
			cacheBase(project, true /* cache for outgoing and conflicting */);
			cacheBase(project, false /* cache for conflicting only */);
		} catch (TeamException e) {
			// see bug 325553
			logIfCausedByInterruptedIOException(e);
		}
	}

	private void logIfCausedByInterruptedIOException(TeamException e)
			throws TeamException {
		IStatus status = e.getStatus();
		if (status.isMultiStatus()) {
			MultiStatus mstatus = (MultiStatus) status;
			status = mstatus.getChildren()[0];
			if (status instanceof TeamStatus) {
				Throwable ex = status.getException();
				if (ex instanceof CVSCommunicationException) {
					CVSCommunicationException cce = (CVSCommunicationException) ex;
					status = cce.getStatus();
					if (status.isMultiStatus()) {
						if (status.getException() instanceof InterruptedIOException) {
							// Prevent the test from failing but log the exception
							log("org.eclipse.team.tests.cvs.core", e.getStatus());
							return;
						}
					}
				}
			}
		}
		throw e;
	}

	public void testCacheRemote() throws TeamException, CoreException {
		IProject project = createProject("testCacheRemote", new String[] { "changed.txt", "deleted.txt", "folder1/", "folder1/a.txt", "folder1/b.txt", "folder1/subfolder1/c.txt"  });
		IProject copy = checkoutCopy(project, "-copy");
		
		// Make some remote changes
		setContentsAndEnsureModified(copy.getFile("changed.txt"), "Uncommitted text");
		setContentsAndEnsureModified(copy.getFile("folder1/b.txt"));
		commitProject(copy);
		// Delete a local file
		project.getFile("deleted.txt").delete(false, true, null);
		cacheRemote(project);
	}

	private void cacheRemote(IProject project) throws CoreException {
		clearCache(project);
		CVSProviderPlugin.getPlugin().getCVSWorkspaceSubscriber().refresh(new IProject[] { project }, IResource.DEPTH_INFINITE, DEFAULT_MONITOR);
		IResourceDiffTree tree = getAllDiffs(new IProject[] { project });
		ResourceMapping[] mappings = new ResourceMapping[] {new SimpleResourceMapping(project)};
		CacheRemoteContentsOperation op = new CacheRemoteContentsOperation(null, mappings, tree);
		executeHeadless(op);
		ensureRemoteCached(tree);
	}

	private void cacheBase(IProject project, boolean includeOutgoing) throws CoreException {
		clearCache(project);
		CVSProviderPlugin.getPlugin().getCVSWorkspaceSubscriber().refresh(new IProject[] { project }, IResource.DEPTH_INFINITE, DEFAULT_MONITOR);
		IResourceDiffTree tree = getAllDiffs(new IProject[] { project });
		ResourceMapping[] mappings = new ResourceMapping[] {new SimpleResourceMapping(project)};
		CacheBaseContentsOperation op = new CacheBaseContentsOperation(null, mappings, tree, includeOutgoing);
		executeHeadless(op);
		ensureBaseCached(tree, includeOutgoing);
	}

	private void ensureRemoteCached(IResourceDiffTree tree) {
		IResource[] resources = tree.getAffectedResources();
		for (IResource resource : resources) {
			IDiff node = tree.getDiff(resource);
			if (node instanceof IThreeWayDiff) {
				IThreeWayDiff twd = (IThreeWayDiff) node;
				IResourceVariant remote = SyncInfoToDiffConverter.getRemoteVariant(twd);
				if (remote != null) {
					boolean isCached = ((CachedResourceVariant)remote).isContentsCached();
					int direction = twd.getDirection();
					if (direction == IThreeWayDiff.CONFLICTING || direction == IThreeWayDiff.INCOMING) {
						assertTrue(NLS.bind("The remote contents should be cached for {0}", new String[] {resource.getFullPath().toString()}), isCached);
					} else {
						assertFalse(NLS.bind("The base contents should NOT be cached for {0}", new String[] {resource.getFullPath().toString()}), isCached);
					}
				}
			}
		}
	}
	
	private void ensureBaseCached(IResourceDiffTree tree, boolean includeOutgoing) throws TeamException, CoreException {
		IResource[] resources = tree.getAffectedResources();
		for (IResource resource : resources) {
			IDiff node = tree.getDiff(resource);
			if (node instanceof IThreeWayDiff) {
				IThreeWayDiff twd = (IThreeWayDiff) node;
				IResourceVariant base = SyncInfoToDiffConverter.getBaseVariant(twd);
				if (base != null) {
					boolean isCached = ((CachedResourceVariant)base).isContentsCached();
					int direction = twd.getDirection();
					if (direction == IThreeWayDiff.CONFLICTING || (includeOutgoing && direction == IThreeWayDiff.OUTGOING)) {
						assertTrue(NLS.bind("The base contents should be cached for {0}", new String[] {resource.getFullPath().toString()}), isCached);
						// For conflicts, ensure that the cache contents do not match the remote
						if (direction == SyncInfo.CONFLICTING) {
							IResourceVariant remote = SyncInfoToDiffConverter.getRemoteVariant(twd);
							if (remote != null) {
								InputStream baseIn = base.getStorage(DEFAULT_MONITOR).getContents();
								if (baseIn == null) {
									fail(NLS.bind("Base was not fetched for {0}", new String[] {resource.getFullPath().toString()}));
								}
								InputStream remoteIn = remote.getStorage(DEFAULT_MONITOR).getContents();
								if (compareContent(baseIn, remoteIn)) {
									fail(NLS.bind("The remote was fetched instead of the base for {0}", new String[] {resource.getFullPath().toString()}));
								}
							}
						}
					} else {
						// assertFalse(NLS.bind("The base contents should NOT be cached for {0}", new String[] {resource.getFullPath().toString()}), isCached);
					}
				}
			}
		}
	}

	private void clearCache(IProject project) {
		ResourceVariantCache cache = ResourceVariantCache.getCache(CVSProviderPlugin.ID);
		if (cache != null) {
			ResourceVariantCacheEntry[] entries = cache.getEntries();
			for (ResourceVariantCacheEntry entry : entries) {
				entry.dispose();
			}
		}
	}
	
	public void testBug134517() throws Exception {
		IProject project = createProject("testBug134517", new String[] { "file1.txt", "file2.txt"});
		IProject copy = checkoutCopy(project, "-copy");
		addResources(copy, new String[] { "file0.txt", 
				"new_folder1/", "new_folder1/file2.txt", "new_folder1/new_folder2/", 
				"new_folder1/new_folder2/new_folder3/", "new_folder1/new_folder2/new_folder3/file3.txt"  }, true);
		IResource[] resources = new IResource[] {
				project.getFile("file0.txt"),
				project.getFile("file1.txt"),
				project.getFile("new_folder1/file2.txt"),
				project.getFile("new_folder1/new_folder2/new_folder3/file3.txt")
		};
		update(asResourceMapping(resources, IResource.DEPTH_ZERO), null);
		assertEquals(project, copy);
	}
	
	public void testDeepNewFolder() throws Exception {
		IProject project = createProject("testBug134517", new String[] { "file1.txt", "file2.txt"});
		IProject copy = checkoutCopy(project, "-copy");
		addResources(copy, new String[] {
				"new_folder1/", 
				"new_folder1/new_folder2/", 
				"new_folder1/new_folder2/new_folder3/", 
				"new_folder1/new_folder2/new_folder3/file3.txt"  }, true);
		IResource[] resources = new IResource[] {
				project.getFolder("new_folder1/new_folder2/new_folder3/")
		};
		update(asResourceMapping(resources, IResource.DEPTH_INFINITE), null);
		assertEquals(project, copy);
	}
}
