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

import static org.eclipse.egit.core.synchronize.GitCommitsModelCache.ADDITION;
import static org.eclipse.egit.core.synchronize.GitCommitsModelCache.CHANGE;
import static org.eclipse.egit.core.synchronize.GitCommitsModelCache.DELETION;
import static org.eclipse.egit.core.synchronize.GitCommitsModelCache.LEFT;
import static org.eclipse.egit.core.synchronize.GitCommitsModelCache.RIGHT;
import static org.eclipse.jgit.junit.JGitTestUtil.deleteTrashFile;
import static org.eclipse.jgit.junit.JGitTestUtil.writeTrashFile;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.Assert.assertThat;

import java.io.IOException;
import java.util.List;

import org.eclipse.egit.core.synchronize.GitCommitsModelCache.Change;
import org.eclipse.egit.core.synchronize.GitCommitsModelCache.Commit;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.errors.AmbiguousObjectException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.junit.Test;

@SuppressWarnings("boxing")
public class GitCommitsModelCacheTest extends AbstractCacheTest {
	@Test
	public void shouldReturnEmptyListForSameSrcAndDstCommit() throws Exception {
		// given
		Git git = new Git(db);
		RevCommit c = commit(git, "second commit");
		// when
		List<Commit> result = GitCommitsModelCache.build(db, c, c, null);
		// then
		assertThat(result, notNullValue());
		assertThat(result.size(), is(0));
	}

	@Test
	public void shouldNotListEmptyCommits() throws Exception {
		// given
		Git git = new Git(db);
		RevCommit c = commit(git, "second commit");
		// when
		List<Commit> result = GitCommitsModelCache.build(db, initialTagId(), c,
				null);
		// then
		assertThat(result, notNullValue());
		assertThat(result.size(), is(0));
	}

	@Test
	public void shouldListAdditionOrDeletionInCommit() throws Exception {
		// given
		Git git = new Git(db);
		writeTrashFile(db, "a.txt", "content");
		git.add().addFilepattern("a.txt").call();
		RevCommit c = commit(git, "first commit");
		// when
		List<Commit> leftResult = GitCommitsModelCache.build(db,
				initialTagId(), c, null);
		List<Commit> rightResult = GitCommitsModelCache.build(db, c,
				initialTagId(), null);
		// then
		// left assertions
		assertThat(leftResult, notNullValue());
		assertCommit(leftResult.get(0), c, 1);
		assertFileAddition(c,
				leftResult.get(0).getChildren().get("a.txt"),
				"a.txt", LEFT);
		// right asserts, after changing sides addition becomes deletion
		assertThat(rightResult, notNullValue());
		assertCommit(rightResult.get(0), c, 1);
		assertFileAddition(c, rightResult.get(0).getChildren().get("a.txt"),
				"a.txt", RIGHT);
	}

	@Test
	public void shouldListAdditionOrDeletionInsideFolderInCommit()
			throws Exception {
		// given
		Git git = new Git(db);
		writeTrashFile(db, "folder/a.txt", "content");
		git.add().addFilepattern("folder/a.txt").call();
		RevCommit c = commit(git, "first commit");
		// when
		List<Commit> leftResult = GitCommitsModelCache.build(db,
				initialTagId(), c, null);
		List<Commit> rightResult = GitCommitsModelCache.build(db, c,
				initialTagId(), null);
		// then
		// left assertions
		assertThat(leftResult, notNullValue());
		assertCommit(leftResult.get(0), c, 1);
		assertThat(leftResult.get(0).getChildren().size(), is(1));
		assertFileAddition(c,
				leftResult.get(0).getChildren().get("folder/a.txt"), "a.txt",
				LEFT);
		// right asserts, after changing sides addition becomes deletion
		assertThat(rightResult, notNullValue());
		assertCommit(rightResult.get(0), c, 1);
		assertThat(rightResult.get(0).getChildren().size(), is(1));
		assertFileAddition(c,
				rightResult.get(0).getChildren().get("folder/a.txt"), "a.txt",
				RIGHT);
	}

