/*
 * Copyright (C) 2012, GitHub Inc.
 * and other copyright owners as documented in the project's IP log.
 *
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Distribution License v1.0 which
 * accompanies this distribution, is reproduced below, and is
 * available at http://www.eclipse.org/org/documents/edl-v10.php
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the following
 * conditions are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above
 *   copyright notice, this list of conditions and the following
 *   disclaimer in the documentation and/or other materials provided
 *   with the distribution.
 *
 * - Neither the name of the Eclipse Foundation, Inc. nor the
 *   names of its contributors may be used to endorse or promote
 *   products derived from this software without specific prior
 *   written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.eclipse.jgit.api;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

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

import org.eclipse.jgit.api.errors.UnmergedPathsException;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.junit.RepositoryTestCase;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.ReflogEntry;
import org.eclipse.jgit.lib.ReflogReader;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
import org.eclipse.jgit.util.FileUtils;
import org.junit.Before;
import org.junit.Test;

/**
 * Unit tests of {@link StashCreateCommand}
 */
public class StashCreateCommandTest extends RepositoryTestCase {

	private RevCommit head;

	private Git git;

	private File committedFile;

	private File untrackedFile;

	@Override
	@Before
	public void setUp() throws Exception {
		super.setUp();
		git = Git.wrap(db);
		committedFile = writeTrashFile("file.txt", "content");
		git.add().addFilepattern("file.txt").call();
		head = git.commit().setMessage("add file").call();
		assertNotNull(head);
		untrackedFile = writeTrashFile("untracked.txt", "content");
	}

	private void validateStashedCommit(final RevCommit commit)
			throws IOException {
		validateStashedCommit(commit, 2);
	}

	/**
	 * Core validation to be performed on all stashed commits
	 *
	 * @param commit
	 * @param parentCount
	 *            number of parent commits required
	 * @throws IOException
	 */
	private void validateStashedCommit(final RevCommit commit,
			int parentCount)
			throws IOException {
		assertNotNull(commit);
		Ref stashRef = db.exactRef(Constants.R_STASH);
		assertNotNull(stashRef);
		assertEquals(commit, stashRef.getObjectId());
		assertNotNull(commit.getAuthorIdent());
		assertEquals(commit.getAuthorIdent(), commit.getCommitterIdent());
		assertEquals(parentCount, commit.getParentCount());

		// Load parents
		try (RevWalk walk = new RevWalk(db)) {
			for (RevCommit parent : commit.getParents())
				walk.parseBody(parent);
		}

		assertEquals(1, commit.getParent(1).getParentCount());
		assertEquals(head, commit.getParent(1).getParent(0));
		assertFalse("Head tree matches stashed commit tree", commit.getTree()
				.equals(head.getTree()));
		assertEquals(head, commit.getParent(0));
		assertFalse(commit.getFullMessage().equals(
				commit.getParent(1).getFullMessage()));
	}

	private TreeWalk createTreeWalk() {
		TreeWalk walk = new TreeWalk(db);
		walk.setRecursive(true);
		walk.setFilter(TreeFilter.ANY_DIFF);
		return walk;
	}

	private List<DiffEntry> diffWorkingAgainstHead(final RevCommit commit)
			throws IOException {
		try (TreeWalk walk = createTreeWalk()) {
			walk.addTree(commit.getParent(0).getTree());
			walk.addTree(commit.getTree());
			return DiffEntry.scan(walk);
		}
	}

	private List<DiffEntry> diffIndexAgainstHead(final RevCommit commit)
			throws IOException {
		try (TreeWalk walk = createTreeWalk()) {
			walk.addTree(commit.getParent(0).getTree());
			walk.addTree(commit.getParent(1).getTree());
			return DiffEntry.scan(walk);
		}
	}

	private List<DiffEntry> diffIndexAgainstWorking(final RevCommit commit)
			throws IOException {
		try (TreeWalk walk = createTreeWalk()) {
			walk.addTree(commit.getParent(1).getTree());
			walk.addTree(commit.getTree());
			return DiffEntry.scan(walk);
		}
	}

	@Test
	public void noLocalChanges() throws Exception {
		assertNull(git.stashCreate().call());
	}

