/*******************************************************************************
 * Copyright (c) 2000, 2017 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.subscriber;

import java.io.ByteArrayInputStream;
import java.util.*;
import java.util.List;

import junit.framework.Test;

import org.eclipse.compare.structuremergeviewer.IDiffElement;
import org.eclipse.core.resources.*;
import org.eclipse.core.runtime.*;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.swt.widgets.*;
import org.eclipse.team.core.TeamException;
import org.eclipse.team.core.diff.IDiff;
import org.eclipse.team.core.subscribers.Subscriber;
import org.eclipse.team.core.synchronize.SyncInfo;
import org.eclipse.team.internal.ccvs.core.CVSTag;
import org.eclipse.team.internal.ccvs.ui.CVSUIPlugin;
import org.eclipse.team.internal.core.subscribers.*;
import org.eclipse.team.internal.ui.synchronize.*;
import org.eclipse.team.tests.ccvs.ui.SubscriberParticipantSyncInfoSource;
import org.eclipse.team.ui.synchronize.*;
import org.eclipse.ui.PartInitException;

/**
 * Tests the change set mode of the synchronize view
 */
public class CVSChangeSetTests extends CVSSyncSubscriberTest {

	public static Test suite() {
	    return suite(CVSChangeSetTests.class);
	}
	
	public CVSChangeSetTests() {
		super();
	}
	
	public CVSChangeSetTests(String name) {
		super(name);
	}
	
    private void assertIncomingChangesInSets(IFile[][] files, String[] messages) throws CoreException {
        // Get the workspace subscriber which also creates a participant and page in the sync view
        Subscriber workspaceSubscriber = getWorkspaceSubscriber();
        refresh(workspaceSubscriber);
        ISynchronizeModelElement root = getModelRoot(workspaceSubscriber);
        ChangeSetDiffNode[] nodes = getCheckedInChangeSetNodes(root);
        assertNodesInViewer(workspaceSubscriber, nodes);
        assertEquals("The number of change sets in the sync view do not match the expected number", messages.length, nodes.length);
        for (int i = 0; i < messages.length; i++) {
            String message = messages[i];
            ChangeSetDiffNode node = getCommitSetFor(root, message);
            assertNotNull("The commit set for '" + message + "' is not in the sync view", node);
            List<IResource> filesInSet = new ArrayList<>();
            getFileChildren(node, filesInSet);
            assertTrue("The number of files in the set do not match the expected number", files[i].length == filesInSet.size());
            for (int j = 0; j < files[i].length; j++) {
                IFile file = files[i][j];
                assertTrue("File " + file.getFullPath() + " is not in the set", filesInSet.contains(file));
            }
        }
    }

    private void assertNodesInViewer(Subscriber workspaceSubscriber, ChangeSetDiffNode[] nodes) throws PartInitException {
        ISynchronizeParticipant participant = SubscriberParticipantSyncInfoSource.getParticipant(workspaceSubscriber);
        SubscriberParticipantPage page = (SubscriberParticipantPage)SubscriberParticipantSyncInfoSource.getSyncViewPage(participant);
        TreeViewer viewer = (TreeViewer)page.getViewer();
        Tree tree = viewer.getTree();
        List<ChangeSetDiffNode> nodeList = new ArrayList<>();
        nodeList.addAll(Arrays.asList(nodes));
        TreeItem[] items = tree.getItems();
        removeTreeItemsFromList(nodeList, items);
        assertTrue("Not all nodes are visible in the view", nodeList.isEmpty());
    }

    private void removeTreeItemsFromList(List<?> nodeList, TreeItem[] items) {
        for (int i = 0; i < items.length; i++) {
            TreeItem item = items[i];
            nodeList.remove(item.getData());
            TreeItem[] children = item.getItems();
            removeTreeItemsFromList(nodeList, children);
        }
    }

    private ChangeSetDiffNode[] getCheckedInChangeSetNodes(ISynchronizeModelElement root) {
        List<ChangeSetDiffNode> result = new ArrayList<>();
        IDiffElement[] children = root.getChildren();
        for (int i = 0; i < children.length; i++) {
            IDiffElement element = children[i];
            if (element instanceof ChangeSetDiffNode) {
                ChangeSetDiffNode node = (ChangeSetDiffNode)element;
                if (node.getSet() instanceof CheckedInChangeSet) {
                    result.add(node);
                }
            }
        }
        return result.toArray(new ChangeSetDiffNode[result.size()]);
    }
    