	@Test
	public void shouldListAdditionsOrDeletionsInsideSeparateFoldersInCommit()
			throws Exception {
		// given
		Git git = new Git(db);
		writeTrashFile(db, "folder/a.txt", "content");
		writeTrashFile(db, "folder2/b.txt", "b content");
		git.add().addFilepattern("folder/a.txt").call();
		git.add().addFilepattern("folder2/b.txt").call();
		RevCommit c = commit(git, "first commit");
		// when
		List<Commit> leftResult = GitCommitsModelCache.build(db,
				initialTagId(), c, null);
		List<Commit> rightResult = GitCommitsModelCache.build(db, c,
				initialTagId(), null);
		// then
		// left assertions
		assertThat(leftResult, notNullValue());
		assertThat(Integer.valueOf(leftResult.size()), is(Integer.valueOf(1)));
		assertThat(leftResult.get(0).getShortMessage(), is("first commit"));
		assertThat(leftResult.get(0).getChildren(), notNullValue());
		assertThat(leftResult.get(0).getChildren().size(), is(2));
		assertFileAddition(c,
				leftResult.get(0).getChildren().get("folder/a.txt"), "a.txt",
				LEFT);
		assertFileAddition(c,
				leftResult.get(0).getChildren().get("folder2/b.txt"), "b.txt",
				LEFT);
		// right asserts, after changing sides addition becomes deletion
		assertThat(rightResult, notNullValue());
		assertThat(Integer.valueOf(rightResult.size()), is(Integer.valueOf(1)));
		assertThat(rightResult.get(0).getShortMessage(), is("first commit"));
		assertThat(rightResult.get(0).getChildren(), notNullValue());
		assertThat(rightResult.get(0).getChildren().size(), is(2));
		assertFileAddition(c,
				rightResult.get(0).getChildren().get("folder/a.txt"), "a.txt",
				RIGHT);
		assertFileAddition(c,
				rightResult.get(0).getChildren().get("folder2/b.txt"), "b.txt",
				RIGHT);
	}

	@Test
	public void shouldApplyPathFilter() throws Exception {
		// given
		Git git = new Git(db);
		writeTrashFile(db, "folder/a.txt", "content");
		writeTrashFile(db, "folder2/b.txt", "b content");
		git.add().addFilepattern("folder/a.txt").call();
		git.add().addFilepattern("folder2/b.txt").call();
		RevCommit c = commit(git, "first commit");

		// when
		PathFilter pathFilter = PathFilter.create("folder");
		List<Commit> leftResult = GitCommitsModelCache.build(db,
				initialTagId(), c, pathFilter);
		// then
		assertThat(leftResult, notNullValue());
		assertThat(Integer.valueOf(leftResult.size()), is(Integer.valueOf(1)));
		assertThat(leftResult.get(0).getShortMessage(), is("first commit"));
		assertThat(leftResult.get(0).getChildren(), notNullValue());
		assertThat(leftResult.get(0).getChildren().size(), is(1));
		assertFileAddition(c,
				leftResult.get(0).getChildren().get("folder/a.txt"), "a.txt",
				LEFT);
	}

	@Test
	public void shouldListAdditionsOrDeletionsInsideFolderInCommit()
			throws Exception {
		// given
		Git git = new Git(db);
		writeTrashFile(db, "folder/a.txt", "content");
		writeTrashFile(db, "folder/b.txt", "b content");
		git.add().addFilepattern("folder").call();
		RevCommit c = commit(git, "first commit");
		// when
		List<Commit> leftResult = GitCommitsModelCache.build(db,
				initialTagId(), c, null);
		List<Commit> rightResult = GitCommitsModelCache.build(db, c,
				initialTagId(), null);
		// then
		// left assertions
		assertThat(leftResult, notNullValue());
		assertThat(Integer.valueOf(leftResult.size()), is(Integer.valueOf(1)));
		assertCommit(leftResult.get(0), c, 2);
		assertThat(leftResult.get(0).getChildren().size(), is(2));
		assertFileAddition(c,
				leftResult.get(0).getChildren().get("folder/a.txt"), "a.txt",
				LEFT);
		assertFileAddition(c,
				leftResult.get(0).getChildren().get("folder/b.txt"), "b.txt",
				LEFT);
		// right asserts, after changing sides addition becomes deletion
		assertThat(rightResult, notNullValue());
		assertThat(Integer.valueOf(rightResult.size()), is(Integer.valueOf(1)));
		assertCommit(rightResult.get(0), c, 2);
		assertThat(rightResult.get(0).getChildren().size(), is(2));
		assertFileAddition(c,
				rightResult.get(0).getChildren().get("folder/a.txt"), "a.txt",
				RIGHT);
		assertFileAddition(c,
				rightResult.get(0).getChildren().get("folder/b.txt"), "b.txt",
				RIGHT);
	}

