/*******************************************************************************
 * Copyright (c) 2000, 2013 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * 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 junit.framework.Test;

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.IDiffVisitor;
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 org.eclipse.core.internal.resources.mapping.SimpleResourceMapping;

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.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.jface.util.Util;

/**
 * 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, new IResourceVisitor() {
            public boolean visit(IResource resource) throws CoreException {
                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 (int i = 0; i < infos.length; i++) {
            SyncInfo info = infos[i];
            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 remotes = getTaggedRemoteFilesByPath(mapping, branch);
        final Map locals = getTaggedLocalFilesByPath(mapping, branch);
        for (Iterator iter = remotes.keySet().iterator(); iter.hasNext();) {
            String key = (String)iter.next();
            ICVSRemoteFile remote = (ICVSRemoteFile)remotes.get(key);
            ICVSFile local = (ICVSFile)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 (Iterator iter = remotes.keySet().iterator(); iter.hasNext();) {
            String path = (String) iter.next();
            fail("Remote file " + path + " was tagged remotely but not locally.");
        }
        // The local map should be empty after traversal
        for (Iterator iter = locals.keySet().iterator(); iter.hasNext();) {
            String path = (String) iter.next();
            fail("Local file " + path + " was tagged locally but not remotely.");
        }
    }

    private void assertTagged(ResourceMapping mapping, final CVSTag tag) throws CoreException {
        final Map tagged = getTaggedRemoteFilesByPath(mapping, tag);
        // Visit all the resources in the traversal and ensure that they are tagged
        visit(mapping, ResourceMappingContext.LOCAL_CONTEXT, new IResourceVisitor() {
            public boolean visit(IResource resource) throws CoreException {
                if (resource.getType() == IResource.FILE) {
                    ICVSRemoteFile file = popRemote(resource, tagged);
                    assertNotNull("Resource was not tagged: " + resource.getFullPath(), file);
                }
                return true;
            }
        });
        
        // The tagged map should be empty after traversal
        for (Iterator iter = tagged.keySet().iterator(); iter.hasNext();) {
            String path = (String) iter.next();
            fail("Remote file " + path + " was tagged but should not have been.");
        }
    }

    private Map getTaggedLocalFilesByPath(ResourceMapping mapping, final CVSTag branch) throws CoreException {
        final Map tagged = new HashMap();
        IProject[] projects = mapping.getProjects();
        for (int i = 0; i < projects.length; i++) {
            IProject project = projects[i];
            project.accept(new IResourceVisitor() {
                public boolean visit(IResource resource) throws CoreException {
                    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 getTaggedRemoteFilesByPath(ResourceMapping mapping, final CVSTag tag) throws CVSException {
        IProject[] projects = mapping.getProjects();
        ICVSResource[] remotes = getRemoteTrees(projects, tag);
        final Map tagged = getFilesByPath(remotes);
        return tagged;
    }

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

    private Map getFilesByPath(ICVSResource[] remotes) throws CVSException {
        Map result = new HashMap();
        for (int i = 0; i < remotes.length; i++) {
            ICVSResource resource = remotes[i];
            collectFiles(resource, result);
        }
        return result;
    }

    private void collectFiles(ICVSResource resource, Map result) throws CVSException {
        if (resource.isFolder()) {
            ICVSResource[] members = ((ICVSFolder)resource).members(ICVSFolder.ALL_EXISTING_MEMBERS);
            for (int i = 0; i < members.length; i++) {
                ICVSResource member = members[i];
                collectFiles(member, result);
            }
        } else {
            result.put(resource.getRepositoryRelativePath(), resource);
        } 
    }

    private ICVSRemoteFile popRemote(IResource resource, Map 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();
            public Object getModelObject() {
                return object;
            }
            public IProject[] getProjects() {
                return getProjects(resources);
            }
            private IProject[] getProjects(IResource[] resources) {
                Set projects = new HashSet();
                for (int i = 0; i < resources.length; i++) {
                    IResource resource = resources[i];
                    projects.add(resource.getProject());
                }
                return (IProject[]) projects.toArray(new IProject[projects.size()]);
            }
            public ResourceTraversal[] getTraversals(ResourceMappingContext context, IProgressMonitor monitor) throws CoreException {
                return new ResourceTraversal[] {
                        new ResourceTraversal(resources, depth, IResource.NONE)
                    };
            }
			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), new IResourceVisitor() {
            public boolean visit(IResource resource) throws CoreException {
                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 e) {
                        exception[0] = e;
                    } catch (CoreException e) {
                        exception[0] = e;
                    } catch (IOException e) {
                        exception[0] = e;
                    }
                }
                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), new IResourceVisitor() {
            public boolean visit(IResource resource) throws CoreException {
                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 (int i = 0; i < infos.length; i++) {
            SyncInfo info = infos[i];
            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() {
            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, new IDiffVisitor() {
			public boolean visit(IDiff 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 (int i = 0; i < traversals.length; i++) {
            ResourceTraversal traversal = traversals[i];
            visit(traversal, context, visitor);
        }
    }

    private void visit(ResourceTraversal traversal, ResourceMappingContext context, IResourceVisitor visitor) throws CoreException {
        IResource[] resources = traversal.getResources();
        for (int i = 0; i < resources.length; i++) {
            IResource resource = resources[i];
            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 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 (Iterator iter = members.iterator(); iter.hasNext();) {
           IResource member = (IResource) iter.next();
           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().indexOf(
						"Timeout while writing to output stream") >= 0) {
			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 (int i = 0; i < resources.length; i++) {
			IResource resource = resources[i];
			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 (int i = 0; i < resources.length; i++) {
			IResource resource = resources[i];
			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 (int i = 0; i < entries.length; i++) {
				ResourceVariantCacheEntry entry = entries[i];
				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);
	}
}