	@Test
	public void workingDirectoryDelete() throws Exception {
		deleteTrashFile("file.txt");
		RevCommit stashed = git.stashCreate().call();
		assertNotNull(stashed);
		assertEquals("content", read(committedFile));
		validateStashedCommit(stashed);

		assertEquals(head.getTree(), stashed.getParent(1).getTree());

		List<DiffEntry> diffs = diffWorkingAgainstHead(stashed);
		assertEquals(1, diffs.size());
		assertEquals(DiffEntry.ChangeType.DELETE, diffs.get(0).getChangeType());
		assertEquals("file.txt", diffs.get(0).getOldPath());
	}

	@Test
	public void indexAdd() throws Exception {
		File addedFile = writeTrashFile("file2.txt", "content2");
		git.add().addFilepattern("file2.txt").call();

		RevCommit stashed = Git.wrap(db).stashCreate().call();
		assertNotNull(stashed);
		assertFalse(addedFile.exists());
		validateStashedCommit(stashed);

		assertEquals(stashed.getTree(), stashed.getParent(1).getTree());

		List<DiffEntry> diffs = diffWorkingAgainstHead(stashed);
		assertEquals(1, diffs.size());
		assertEquals(DiffEntry.ChangeType.ADD, diffs.get(0).getChangeType());
		assertEquals("file2.txt", diffs.get(0).getNewPath());
	}

	@Test
	public void newFileInIndexThenModifiedInWorkTree() throws Exception {
		writeTrashFile("file", "content");
		git.add().addFilepattern("file").call();
		writeTrashFile("file", "content2");
		RevCommit stashedWorkTree = Git.wrap(db).stashCreate().call();
		validateStashedCommit(stashedWorkTree);
		try (RevWalk walk = new RevWalk(db)) {
			RevCommit stashedIndex = stashedWorkTree.getParent(1);
			walk.parseBody(stashedIndex);
			walk.parseBody(stashedIndex.getTree());
			walk.parseBody(stashedIndex.getParent(0));
		}
		List<DiffEntry> workTreeStashAgainstWorkTree = diffWorkingAgainstHead(stashedWorkTree);
		assertEquals(1, workTreeStashAgainstWorkTree.size());
		List<DiffEntry> workIndexAgainstWorkTree = diffIndexAgainstHead(stashedWorkTree);
		assertEquals(1, workIndexAgainstWorkTree.size());
		List<DiffEntry> indexStashAgainstWorkTree = diffIndexAgainstWorking(stashedWorkTree);
		assertEquals(1, indexStashAgainstWorkTree.size());
	}

	@Test
	public void indexDelete() throws Exception {
		git.rm().addFilepattern("file.txt").call();

		RevCommit stashed = Git.wrap(db).stashCreate().call();
		assertNotNull(stashed);
		assertEquals("content", read(committedFile));
		validateStashedCommit(stashed);

		assertEquals(stashed.getTree(), stashed.getParent(1).getTree());

		List<DiffEntry> diffs = diffWorkingAgainstHead(stashed);
		assertEquals(1, diffs.size());
		assertEquals(DiffEntry.ChangeType.DELETE, diffs.get(0).getChangeType());
		assertEquals("file.txt", diffs.get(0).getOldPath());
	}

	@Test
	public void workingDirectoryModify() throws Exception {
		writeTrashFile("file.txt", "content2");

		RevCommit stashed = Git.wrap(db).stashCreate().call();
		assertNotNull(stashed);
		assertEquals("content", read(committedFile));
		validateStashedCommit(stashed);

		assertEquals(head.getTree(), stashed.getParent(1).getTree());

		List<DiffEntry> diffs = diffWorkingAgainstHead(stashed);
		assertEquals(1, diffs.size());
		assertEquals(DiffEntry.ChangeType.MODIFY, diffs.get(0).getChangeType());
		assertEquals("file.txt", diffs.get(0).getNewPath());
	}

	@Test
	public void workingDirectoryModifyInSubfolder() throws Exception {
		String path = "d1/d2/f.txt";
		File subfolderFile = writeTrashFile(path, "content");
		git.add().addFilepattern(path).call();
		head = git.commit().setMessage("add file").call();

		writeTrashFile(path, "content2");

		RevCommit stashed = Git.wrap(db).stashCreate().call();
		assertNotNull(stashed);
		assertEquals("content", read(subfolderFile));
		validateStashedCommit(stashed);

		assertEquals(head.getTree(), stashed.getParent(1).getTree());

		List<DiffEntry> diffs = diffWorkingAgainstHead(stashed);
		assertEquals(1, diffs.size());
		assertEquals(DiffEntry.ChangeType.MODIFY, diffs.get(0).getChangeType());
		assertEquals(path, diffs.get(0).getNewPath());
	}