	@Test
	public void shouldListChangeInCommit() throws Exception {
		// given
		Git git = new Git(db);
		writeTrashFile(db, "a.txt", "content");
		git.add().addFilepattern("a.txt").call();
		RevCommit c1 = commit(git, "first commit");
		writeTrashFile(db, "a.txt", "new content");
		RevCommit c2 = commit(git, "second commit");
		// when
		List<Commit> leftResult = GitCommitsModelCache.build(db, c1, c2, null);
		List<Commit> rightResult = GitCommitsModelCache.build(db, c2, c1, null);
		// then
		// left assertions
		assertThat(leftResult, notNullValue());
		assertCommit(leftResult.get(0), c2, 1);
		assertFileChange(c2, c1, leftResult.get(0).getChildren().get("a.txt"),
				"a.txt", LEFT);
		// right asserts
		assertThat(rightResult, notNullValue());
		assertCommit(rightResult.get(0), c2, 1);
		assertFileChange(c2, c1, rightResult.get(0).getChildren().get("a.txt"),
				"a.txt", RIGHT);
	}

	@Test
	public void shouldListChangeInsideFolderInCommit() throws Exception {
		// given
		Git git = new Git(db);
		writeTrashFile(db, "folder/a.txt", "content");
		git.add().addFilepattern("folder/a.txt").call();
		RevCommit c1 = commit(git, "first commit");
		writeTrashFile(db, "folder/a.txt", "new content");
		RevCommit c2 = commit(git, "second commit");
		// when
		List<Commit> leftResult = GitCommitsModelCache.build(db, c1, c2, null);
		List<Commit> rightResult = GitCommitsModelCache.build(db, c2, c1, null);
		// then
		// left assertions
		assertThat(leftResult, notNullValue());
		assertCommit(leftResult.get(0), c2, 1);
		assertFileChange(c2, c1,
				leftResult.get(0).getChildren().get("folder/a.txt"), "a.txt",
				LEFT);
		// right asserts
		assertThat(rightResult, notNullValue());
		assertCommit(rightResult.get(0), c2, 1);
		assertFileChange(c2, c1,
				rightResult.get(0).getChildren().get("folder/a.txt"), "a.txt",
				RIGHT);
	}

	@Test
	public void shouldListChangesInsideSeparateFoldersInCommit()
			throws Exception {
		// given
		Git git = new Git(db);
		writeTrashFile(db, "folder/a.txt", "content");
		writeTrashFile(db, "folder2/b.txt", "b content");
		git.add().addFilepattern("folder/a.txt").call();
		git.add().addFilepattern("folder2/b.txt").call();
		RevCommit c1 = commit(git, "first commit");
		writeTrashFile(db, "folder/a.txt", "new content");
		writeTrashFile(db, "folder2/b.txt", "new b content");
		RevCommit c2 = commit(git, "second commit");
		// when
		List<Commit> leftResult = GitCommitsModelCache.build(db, c1, c2, null);
		List<Commit> rightResult = GitCommitsModelCache.build(db, c2, c1, null);
		// then
		// left assertions
		assertThat(leftResult, notNullValue());
		assertThat(Integer.valueOf(leftResult.size()), is(Integer.valueOf(1)));
		assertThat(leftResult.get(0).getShortMessage(), is("second commit"));
		assertThat(leftResult.get(0).getChildren(), notNullValue());
		assertThat(leftResult.get(0).getChildren().size(), is(2));
		assertFileChange(c2, c1,
				leftResult.get(0).getChildren().get("folder/a.txt"), "a.txt",
				LEFT);
		assertFileChange(c2, c1,
				leftResult.get(0).getChildren().get("folder2/b.txt"), "b.txt",
				LEFT);
		// right asserts
		assertThat(rightResult, notNullValue());
		assertThat(Integer.valueOf(rightResult.size()), is(Integer.valueOf(1)));
		assertThat(rightResult.get(0).getShortMessage(), is("second commit"));
		assertThat(rightResult.get(0).getChildren().size(), is(2));
		assertFileChange(c2, c1,
				rightResult.get(0).getChildren().get("folder/a.txt"), "a.txt",
				RIGHT);
		assertFileChange(c2, c1,
				rightResult.get(0).getChildren().get("folder2/b.txt"), "b.txt",
				RIGHT);
	}

