/*******************************************************************************
 * Copyright (C) 2010, Dariusz Luksza <dariusz@luksza.org>
 *
 * 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
 *******************************************************************************/
package org.eclipse.egit.core.synchronize;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.eclipse.jgit.lib.Constants.HEAD;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.util.Arrays;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IStorage;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.egit.core.project.RepositoryMapping;
import org.eclipse.egit.core.synchronize.dto.GitSynchronizeData;
import org.eclipse.egit.core.synchronize.dto.GitSynchronizeDataSet;
import org.eclipse.egit.core.test.GitTestCase;
import org.eclipse.egit.core.test.TestRepository;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.team.core.variants.IResourceVariant;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class GitResourceVariantComparatorTest extends GitTestCase {

	private Repository repo;

	private IProject iProject;

	private TestRepository testRepo;

	@Before
	public void setUp() throws Exception {
		super.setUp();

		iProject = project.project;
		testRepo = new TestRepository(gitDir);
		testRepo.connect(iProject);
		repo = RepositoryMapping.getMapping(iProject).getRepository();

		// make initial commit
		new Git(repo).commit().setAuthor("JUnit", "junit@jgit.org")
				.setMessage("Initall commit").call();
	}

	@After
	public void clearGitResources() throws Exception {
		testRepo.disconnect(iProject);
		testRepo.dispose();
		repo = null;
		super.tearDown();
	}

	/* ============================================
	 * compare(IResource, IResourceVariant) tests
	 * ============================================ */

	/**
	 * When remote variant wasn't found, compare method is called with null as
	 * second parameter. In this case compare should return false.
	 */
	@Test
	@SuppressWarnings("boxing")
	public void shouldReturnFalseWhenRemoteDoesNotExist() {
		// when
		GitResourceVariantComparator grvc = new GitResourceVariantComparator(
				null);

		// given
		IResource local = mock(IResource.class);
		when(local.exists()).thenReturn(false);

		// then
		assertFalse(grvc.compare(local, null));
	}

	@Test
	@SuppressWarnings("boxing")
	public void shouldReturnFalseWhenRemoteDoesNotExist2() throws Exception {
		// when
		GitResourceVariantComparator grvc = new GitResourceVariantComparator(
				null);

		// given
		IResource local = mock(IResource.class);
		when(local.exists()).thenReturn(false);
		IResourceVariant remote = new GitRemoteFolder(repo, null, null, null, "./");

		// then
		assertFalse(grvc.compare(local, remote));
	}

	/**
	 * It is possible to have a local file that has same name as a remote
	 * folder. In some cases that two resources can be compared. In this case
	 * compare method should return false, because they aren't same resources
	 */
	@Test
	@SuppressWarnings("boxing")
	public void shouldReturnFalseWhenComparingFileAndContainer() {
		// when
		GitResourceVariantComparator grvc = new GitResourceVariantComparator(
				null);

		// given
		IFile local = mock(IFile.class);
		when(local.exists()).thenReturn(true);

		IResourceVariant remote = mock(IResourceVariant.class);
		when(remote.isContainer()).thenReturn(true);

		// then
		assertFalse(grvc.compare(local, remote));
	}

	/**
	 * Comparing two folders that have different path should return false.
	 *
	 * @throws Exception
	 */
	@Test
	@SuppressWarnings("boxing")
	public void shouldReturnFalseWhenComparingContainerAndContainer()
			throws Exception {
		// when
		GitResourceVariantComparator grvc = new GitResourceVariantComparator(
				null);

		// given
		IPath localPath = mock(IPath.class);
		IContainer local = mock(IContainer.class);
		when(local.exists()).thenReturn(true);
		when(local.getLocation()).thenReturn(localPath);

		File file = testRepo.createFile(iProject, "test" + File.separator
				+ "keep");
		RevCommit commit = testRepo.addAndCommit(iProject, file,
				"initial commit");
		String path = Repository.stripWorkDir(repo.getWorkTree(), file);

		GitRemoteFolder remote = new GitRemoteFolder(repo, null, commit,
				commit.getTree(), path);

		// then
		assertFalse(grvc.compare(local, remote));
	}

	/**
	 * When comparing two folders that have same path, compare() method should
	 * return true.
	 *
	 * @throws Exception
	 */
	@Test
	@SuppressWarnings("boxing")
	public void shouldReturnTrueWhenComparingContainerAndContainer()
			throws Exception {
		// when
		GitResourceVariantComparator grvc = new GitResourceVariantComparator(
				null);

		// given
		File file = testRepo.createFile(iProject, "test" + File.separator
				+ "keep");
		RevCommit commit = testRepo.addAndCommit(iProject, file,
				"initial commit");
		String path = Repository.stripWorkDir(repo.getWorkTree(), file);
		IPath iPath = new Path(path);

		IContainer local = mock(IContainer.class);
		when(local.exists()).thenReturn(true);
		when(local.getLocation()).thenReturn(iPath);

		GitRemoteFolder remote = new GitRemoteFolder(repo, null, commit,
				commit.getTree(), path);

		// then
		assertTrue(grvc.compare(local, remote));
	}

	/**
	 * Compare() should return false when comparing two files with different
	 * content length
	 *
	 * @throws Exception
	 */
	@Test
	@SuppressWarnings("boxing")
	public void shouldReturnFalseWhenContentLengthIsDifferent()
			throws Exception {
		// when
		byte[] shortContent = "short content".getBytes("UTF-8");
		byte[] longContent = "very long long content".getBytes("UTF-8");
		GitSynchronizeData data = new GitSynchronizeData(repo, HEAD, HEAD, true);
		GitSynchronizeDataSet dataSet = new GitSynchronizeDataSet(data);
		GitResourceVariantComparator grvc = new GitResourceVariantComparator(
				dataSet);

		// given
		IFile local = mock(IFile.class);
		when(local.exists()).thenReturn(true);
		when(local.getProject()).thenReturn(project.getProject());
		when(local.getContents()).thenReturn(
				new ByteArrayInputStream(longContent));

		IStorage storage = mock(IStorage.class);
		when(storage.getContents()).thenReturn(
				new ByteArrayInputStream(shortContent));

		IResourceVariant remote = mock(IResourceVariant.class);
		when(remote.isContainer()).thenReturn(false);
		when(remote.getStorage(any(IProgressMonitor.class))).thenReturn(
				storage);

		// then
		assertFalse(grvc.compare(local, remote));
	}

	/**
	 * Comparing two files that have same content length but having small
	 * difference inside content should return false.
	 *
	 * @throws Exception
	 */
	@Test
	@SuppressWarnings("boxing")
	public void shouldReturnFalseWhenShortContentIsDifferent() throws Exception {
		// when
		byte[] localContent = "very long long content".getBytes("UTF-8");
		// this typo should be here
		byte[] remoteContent = "very long lonk content".getBytes("UTF-8");
		GitSynchronizeData data = new GitSynchronizeData(repo, HEAD, HEAD, true);
		GitSynchronizeDataSet dataSet = new GitSynchronizeDataSet(data);
		GitResourceVariantComparator grvc = new GitResourceVariantComparator(
				dataSet);

		// given
		IFile local = mock(IFile.class);
		when(local.exists()).thenReturn(true);
		when(local.getProject()).thenReturn(project.getProject());
		when(local.getContents()).thenReturn(
				new ByteArrayInputStream(localContent));

		IStorage storage = mock(IStorage.class);
		when(storage.getContents()).thenReturn(
				new ByteArrayInputStream(remoteContent));

		IResourceVariant remote = mock(IResourceVariant.class);
		when(remote.isContainer()).thenReturn(false);
		when(remote.getStorage(any(IProgressMonitor.class))).thenReturn(
				storage);

		// then
		assertFalse(grvc.compare(local, remote));
	}

	/**
	 * Comparing two 'large' files that have same length and almost identical
	 * content should return false.
	 *
	 * @throws Exception
	 */
	@Test
	@SuppressWarnings("boxing")
	public void shouldReturnFalseWhenLongContentIsDifferent() throws Exception {
		// when
		byte[] localContent = new byte[8192];
		Arrays.fill(localContent, (byte) 'a');
		byte[] remoteContent = new byte[8192];
		Arrays.fill(remoteContent, (byte) 'a');
		remoteContent[8101] = 'b';
		GitSynchronizeData data = new GitSynchronizeData(repo, HEAD, HEAD, true);
		GitSynchronizeDataSet dataSet = new GitSynchronizeDataSet(data);
		GitResourceVariantComparator grvc = new GitResourceVariantComparator(
				dataSet);

		// given
		IFile local = mock(IFile.class);
		when(local.exists()).thenReturn(true);
		when(local.getProject()).thenReturn(project.getProject());
		when(local.getContents()).thenReturn(
				new ByteArrayInputStream(localContent));

		IStorage storage = mock(IStorage.class);
		when(storage.getContents()).thenReturn(
				new ByteArrayInputStream(remoteContent));

		IResourceVariant remote = mock(IResourceVariant.class);
		when(remote.isContainer()).thenReturn(false);
		when(remote.getStorage(any(IProgressMonitor.class))).thenReturn(
				storage);

		// then
		assertFalse(grvc.compare(local, remote));
	}

	/**
	 * Comparing two 'large' files that have different length but same content
	 * should return false.
	 * <p>
	 * This and previous three test cases cover almost the same functionality
	 * but they are covering all return points in compare methods that can be
	 * used when comparing files content
	 *
	 * @throws Exception
	 */
	@Test
	@SuppressWarnings("boxing")
	public void shouldReturnFalseWhenLongContentLengthIsDifferent()
			throws Exception {
		// when
		byte[] localContent = new byte[8192];
		Arrays.fill(localContent, (byte) 'a');
		byte[] remoteContent = new byte[8200];
		Arrays.fill(remoteContent, (byte) 'a');
		GitSynchronizeData data = new GitSynchronizeData(repo, HEAD, HEAD, true);
		GitSynchronizeDataSet dataSet = new GitSynchronizeDataSet(data);
		GitResourceVariantComparator grvc = new GitResourceVariantComparator(
				dataSet);

		// given
		IFile local = mock(IFile.class);
		when(local.exists()).thenReturn(true);
		when(local.getProject()).thenReturn(project.getProject());
		when(local.getContents()).thenReturn(
				new ByteArrayInputStream(localContent));

		IStorage storage = mock(IStorage.class);
		when(storage.getContents()).thenReturn(
				new ByteArrayInputStream(remoteContent));

		IResourceVariant remote = mock(IResourceVariant.class);
		when(remote.isContainer()).thenReturn(false);
		when(remote.getStorage(any(IProgressMonitor.class))).thenReturn(
				storage);

		// then
		assertFalse(grvc.compare(local, remote));
	}

	/**
	 * Comparing two files that have the same content and content length should
	 * return true
	 *
	 * @throws Exception
	 */
	@Test
	@SuppressWarnings("boxing")
	public void shouldReturnTrueWhenShortContentIsDifferent() throws Exception {
		// when
		byte[] localContent = "very long long content".getBytes("UTF-8");
		byte[] remoteContent = "very long long content".getBytes("UTF-8");
		GitSynchronizeData data = new GitSynchronizeData(repo, HEAD, HEAD, true);
		GitSynchronizeDataSet dataSet = new GitSynchronizeDataSet(data);
		GitResourceVariantComparator grvc = new GitResourceVariantComparator(
				dataSet);

		// given
		IFile local = mock(IFile.class);
		when(local.exists()).thenReturn(true);
		when(local.getProject()).thenReturn(project.getProject());
		when(local.getContents()).thenReturn(
				new ByteArrayInputStream(localContent));

		IStorage storage = mock(IStorage.class);
		when(storage.getContents()).thenReturn(
				new ByteArrayInputStream(remoteContent));

		IResourceVariant remote = mock(IResourceVariant.class);
		when(remote.isContainer()).thenReturn(false);
		when(remote.getStorage(any(IProgressMonitor.class))).thenReturn(
				storage);

		// then
		assertTrue(grvc.compare(local, remote));
	}

	/**
	 * Compare two 'large' files that have same content length and content
	 * should return true.
	 *
	 * @throws Exception
	 */
	@Test
	@SuppressWarnings("boxing")
	public void shouldReturnTrueWhenLongContentLengthIsDifferent()
			throws Exception {
		// when
		byte[] localContent = new byte[8192];
		Arrays.fill(localContent, (byte) 'a');
		byte[] remoteContent = new byte[8192];
		Arrays.fill(remoteContent, (byte) 'a');
		GitSynchronizeData data = new GitSynchronizeData(repo, HEAD, HEAD, true);
		GitSynchronizeDataSet dataSet = new GitSynchronizeDataSet(data);
		GitResourceVariantComparator grvc = new GitResourceVariantComparator(
				dataSet);

		// given
		IFile local = mock(IFile.class);
		when(local.exists()).thenReturn(true);
		when(local.getProject()).thenReturn(project.getProject());
		when(local.getContents()).thenReturn(
				new ByteArrayInputStream(localContent));

		IStorage storage = mock(IStorage.class);
		when(storage.getContents()).thenReturn(
				new ByteArrayInputStream(remoteContent));

		IResourceVariant remote = mock(IResourceVariant.class);
		when(remote.isContainer()).thenReturn(false);
		when(remote.getStorage(any(IProgressMonitor.class))).thenReturn(
				storage);

		// then
		assertTrue(grvc.compare(local, remote));
	}

	/* ==================================================
	 * compare(IResourceVariant, IResourceVariant) tests
	 * ================================================== */

	/**
	 * When comparing file that don't exist in base, but exists in remote
	 * compare method should return false.
	 *
	 * @throws Exception
	 */
	@Test
	public void shouldReturnFalseWhenBaseDoesntExist() throws Exception {
		// when
		GitResourceVariantComparator grvc = new GitResourceVariantComparator(
				null);

		// given
		RevCommit baseCommit = testRepo.createInitialCommit("initial commit");
		testRepo.createAndCheckoutBranch(Constants.HEAD, Constants.R_HEADS
				+ "test");
		File file = testRepo.createFile(iProject, "test-file");
		RevCommit remoteCommit = testRepo.addAndCommit(iProject, file,
				"second commit");
		String path = Repository.stripWorkDir(repo.getWorkTree(), file);

		GitRemoteFile base = new GitRemoteFile(repo, baseCommit,
				baseCommit.getTree(), path);
		GitRemoteFile remote = new GitRemoteFile(repo, baseCommit,
				remoteCommit.getTree(), path);

		// then
		assertFalse(grvc.compare(base, remote));
	}

	/**
	 * Compare() should return false when remote file does not exists, but
	 * equivalent local file exist.
	 *
	 * @throws Exception
	 */
	@Test
	public void shouldReturnFalseWhenRemoteVariantDoesntExist()
			throws Exception {
		// when
		GitResourceVariantComparator grvc = new GitResourceVariantComparator(
				null);

		// given
		RevCommit remoteCommit = testRepo.createInitialCommit("initial commit");
		testRepo.createAndCheckoutBranch(Constants.HEAD, Constants.R_HEADS
				+ "test");
		File file = testRepo.createFile(iProject, "test-file");
		RevCommit baseCommit = testRepo.addAndCommit(iProject, file,
				"second commit");
		String path = Repository.stripWorkDir(repo.getWorkTree(), file);

		GitRemoteFile base = new GitRemoteFile(repo, baseCommit,
				baseCommit.getTree(), path);
		GitRemoteFile remote = new GitRemoteFile(repo, remoteCommit,
				remoteCommit.getTree(), path);

		// then
		assertFalse(grvc.compare(base, remote));
	}

	/**
	 * Return false when comparing incompatible types (file against folder) that
	 * also maps onto different resources
	 *
	 * @throws Exception
	 */
	@Test
	public void shouldReturnFalseWhenComparingRemoteVariantFileWithContainer()
			throws Exception {
		// when
		GitResourceVariantComparator grvc = new GitResourceVariantComparator(
				null);

		// given
		File file = testRepo.createFile(iProject, "test" + File.separator
				+ "keep");
		RevCommit commit = testRepo.addAndCommit(iProject, file,
				"initial commit");
		String filePath = Repository.stripWorkDir(repo.getWorkTree(), file);
		String folderPath = Repository.stripWorkDir(repo.getWorkTree(),
				new File(file.getParent()));
		GitRemoteFile base = new GitRemoteFile(repo, commit, commit.getTree(),
				filePath);
		GitRemoteFolder remote = new GitRemoteFolder(repo, null, commit,
				commit.getTree(), folderPath);

		// then
		assertFalse(grvc.compare(base, remote));
	}

	/**
	 * Return false when comparing incompatible types (folder against file) that
	 * also map onto different resources
	 *
	 * @throws Exception
	 */
	@Test
	public void shouldReturnFalseWhenComparingRemoteVariantContainerWithFile()
			throws Exception {
		// when
		GitResourceVariantComparator grvc = new GitResourceVariantComparator(
				null);

		// given
		File file = testRepo.createFile(iProject, "test" + File.separator
				+ "keep");
		RevCommit commit = testRepo.addAndCommit(iProject, file,
				"initial commit");
		String filePath = Repository.stripWorkDir(repo.getWorkTree(), file);
		String folderPath = Repository.stripWorkDir(repo.getWorkTree(),
				new File(file.getParent()));

		GitRemoteFolder base = new GitRemoteFolder(repo, null, commit,
				commit.getTree(), folderPath);
		GitRemoteFile remote = new GitRemoteFile(repo, commit,
				commit.getTree(), filePath);

		// then
		assertFalse(grvc.compare(base, remote));
	}

	/**
	 * When comparing two remote variants that have different path compare
	 * method should return false
	 *
	 * @throws Exception
	 */
	@Test
	public void shouldReturnFalseWhenComparingRemoteVariantContainerWithContainer()
			throws Exception {
		// when
		GitResourceVariantComparator grvc = new GitResourceVariantComparator(
				null);

		// given
		File file1 = testRepo.createFile(iProject, "test1" + File.separator
				+ "keep1");
		File file2 = testRepo.createFile(iProject, "test2" + File.separator
				+ "keep2");
		testRepo.track(file1);
		testRepo.track(file2);
		testRepo.addToIndex(testRepo.getIFile(iProject, file1));
		testRepo.addToIndex(testRepo.getIFile(iProject, file2));
		RevCommit commit = testRepo.commit("initial commit");

		TreeWalk tw = new TreeWalk(repo);
		int nth = tw.addTree(commit.getTree());

		tw.next();
		tw.enterSubtree(); // enter project node
		tw.next();
		GitRemoteFolder base = new GitRemoteFolder(repo, null, commit,
				tw.getObjectId(nth), tw.getNameString());

		tw.next();
		GitRemoteFolder remote = new GitRemoteFolder(repo, null, commit,
				tw.getObjectId(nth), tw.getNameString());

		// then
		assertFalse(grvc.compare(base, remote));
	}

	/**
	 * Comparing two remote folders that have same path should return true
	 *
	 * @throws Exception
	 */
	@Test
	public void shouldReturnTrueWhenComparingRemoteVariantContainerWithContainer()
			throws Exception {
		// when
		GitResourceVariantComparator grvc = new GitResourceVariantComparator(
				null);

		// given
		File file1 = testRepo.createFile(iProject, "test1" + File.separator
				+ "keep1");
		testRepo.track(file1);
		testRepo.addToIndex(testRepo.getIFile(iProject, file1));
		RevCommit commit = testRepo.commit("initial commit");

		String path1 = Repository.stripWorkDir(repo.getWorkTree(), new File(
				file1.getParent()));

		GitRemoteFolder base = new GitRemoteFolder(repo, null, commit,
				commit.getTree(), path1);
		GitRemoteFolder remote = new GitRemoteFolder(repo, null, commit,
				commit.getTree(), path1);

		// then
		assertTrue(grvc.compare(base, remote));
	}

	/**
	 * Comparing two remote files that have different git ObjectId should return false.
	 *
	 * @throws Exception
	 */
	@Test
	public void shouldReturnFalseWhenComparingRemoteVariantWithDifferentObjectId()
			throws Exception {
		// when
		GitResourceVariantComparator grvc = new GitResourceVariantComparator(
				null);

		// given
		File file = testRepo.createFile(iProject, "test-file");
		RevCommit baseCommit = testRepo.appendContentAndCommit(iProject, file,
				"a", "initial commit");
		RevCommit remoteCommit = testRepo.appendContentAndCommit(iProject,
				file, "bc", "second commit");

		String path = Repository.stripWorkDir(repo.getWorkTree(), file);
		GitRemoteFile base = new GitRemoteFile(repo, baseCommit,
				baseCommit.getTree(), path);

		GitRemoteFile remote = new GitRemoteFile(repo, remoteCommit,
				remoteCommit.getTree(), path);

		// then
		assertFalse(grvc.compare(base, remote));
	}

	/**
	 * Comparing two remote files that have the same git ObjectId should return
	 * true.
	 *
	 * @throws Exception
	 */
	@Test
	public void shouldReturnTrueWhenComparingRemoteVariant() throws Exception {
		// when
		GitResourceVariantComparator grvc = new GitResourceVariantComparator(
				null);

		// given
		File file = testRepo.createFile(iProject, "test-file");
		RevCommit commit = testRepo.appendContentAndCommit(iProject, file,
				"a", "initial commit");

		String path = Repository.stripWorkDir(repo.getWorkTree(), file);
		GitRemoteFile base = new GitRemoteFile(repo, commit, commit.getTree(),
				path);

		GitRemoteFile remote = new GitRemoteFile(repo, commit,
				commit.getTree(), path);

		// then
		assertTrue(grvc.compare(base, remote));
	}
}