    private ChangeSetDiffNode[] getActiveChangeSetNodes(ISynchronizeModelElement root) {
        List<ChangeSetDiffNode> result = new ArrayList<>();
        IDiffElement[] children = root.getChildren();
        for (int i = 0; i < children.length; i++) {
            IDiffElement element = children[i];
            if (element instanceof ChangeSetDiffNode) {
                ChangeSetDiffNode node = (ChangeSetDiffNode)element;
                if (node.getSet() instanceof ActiveChangeSet) {
                    result.add(node);
                }
            }
        }
        return result.toArray(new ChangeSetDiffNode[result.size()]);
    }

    /**
     * Adds IFiles to the list
     */
    private void getFileChildren(ISynchronizeModelElement node, List<IResource> list) {
        IResource resource = node.getResource();
        if (resource != null && resource.getType() == IResource.FILE) {
            list.add(resource);
        }
        IDiffElement[] children = node.getChildren();
        for (int i = 0; i < children.length; i++) {
            IDiffElement child = children[i];
            getFileChildren((ISynchronizeModelElement)child, list);
        }
        return;
    }

    private ChangeSetDiffNode getCommitSetFor(ISynchronizeModelElement root, String message) {
        IDiffElement[] children = root.getChildren();
        for (int i = 0; i < children.length; i++) {
            IDiffElement element = children[i];
            if (element instanceof ChangeSetDiffNode) {
                ChangeSetDiffNode node = (ChangeSetDiffNode)element;
                if (node.getSet().getComment().equals(message)) {
                    return node;
                }
            }
        }
        return null;
    }

    private void refresh(Subscriber workspaceSubscriber) throws TeamException {
        workspaceSubscriber.refresh(workspaceSubscriber.roots(), IResource.DEPTH_INFINITE, DEFAULT_MONITOR);
    }

    private void enableChangeSets(Subscriber workspaceSubscriber) throws PartInitException {
        ISynchronizeParticipant participant = SubscriberParticipantSyncInfoSource.getParticipant(workspaceSubscriber);
        SubscriberParticipantPage page = (SubscriberParticipantPage)SubscriberParticipantSyncInfoSource.getSyncViewPage(participant);
        ChangeSetModelManager manager = (ChangeSetModelManager)page.getConfiguration().getProperty(SynchronizePageConfiguration.P_MODEL_MANAGER);
        manager.setCommitSetsEnabled(true);
        page.getConfiguration().setMode(ISynchronizePageConfiguration.BOTH_MODE);
    }

    private void enableCheckedInChangeSets(Subscriber workspaceSubscriber) throws PartInitException {
        enableChangeSets(workspaceSubscriber);
        ISynchronizeParticipant participant = SubscriberParticipantSyncInfoSource.getParticipant(workspaceSubscriber);
        SubscriberParticipantPage page = (SubscriberParticipantPage)SubscriberParticipantSyncInfoSource.getSyncViewPage(participant);
        page.getConfiguration().setMode(ISynchronizePageConfiguration.INCOMING_MODE);
    }
    
    private void enableActiveChangeSets(Subscriber workspaceSubscriber) throws PartInitException {
        enableChangeSets(workspaceSubscriber);
        ISynchronizeParticipant participant = SubscriberParticipantSyncInfoSource.getParticipant(workspaceSubscriber);
        SubscriberParticipantPage page = (SubscriberParticipantPage)SubscriberParticipantSyncInfoSource.getSyncViewPage(participant);
        page.getConfiguration().setMode(ISynchronizePageConfiguration.OUTGOING_MODE);
    }
    
    /*
     * Wait until all the background handlers have settled and then return the root element in the sync view
     */
    private ISynchronizeModelElement getModelRoot(Subscriber workspaceSubscriber) throws CoreException {
        IProgressMonitor eventLoopProgressMonitor = new IProgressMonitor() {
			public void beginTask(String name, int totalWork) {
			}
			public void done() {
			}
			public void internalWorked(double work) {
			}
			public boolean isCanceled() {
				return false;
			}
			public void setCanceled(boolean value) {
			}
			public void setTaskName(String name) {
			}
			public void subTask(String name) {
			}
			public void worked(int work) {
				while (Display.getCurrent().readAndDispatch()) {}
			}
		};
        SubscriberParticipantSyncInfoSource.getCollector(workspaceSubscriber);
        ISynchronizeParticipant participant = SubscriberParticipantSyncInfoSource.getParticipant(workspaceSubscriber);
        ChangeSetCapability capability = ((IChangeSetProvider)participant).getChangeSetCapability();
        SubscriberChangeSetManager activeManager = (SubscriberChangeSetManager)capability.getActiveChangeSetManager();
        activeManager.waitUntilDone(eventLoopProgressMonitor);
        SubscriberParticipantPage page = (SubscriberParticipantPage)SubscriberParticipantSyncInfoSource.getSyncViewPage(participant);
        ChangeSetModelManager manager = (ChangeSetModelManager)page.getConfiguration().getProperty(SynchronizePageConfiguration.P_MODEL_MANAGER);
        AbstractSynchronizeModelProvider provider = (AbstractSynchronizeModelProvider)manager.getActiveModelProvider();
        provider.waitUntilDone(eventLoopProgressMonitor);
        return provider.getModelRoot();
    }