	@Test
	public void shouldListChangesInsideFolderInCommit() throws Exception {
		// given
		Git git = new Git(db);
		writeTrashFile(db, "folder/a.txt", "content");
		writeTrashFile(db, "folder/b.txt", "b content");
		git.add().addFilepattern("folder").call();
		RevCommit c1 = commit(git, "first commit");
		writeTrashFile(db, "folder/a.txt", "new content");
		writeTrashFile(db, "folder/b.txt", "new b content");
		RevCommit c2 = commit(git, "second commit");
		// when
		List<Commit> leftResult = GitCommitsModelCache.build(db, c1, c2, null);
		List<Commit> rightResult = GitCommitsModelCache.build(db, c2, c1, null);
		// then
		// left assertions
		assertThat(leftResult, notNullValue());
		assertThat(Integer.valueOf(leftResult.size()), is(Integer.valueOf(1)));
		assertCommit(leftResult.get(0), c2, 2);
		assertFileChange(c2, c1,
				leftResult.get(0).getChildren().get("folder/a.txt"), "a.txt",
				LEFT);
		assertFileChange(c2, c1,
				leftResult.get(0).getChildren().get("folder/b.txt"), "b.txt",
				LEFT);
		// right asserts
		assertThat(rightResult, notNullValue());
		assertThat(Integer.valueOf(rightResult.size()), is(Integer.valueOf(1)));
		assertCommit(rightResult.get(0), c2, 2);
		assertFileChange(c2, c1,
				rightResult.get(0).getChildren().get("folder/a.txt"), "a.txt",
				RIGHT);
		assertFileChange(c2, c1,
				rightResult.get(0).getChildren().get("folder/b.txt"), "b.txt",
				RIGHT);
	}

	@Test
	public void shouldListAllTypeOfChangesInOneCommit() throws Exception {
		// given
		Git git = new Git(db);
		writeTrashFile(db, "a.txt", "a content");
		writeTrashFile(db, "c.txt", "c content");
		git.add().addFilepattern("a.txt").call();
		git.add().addFilepattern("c.txt").call();
		RevCommit c1 = commit(git, "first commit");
		deleteTrashFile(db, "a.txt");
		writeTrashFile(db, "b.txt", "b content");
		writeTrashFile(db, "c.txt", "new c content");
		git.add().addFilepattern("b.txt").call();
		RevCommit c2 = commit(git, "second commit");
		// when
		List<Commit> leftResult = GitCommitsModelCache.build(db, c1, c2, null);
		List<Commit> rightResult = GitCommitsModelCache.build(db, c2, c1, null);
		// then
		// left asserts
		assertThat(leftResult, notNullValue());
		assertCommit(leftResult.get(0), c2, 3);
		assertFileDeletion(c2, c1,
				leftResult.get(0).getChildren().get("a.txt"), "a.txt", LEFT);
		assertFileAddition(c2, c1,
				leftResult.get(0).getChildren().get("b.txt"), "b.txt", LEFT);
		assertFileChange(c2, c1, leftResult.get(0).getChildren().get("c.txt"),
				"c.txt", LEFT);
		// right asserts
		assertThat(rightResult, notNullValue());
		assertCommit(rightResult.get(0), c2, 3);
		assertFileDeletion(c2, c1, rightResult.get(0).getChildren()
				.get("a.txt"), "a.txt", RIGHT);
		assertFileAddition(c2, c1, rightResult.get(0).getChildren()
				.get("b.txt"), "b.txt", RIGHT);
		assertFileChange(c2, c1, rightResult.get(0).getChildren().get("c.txt"),
				"c.txt", RIGHT);
	}