	@Test
	public void workingDirectoryModifyIndexChanged() throws Exception {
		writeTrashFile("file.txt", "content2");
		git.add().addFilepattern("file.txt").call();
		writeTrashFile("file.txt", "content3");

		RevCommit stashed = Git.wrap(db).stashCreate().call();
		assertNotNull(stashed);
		assertEquals("content", read(committedFile));
		validateStashedCommit(stashed);

		assertFalse(stashed.getTree().equals(stashed.getParent(1).getTree()));

		List<DiffEntry> workingDiffs = diffWorkingAgainstHead(stashed);
		assertEquals(1, workingDiffs.size());
		assertEquals(DiffEntry.ChangeType.MODIFY, workingDiffs.get(0)
				.getChangeType());
		assertEquals("file.txt", workingDiffs.get(0).getNewPath());

		List<DiffEntry> indexDiffs = diffIndexAgainstHead(stashed);
		assertEquals(1, indexDiffs.size());
		assertEquals(DiffEntry.ChangeType.MODIFY, indexDiffs.get(0)
				.getChangeType());
		assertEquals("file.txt", indexDiffs.get(0).getNewPath());

		assertEquals(workingDiffs.get(0).getOldId(), indexDiffs.get(0)
				.getOldId());
		assertFalse(workingDiffs.get(0).getNewId()
				.equals(indexDiffs.get(0).getNewId()));
	}

	@Test
	public void workingDirectoryCleanIndexModify() throws Exception {
		writeTrashFile("file.txt", "content2");
		git.add().addFilepattern("file.txt").call();
		writeTrashFile("file.txt", "content");

		RevCommit stashed = Git.wrap(db).stashCreate().call();
		assertNotNull(stashed);
		assertEquals("content", read(committedFile));
		validateStashedCommit(stashed);

		assertEquals(stashed.getParent(1).getTree(), stashed.getTree());

		List<DiffEntry> workingDiffs = diffWorkingAgainstHead(stashed);
		assertEquals(1, workingDiffs.size());
		assertEquals(DiffEntry.ChangeType.MODIFY, workingDiffs.get(0)
				.getChangeType());
		assertEquals("file.txt", workingDiffs.get(0).getNewPath());

		List<DiffEntry> indexDiffs = diffIndexAgainstHead(stashed);
		assertEquals(1, indexDiffs.size());
		assertEquals(DiffEntry.ChangeType.MODIFY, indexDiffs.get(0)
				.getChangeType());
		assertEquals("file.txt", indexDiffs.get(0).getNewPath());

		assertEquals(workingDiffs.get(0).getOldId(), indexDiffs.get(0)
				.getOldId());
		assertTrue(workingDiffs.get(0).getNewId()
				.equals(indexDiffs.get(0).getNewId()));
	}

	@Test
	public void workingDirectoryDeleteIndexAdd() throws Exception {
		String path = "file2.txt";
		File added = writeTrashFile(path, "content2");
		assertTrue(added.exists());
		git.add().addFilepattern(path).call();
		FileUtils.delete(added);
		assertFalse(added.exists());

		RevCommit stashed = Git.wrap(db).stashCreate().call();
		assertNotNull(stashed);
		assertFalse(added.exists());

		validateStashedCommit(stashed);

		assertEquals(stashed.getParent(1).getTree(), stashed.getTree());

		List<DiffEntry> workingDiffs = diffWorkingAgainstHead(stashed);
		assertEquals(1, workingDiffs.size());
		assertEquals(DiffEntry.ChangeType.ADD, workingDiffs.get(0)
				.getChangeType());
		assertEquals(path, workingDiffs.get(0).getNewPath());

		List<DiffEntry> indexDiffs = diffIndexAgainstHead(stashed);
		assertEquals(1, indexDiffs.size());
		assertEquals(DiffEntry.ChangeType.ADD, indexDiffs.get(0)
				.getChangeType());
		assertEquals(path, indexDiffs.get(0).getNewPath());

		assertEquals(workingDiffs.get(0).getOldId(), indexDiffs.get(0)
				.getOldId());
		assertTrue(workingDiffs.get(0).getNewId()
				.equals(indexDiffs.get(0).getNewId()));
	}