    private ActiveChangeSetManager getActiveChangeSetManager() {
        return CVSUIPlugin.getPlugin().getChangeSetManager();
    }
    
    /*
     * Assert that the given resources make up the given set both directly
     * and by what is displayed in the sync view.
     */
    private void assertInActiveSet(IResource[] resources, ActiveChangeSet set) throws CoreException {
        assertResourcesAreTheSame(resources, set.getResources(), true);
        ISynchronizeModelElement root = getModelRoot(((SubscriberChangeSetManager)getActiveChangeSetManager()).getSubscriber());
        ChangeSetDiffNode node = getChangeSetNodeFor(root, set);
        assertNotNull("Change set " + set.getTitle() + " did not appear in the sync view", node);
        IResource[] outOfSync = getOutOfSyncResources(node);
        assertResourcesAreTheSame(resources, outOfSync, true);
        // Assert that all active sets are visible in the view
        ChangeSet[] sets = getActiveChangeSetManager().getSets();
        for (int i = 0; i < sets.length; i++) {
            ChangeSet changeSet = sets[i];
            node = getChangeSetNodeFor(root, changeSet);
            assertNotNull("The node for set " + set.getName() + " is not in the view", node);
            
        }
        ChangeSetDiffNode[] nodes = getActiveChangeSetNodes(root);
        assertNodesInViewer(getWorkspaceSubscriber(), nodes);
    }
    
    private ChangeSetDiffNode getChangeSetNodeFor(ISynchronizeModelElement root, ChangeSet set) {
        IDiffElement[] children = root.getChildren();
        for (int i = 0; i < children.length; i++) {
            IDiffElement element = children[i];
            if (element instanceof ChangeSetDiffNode) {
                ChangeSetDiffNode node = (ChangeSetDiffNode)element;
                if (node.getSet() == set) {
                    return node;
                }
            }
        }
        return null;
    }

    private IResource[] getOutOfSyncResources(ISynchronizeModelElement element) {
        ArrayList<SyncInfo> arrayList = new ArrayList<>();
        getOutOfSync(element, arrayList);
        SyncInfo[] infos = arrayList.toArray(new SyncInfo[arrayList.size()]);
        IResource[] resources = getResources(infos);
        return resources;
    }

    private IResource[] getResources(SyncInfo[] infos) {
        IResource[] resources = new IResource[infos.length];
        for (int i = 0; i < resources.length; i++) {
            resources[i] = infos[i].getLocal();
        }
        return resources;
    }

    private void getOutOfSync(ISynchronizeModelElement node, List<SyncInfo> list) {
        SyncInfo info = getSyncInfo(node);
        if (info != null && info.getKind() != SyncInfo.IN_SYNC) {
            list.add(info);
        }
        IDiffElement[] children = node.getChildren();
        for (int i = 0; i < children.length; i++) {
            IDiffElement child = children[i];
            getOutOfSync((ISynchronizeModelElement)child, list);
        }
        return;
    }
    
    private SyncInfo getSyncInfo(ISynchronizeModelElement node) {
        if (node instanceof IAdaptable) {
            return ((IAdaptable)node).getAdapter(SyncInfo.class);
        }
        return null;
    }

    private void assertResourcesAreTheSame(IResource[] resources1, IResource[] resources2, boolean doNotAllowExtra) {
        if (doNotAllowExtra) {
            if (resources1.length != resources2.length) {
	            System.out.println("Expected");
	            for (int i = 0; i < resources1.length; i++) {
	                IResource resource = resources1[i];
	                System.out.println(resource.getFullPath().toString());
	            }
	            System.out.println("Actual");
	            for (int i = 0; i < resources2.length; i++) {
	                IResource resource = resources2[i];
	                System.out.println(resource.getFullPath().toString());
	            }
	        }
	        assertEquals("The number of resources do not match the expected number", resources1.length, resources2.length);
        }
        for (int i = 0; i < resources1.length; i++) {
            IResource resource = resources1[i];
            boolean found = false;
            for (int j = 0; j < resources2.length; j++) {
                IResource resource2 = resources2[j];
                if (resource2.equals(resource)) {
                    found = true;
                    break;
                }
            }
            assertTrue("Expected resource " + resource.getFullPath().toString() + " was not present", found);
        }
    }