	@Test
	public void shouldListAllTypeOfChangesInsideFolderInOneCommit()
			throws Exception {
		// given
		Git git = new Git(db);
		writeTrashFile(db, "folder/a.txt", "a content");
		writeTrashFile(db, "folder/c.txt", "c content");
		git.add().addFilepattern("folder/a.txt").call();
		git.add().addFilepattern("folder/c.txt").call();
		RevCommit c1 = commit(git, "first commit");
		deleteTrashFile(db, "folder/a.txt");
		writeTrashFile(db, "folder/b.txt", "b content");
		writeTrashFile(db, "folder/c.txt", "new c content");
		git.add().addFilepattern("folder/b.txt").call();
		RevCommit c2 = commit(git, "second commit");
		// when
		List<Commit> leftResult = GitCommitsModelCache.build(db, c1, c2, null);
		List<Commit> rightResult = GitCommitsModelCache.build(db, c2, c1, null);
		// then
		// left assertions
		assertThat(leftResult, notNullValue());
		assertCommit(leftResult.get(0), c2, 3);
		assertFileDeletion(c2, c1,
				leftResult.get(0).getChildren().get("folder/a.txt"), "a.txt",
				LEFT);
		assertFileAddition(c2, c1,
				leftResult.get(0).getChildren().get("folder/b.txt"), "b.txt",
				LEFT);
		assertFileChange(c2, c1,
				leftResult.get(0).getChildren().get("folder/c.txt"), "c.txt",
				LEFT);
		// right asserts
		assertThat(rightResult, notNullValue());
		assertCommit(rightResult.get(0), c2, 3);
		assertFileDeletion(c2, c1,
				rightResult.get(0).getChildren().get("folder/a.txt"), "a.txt",
				RIGHT);
		assertFileAddition(c2, c1,
				rightResult.get(0).getChildren().get("folder/b.txt"), "b.txt",
				RIGHT);
		assertFileChange(c2, c1,
				rightResult.get(0).getChildren().get("folder/c.txt"), "c.txt",
				RIGHT);
	}

	@Test
	public void shouldListAllTypeOfChangesInsideSeparateFoldersInOneCommit()
			throws Exception {
		// given
		Git git = new Git(db);
		writeTrashFile(db, "folder/a.txt", "a content");
		writeTrashFile(db, "folder2/c.txt", "c content");
		git.add().addFilepattern("folder/a.txt").call();
		git.add().addFilepattern("folder2/c.txt").call();
		RevCommit c1 = commit(git, "first commit");
		deleteTrashFile(db, "folder/a.txt");
		writeTrashFile(db, "folder1/b.txt", "b content");
		writeTrashFile(db, "folder2/c.txt", "new c content");
		git.add().addFilepattern("folder1/b.txt").call();
		RevCommit c2 = commit(git, "second commit");
		// when
		List<Commit> leftResult = GitCommitsModelCache.build(db, c1, c2, null);
		List<Commit> rightResult = GitCommitsModelCache.build(db, c2, c1, null);
		// then
		// left assertions
		assertThat(leftResult, notNullValue());
		assertThat(Integer.valueOf(leftResult.size()), is(Integer.valueOf(1)));
		assertThat(leftResult.get(0).getShortMessage(), is("second commit"));
		assertThat(leftResult.get(0).getChildren(), notNullValue());
		assertThat(leftResult.get(0).getChildren().size(), is(3));
		assertFileDeletion(c2, c1,
				leftResult.get(0).getChildren().get("folder/a.txt"), "a.txt",
				LEFT);
		assertFileAddition(c2, c1,
				leftResult.get(0).getChildren().get("folder1/b.txt"), "b.txt",
				LEFT);
		assertFileChange(c2, c1,
				leftResult.get(0).getChildren().get("folder2/c.txt"), "c.txt",
				LEFT);
		// right asserts
		assertThat(rightResult, notNullValue());
		assertThat(Integer.valueOf(rightResult.size()), is(Integer.valueOf(1)));
		assertThat(rightResult.get(0).getShortMessage(), is("second commit"));
		assertThat(rightResult.get(0).getChildren(), notNullValue());
		assertThat(rightResult.get(0).getChildren().size(), is(3));
		assertFileDeletion(c2, c1,
				rightResult.get(0).getChildren().get("folder/a.txt"), "a.txt",
				RIGHT);
		assertFileAddition(c2, c1,
				rightResult.get(0).getChildren().get("folder1/b.txt"), "b.txt",
				RIGHT);
		assertFileChange(c2, c1,
				rightResult.get(0).getChildren().get("folder2/c.txt"), "c.txt",
				RIGHT);
	}