	@Test
	public void workingDirectoryDeleteIndexEdit() throws Exception {
		File edited = writeTrashFile("file.txt", "content2");
		git.add().addFilepattern("file.txt").call();
		FileUtils.delete(edited);
		assertFalse(edited.exists());

		RevCommit stashed = Git.wrap(db).stashCreate().call();
		assertNotNull(stashed);
		assertEquals("content", read(committedFile));
		validateStashedCommit(stashed);

		assertFalse(stashed.getTree().equals(stashed.getParent(1).getTree()));

		List<DiffEntry> workingDiffs = diffWorkingAgainstHead(stashed);
		assertEquals(1, workingDiffs.size());
		assertEquals(DiffEntry.ChangeType.DELETE, workingDiffs.get(0)
				.getChangeType());
		assertEquals("file.txt", workingDiffs.get(0).getOldPath());

		List<DiffEntry> indexDiffs = diffIndexAgainstHead(stashed);
		assertEquals(1, indexDiffs.size());
		assertEquals(DiffEntry.ChangeType.MODIFY, indexDiffs.get(0)
				.getChangeType());
		assertEquals("file.txt", indexDiffs.get(0).getNewPath());

		assertEquals(workingDiffs.get(0).getOldId(), indexDiffs.get(0)
				.getOldId());
		assertFalse(workingDiffs.get(0).getNewId()
				.equals(indexDiffs.get(0).getNewId()));
	}

	@Test
	public void multipleEdits() throws Exception {
		git.rm().addFilepattern("file.txt").call();
		File addedFile = writeTrashFile("file2.txt", "content2");
		git.add().addFilepattern("file2.txt").call();

		RevCommit stashed = Git.wrap(db).stashCreate().call();
		assertNotNull(stashed);
		assertFalse(addedFile.exists());
		validateStashedCommit(stashed);

		assertEquals(stashed.getTree(), stashed.getParent(1).getTree());

		List<DiffEntry> diffs = diffWorkingAgainstHead(stashed);
		assertEquals(2, diffs.size());
		assertEquals(DiffEntry.ChangeType.DELETE, diffs.get(0).getChangeType());
		assertEquals("file.txt", diffs.get(0).getOldPath());
		assertEquals(DiffEntry.ChangeType.ADD, diffs.get(1).getChangeType());
		assertEquals("file2.txt", diffs.get(1).getNewPath());
	}

	@Test
	public void refLogIncludesCommitMessage() throws Exception {
		PersonIdent who = new PersonIdent("user", "user@email.com");
		deleteTrashFile("file.txt");
		RevCommit stashed = git.stashCreate().setPerson(who).call();
		assertNotNull(stashed);
		assertEquals("content", read(committedFile));
		validateStashedCommit(stashed);

		ReflogReader reader = git.getRepository().getReflogReader(
				Constants.R_STASH);
		ReflogEntry entry = reader.getLastEntry();
		assertNotNull(entry);
		assertEquals(ObjectId.zeroId(), entry.getOldId());
		assertEquals(stashed, entry.getNewId());
		assertEquals(who, entry.getWho());
		assertEquals(stashed.getFullMessage(), entry.getComment());
	}

	@Test(expected = UnmergedPathsException.class)
	public void unmergedPathsShouldCauseException() throws Exception {
		commitFile("file.txt", "master", "base");
		RevCommit side = commitFile("file.txt", "side", "side");
		commitFile("file.txt", "master", "master");
		git.merge().include(side).call();

		git.stashCreate().call();
	}

	@Test
	public void untrackedFileIncluded() throws Exception {
		String trackedPath = "tracked.txt";
		writeTrashFile(trackedPath, "content2");
		git.add().addFilepattern(trackedPath).call();

		RevCommit stashed = git.stashCreate()
				.setIncludeUntracked(true).call();
		validateStashedCommit(stashed, 3);

		assertEquals(
				"Expected commits for workingDir,stashedIndex and untrackedFiles.",
				3, stashed.getParentCount());
		assertFalse("untracked file should be deleted.", untrackedFile.exists());
	}

	@Test
	public void untrackedFileNotIncluded() throws Exception {
		String trackedPath = "tracked.txt";
		// at least one modification needed
		writeTrashFile(trackedPath, "content2");
		git.add().addFilepattern(trackedPath).call();

		RevCommit stashed = git.stashCreate().call();
		validateStashedCommit(stashed);

		assertTrue("untracked file should be left untouched.",
				untrackedFile.exists());
		assertEquals("content", read(untrackedFile));
	}
}