    /*
     * Assert that the given resources make up the root set
     * displayed in the sync view. The root set is those 
     * resources that are not part of an active change set.
     */
    private void assertInRootSet(IResource[] resources) throws CoreException {
        ISynchronizeModelElement[] nodes = getNonChangeSetRoots(getModelRoot(((SubscriberChangeSetManager)getActiveChangeSetManager()).getSubscriber()));
        List<SyncInfo> list = new ArrayList<>();
        for (int i = 0; i < nodes.length; i++) {
            ISynchronizeModelElement element = nodes[i];
            getOutOfSync(element, list);
        }
        IResource[] outOfSync = getResources(list.toArray(new SyncInfo[list.size()]));
        // Only require that the expected resources are there but allow extra.
        // This is required because of junk left over from previous tests.
        // This means there is a bug somewhere. But where?
        assertResourcesAreTheSame(resources, outOfSync, false /* allow extra out-of-sync resources */);
        
    }
    
    private ISynchronizeModelElement[] getNonChangeSetRoots(ISynchronizeModelElement modelRoot) {
        List<ISynchronizeModelElement> result = new ArrayList<>();
        IDiffElement[] children = modelRoot.getChildren();
        for (int i = 0; i < children.length; i++) {
            IDiffElement element = children[i];
            if (!(element instanceof ChangeSetDiffNode)) {
                result.add((ISynchronizeModelElement) element);
            }
        }
        return result.toArray(new ISynchronizeModelElement[result.size()]);
    }

    public void testSimpleCommit() throws CoreException {
        enableCheckedInChangeSets(getWorkspaceSubscriber());
        
	    IProject project = createProject(new String[] { "file1.txt", "file2.txt", "folder1/", "folder1/a.txt", "folder1/b.txt"});
	    
	    // Modify a file in a copy
	    IProject copy = checkoutCopy(project, CVSTag.DEFAULT);
	    setContentsAndEnsureModified(copy.getFile("file1.txt"));
	    String message1 = "Commit 1";
	    commitResources(new IResource[] {copy}, IResource.DEPTH_INFINITE, message1);
	    assertIncomingChangesInSets(new IFile[][] {{ project.getFile("file1.txt") }}, new String[] {message1});
	    
	    // Modify the copy some more
	    setContentsAndEnsureModified(copy.getFile("file2.txt"));
	    setContentsAndEnsureModified(copy.getFile("folder1/a.txt"));
	    String message2 = "Commit 2";
	    commitResources(new IResource[] {copy}, IResource.DEPTH_INFINITE, message2);
	    assertIncomingChangesInSets(new IFile[][] {
	            { project.getFile("file1.txt") },
	            { project.getFile("file2.txt"), project.getFile("folder1/a.txt") }
	            }, new String[] {message1, message2});
	    
	    // Modify the copy some more
	    setContentsAndEnsureModified(copy.getFile("file2.txt"));
	    String message3 = "Commit 3";
	    commitResources(new IResource[] {copy}, IResource.DEPTH_INFINITE, message3);
	    assertIncomingChangesInSets(new IFile[][] {
	            { project.getFile("file1.txt") },
	            { project.getFile("folder1/a.txt") },
	            { project.getFile("file2.txt")}
	            }, new String[] {message1, message2, message3});
	    
	    // Now commit the files in one of the sets and ensure it is removed from the view
	    updateResources(new IResource[] { project.getFile("file1.txt")}, false);
	    assertIncomingChangesInSets(new IFile[][] {
	            { project.getFile("folder1/a.txt") },
	            { project.getFile("file2.txt")}
	            }, new String[] {message2, message3});
	}
	
    public void testSimpleActiveChangeSet() throws CoreException {
        IProject project = createProject(new String[] { "file1.txt", "file2.txt", "folder1/", "folder1/a.txt", "folder1/b.txt"});
        // Enable Change Sets
        enableActiveChangeSets(getWorkspaceSubscriber());
	    // Add a folder and file
	    IFolder newFolder = project.getFolder("folder2");
        newFolder.create(false, true, null);
        IFile newFile = newFolder.getFile("file.txt");
        newFile.create(new ByteArrayInputStream("Hi There".getBytes()), false, null);
        // Create an active commit set and assert that it appears in the sync view
        ActiveChangeSetManager manager = getActiveChangeSetManager();
        ActiveChangeSet set = manager.createSet("test", new IDiff[0]);
        manager.add(set);
        assertInActiveSet(new IResource[] { }, set);
        assertInRootSet(new IResource[] {newFolder, newFile});
        // Add the new file to the set and assert that the file is in the set and the folder is still at the root
        set.add(new IResource[] { newFile });
        assertInActiveSet(new IResource[] { newFile }, set);
        assertInRootSet(new IResource[] {newFolder });
	    // Add the folder to the set
        set.add(new IResource[] { newFolder });
        assertInActiveSet(new IResource[] { newFolder, newFile }, set);
    }
}