	private RevCommit commit(Git git, String msg) throws Exception {
		tick();
		return git.commit().setAll(true).setMessage(msg)
				.setCommitter(committer).call();
	}

	private ObjectId initialTagId() throws AmbiguousObjectException,
			IOException {
		return db.resolve(INITIAL_TAG);
	}

	private void assertCommit(Commit commit, RevCommit actualCommit,
			int childrenCount) {
		commonCommitAsserts(commit, actualCommit);
		assertThat(commit.getChildren(), notNullValue());
		assertThat(commit.getChildren().size(), is(childrenCount));
	}

	private void commonCommitAsserts(Commit commit, RevCommit actualCommit) {
		assertThat(commit.getShortMessage(), is(actualCommit.getShortMessage()));
		assertThat(commit.getId().toObjectId(), is(actualCommit.getId()));
		assertThat(commit.getAuthorName(), is(actualCommit.getAuthorIdent()
				.getName()));
		assertThat(commit.getCommitterName(), is(actualCommit
				.getCommitterIdent().getName()));
		assertThat(commit.getCommitDate(), is(actualCommit.getAuthorIdent()
				.getWhen()));
	}

	private void assertFileChange(RevCommit actual, RevCommit parent,
			Change change, String name, int direction) {
		commonFileAssertions(actual, parent, change, name);
		assertThat(change.getObjectId(), not(ZERO_ID));
		assertThat(change.getRemoteObjectId(), not(ZERO_ID));
		if (direction == LEFT)
			assertThat(change.getKind(), is(LEFT | CHANGE));
		else
			assertThat(change.getKind(), is(RIGHT | CHANGE));
	}

	private void assertFileAddition(RevCommit actual, Change change,
			String name, int direction) {
		assertFileAddition(actual, null, change, name, direction);
	}

	private void assertFileAddition(RevCommit actual, RevCommit parent,
			Change change, String name, int direction) {
		commonFileAssertions(actual, parent, change, name);
		if (direction == LEFT) {
			assertThat(change.getKind(), is(LEFT | ADDITION));
		} else { // should be Differencer.Right
			assertThat(change.getKind(), is(RIGHT | ADDITION));
		}
		assertThat(change.getObjectId(), not(ZERO_ID));
		assertThat(change.getRemoteObjectId(), nullValue());
	}

	private void assertFileDeletion(RevCommit actual, RevCommit parent,
			Change change, String name, int direction) {
		commonFileAssertions(actual, parent, change, name);
		if (direction == LEFT) {
			assertThat(change.getKind(), is(LEFT | DELETION));
		} else { // should be Differencer.Right
			assertThat(change.getKind(), is(RIGHT | DELETION));
		}
		assertThat(change.getObjectId(), nullValue());
		assertThat(change.getRemoteObjectId(), not(ZERO_ID));
	}

	private void commonFileAssertions(RevCommit actual, RevCommit parent,
			Change change, String name) {
		assertThat(change, notNullValue());
		if (parent != null)
			assertThat(change.getRemoteCommitId().toObjectId(),
					is(parent.getId()));
		if (actual != null && !ObjectId.zeroId().equals(actual))
			assertThat(change.getCommitId().toObjectId(), is(actual.getId()));
		assertThat(change.getName(), is(name));
	}
}
