/*
 * Copyright (C) 2011, 2013 Dariusz Luksza <dariusz@luksza.org> and others
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Distribution License v. 1.0 which is available at
 * https://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */
package org.eclipse.jgit.diff;

import static org.eclipse.jgit.diff.DiffEntry.DEV_NULL;
import static org.eclipse.jgit.util.FileUtils.delete;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

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

import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.diff.DiffEntry.ChangeType;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheEditor;
import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.internal.storage.file.FileRepository;
import org.eclipse.jgit.junit.JGitTestUtil;
import org.eclipse.jgit.junit.RepositoryTestCase;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.treewalk.EmptyTreeIterator;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
import org.eclipse.jgit.util.FileUtils;
import org.junit.Test;

public class DiffEntryTest extends RepositoryTestCase {

	@Test
	public void shouldListAddedFileInInitialCommit() throws Exception {
		// given
		writeTrashFile("a.txt", "content");
		try (Git git = new Git(db);
				TreeWalk walk = new TreeWalk(db)) {
			git.add().addFilepattern("a.txt").call();
			RevCommit c = git.commit().setMessage("initial commit").call();

			// when
			walk.addTree(new EmptyTreeIterator());
			walk.addTree(c.getTree());
			List<DiffEntry> result = DiffEntry.scan(walk);

			// then
			assertThat(result, notNullValue());
			assertThat(Integer.valueOf(result.size()), is(Integer.valueOf(1)));

			DiffEntry entry = result.get(0);
			assertThat(entry.getChangeType(), is(ChangeType.ADD));
			assertThat(entry.getNewPath(), is("a.txt"));
			assertThat(entry.getOldPath(), is(DEV_NULL));
		}
	}

	@Test
	public void shouldListAddedFileBetweenTwoCommits() throws Exception {
		// given
		try (Git git = new Git(db);
				TreeWalk walk = new TreeWalk(db)) {
			RevCommit c1 = git.commit().setMessage("initial commit").call();
			writeTrashFile("a.txt", "content");
			git.add().addFilepattern("a.txt").call();
			RevCommit c2 = git.commit().setMessage("second commit").call();

			// when
			walk.addTree(c1.getTree());
			walk.addTree(c2.getTree());
			List<DiffEntry> result = DiffEntry.scan(walk);

			// then
			assertThat(result, notNullValue());
			assertThat(Integer.valueOf(result.size()), is(Integer.valueOf(1)));

			DiffEntry entry = result.get(0);
			assertThat(entry.getChangeType(), is(ChangeType.ADD));
			assertThat(entry.getNewPath(), is("a.txt"));
			assertThat(entry.getOldPath(), is(DEV_NULL));
		}
	}

	@Test
	public void shouldListModificationBetweenTwoCommits() throws Exception {
		// given
		try (Git git = new Git(db);
				TreeWalk walk = new TreeWalk(db)) {
			File file = writeTrashFile("a.txt", "content");
			git.add().addFilepattern("a.txt").call();
			RevCommit c1 = git.commit().setMessage("initial commit").call();
			write(file, "new content");
			RevCommit c2 = git.commit().setAll(true).setMessage("second commit")
					.call();

			// when
			walk.addTree(c1.getTree());
			walk.addTree(c2.getTree());
			List<DiffEntry> result = DiffEntry.scan(walk);

			// then
			assertThat(result, notNullValue());
			assertThat(Integer.valueOf(result.size()), is(Integer.valueOf(1)));

			DiffEntry entry = result.get(0);
			assertThat(entry.getChangeType(), is(ChangeType.MODIFY));
			assertThat(entry.getNewPath(), is("a.txt"));
		}
	}

	@Test
	public void shouldListDeletionBetweenTwoCommits() throws Exception {
		// given
		try (Git git = new Git(db);
				TreeWalk walk = new TreeWalk(db)) {
			File file = writeTrashFile("a.txt", "content");
			git.add().addFilepattern("a.txt").call();
			RevCommit c1 = git.commit().setMessage("initial commit").call();
			delete(file);
			RevCommit c2 = git.commit().setAll(true).setMessage("delete a.txt")
					.call();

			// when
			walk.addTree(c1.getTree());
			walk.addTree(c2.getTree());
			List<DiffEntry> result = DiffEntry.scan(walk);

			// then
			assertThat(result, notNullValue());
			assertThat(Integer.valueOf(result.size()), is(Integer.valueOf(1)));

			DiffEntry entry = result.get(0);
			assertThat(entry.getOldPath(), is("a.txt"));
			assertThat(entry.getNewPath(), is(DEV_NULL));
			assertThat(entry.getChangeType(), is(ChangeType.DELETE));
		}
	}

	@Test
	public void shouldListModificationInDirWithoutModifiedTrees()
			throws Exception {
		// given
		try (Git git = new Git(db);
				TreeWalk walk = new TreeWalk(db)) {
			File tree = new File(new File(db.getWorkTree(), "a"), "b");
			FileUtils.mkdirs(tree);
			File file = new File(tree, "c.txt");
			FileUtils.createNewFile(file);
			write(file, "content");
			git.add().addFilepattern("a").call();
			RevCommit c1 = git.commit().setMessage("initial commit").call();
			write(file, "new line");
			RevCommit c2 = git.commit().setAll(true).setMessage("second commit")
					.call();

			// when
			walk.addTree(c1.getTree());
			walk.addTree(c2.getTree());
			walk.setRecursive(true);
			List<DiffEntry> result = DiffEntry.scan(walk);

			// then
			assertThat(result, notNullValue());
			assertThat(Integer.valueOf(result.size()), is(Integer.valueOf(1)));

			DiffEntry entry = result.get(0);
			assertThat(entry.getChangeType(), is(ChangeType.MODIFY));
			assertThat(entry.getNewPath(), is("a/b/c.txt"));
		}
	}

	@Test
	public void shouldListModificationInDirWithModifiedTrees() throws Exception {
		// given
		try (Git git = new Git(db);
				TreeWalk walk = new TreeWalk(db)) {
			File tree = new File(new File(db.getWorkTree(), "a"), "b");
			FileUtils.mkdirs(tree);
			File file = new File(tree, "c.txt");
			FileUtils.createNewFile(file);
			write(file, "content");
			git.add().addFilepattern("a").call();
			RevCommit c1 = git.commit().setMessage("initial commit").call();
			write(file, "new line");
			RevCommit c2 = git.commit().setAll(true).setMessage("second commit")
					.call();

			// when
			walk.addTree(c1.getTree());
			walk.addTree(c2.getTree());
			List<DiffEntry> result = DiffEntry.scan(walk, true);

			// then
			assertThat(result, notNullValue());
			assertThat(Integer.valueOf(result.size()), is(Integer.valueOf(3)));

			DiffEntry entry = result.get(0);
			assertThat(entry.getChangeType(), is(ChangeType.MODIFY));
			assertThat(entry.getNewPath(), is("a"));

			entry = result.get(1);
			assertThat(entry.getChangeType(), is(ChangeType.MODIFY));
			assertThat(entry.getNewPath(), is("a/b"));

			entry = result.get(2);
			assertThat(entry.getChangeType(), is(ChangeType.MODIFY));
			assertThat(entry.getNewPath(), is("a/b/c.txt"));
		}
	}

	@Test
	public void shouldListChangesInWorkingTree() throws Exception {
		// given
		writeTrashFile("a.txt", "content");
		try (Git git = new Git(db);
				TreeWalk walk = new TreeWalk(db)) {
			git.add().addFilepattern("a.txt").call();
			RevCommit c = git.commit().setMessage("initial commit").call();
			writeTrashFile("b.txt", "new line");

			// when
			walk.addTree(c.getTree());
			walk.addTree(new FileTreeIterator(db));
			List<DiffEntry> result = DiffEntry.scan(walk, true);

			// then
			assertThat(Integer.valueOf(result.size()), is(Integer.valueOf(1)));
			DiffEntry entry = result.get(0);

			assertThat(entry.getChangeType(), is(ChangeType.ADD));
			assertThat(entry.getNewPath(), is("b.txt"));
		}
	}

	@Test
	public void shouldMarkEntriesWhenGivenMarkTreeFilter() throws Exception {
		// given
		try (Git git = new Git(db);
				TreeWalk walk = new TreeWalk(db)) {
			RevCommit c1 = git.commit().setMessage("initial commit").call();
			FileUtils.mkdir(new File(db.getWorkTree(), "b"));
			writeTrashFile("a.txt", "a");
			writeTrashFile("b/1.txt", "b1");
			writeTrashFile("b/2.txt", "b2");
			writeTrashFile("c.txt", "c");
			git.add().addFilepattern("a.txt").addFilepattern("b")
					.addFilepattern("c.txt").call();
			RevCommit c2 = git.commit().setMessage("second commit").call();
			TreeFilter filterA = PathFilterGroup.createFromStrings("a.txt");
			TreeFilter filterB = PathFilterGroup.createFromStrings("b");
			TreeFilter filterB2 = PathFilterGroup.createFromStrings("b/2.txt");

			// when
			walk.addTree(c1.getTree());
			walk.addTree(c2.getTree());
			List<DiffEntry> result = DiffEntry.scan(walk, true, new TreeFilter[] {
					filterA, filterB, filterB2 });

			// then
			assertThat(result, notNullValue());
			assertEquals(5, result.size());

			DiffEntry entryA = result.get(0);
			DiffEntry entryB = result.get(1);
			DiffEntry entryB1 = result.get(2);
			DiffEntry entryB2 = result.get(3);
			DiffEntry entryC = result.get(4);

			assertThat(entryA.getNewPath(), is("a.txt"));
			assertTrue(entryA.isMarked(0));
			assertFalse(entryA.isMarked(1));
			assertFalse(entryA.isMarked(2));
			assertEquals(1, entryA.getTreeFilterMarks());

			assertThat(entryB.getNewPath(), is("b"));
			assertFalse(entryB.isMarked(0));
			assertTrue(entryB.isMarked(1));
			assertTrue(entryB.isMarked(2));
			assertEquals(6, entryB.getTreeFilterMarks());

			assertThat(entryB1.getNewPath(), is("b/1.txt"));
			assertFalse(entryB1.isMarked(0));
			assertTrue(entryB1.isMarked(1));
			assertFalse(entryB1.isMarked(2));
			assertEquals(2, entryB1.getTreeFilterMarks());

			assertThat(entryB2.getNewPath(), is("b/2.txt"));
			assertFalse(entryB2.isMarked(0));
			assertTrue(entryB2.isMarked(1));
			assertTrue(entryB2.isMarked(2));
			assertEquals(6, entryB2.getTreeFilterMarks());

			assertThat(entryC.getNewPath(), is("c.txt"));
			assertFalse(entryC.isMarked(0));
			assertFalse(entryC.isMarked(1));
			assertFalse(entryC.isMarked(2));
			assertEquals(0, entryC.getTreeFilterMarks());
		}
	}

	@Test(expected = IllegalArgumentException.class)
	public void shouldThrowIAEWhenTreeWalkHasLessThanTwoTrees()
			throws Exception {
		// given - we don't need anything here

		// when
		try (TreeWalk walk = new TreeWalk(db)) {
			walk.addTree(new EmptyTreeIterator());
			DiffEntry.scan(walk);
		}
	}

	@Test(expected = IllegalArgumentException.class)
	public void shouldThrowIAEWhenTreeWalkHasMoreThanTwoTrees()
			throws Exception {
		// given - we don't need anything here

		// when
		try (TreeWalk walk = new TreeWalk(db)) {
			walk.addTree(new EmptyTreeIterator());
			walk.addTree(new EmptyTreeIterator());
			walk.addTree(new EmptyTreeIterator());
			DiffEntry.scan(walk);
		}
	}

	@Test(expected = IllegalArgumentException.class)
	public void shouldThrowIAEWhenScanShouldIncludeTreesAndWalkIsRecursive()
			throws Exception {
		// given - we don't need anything here

		// when
		try (TreeWalk walk = new TreeWalk(db)) {
			walk.addTree(new EmptyTreeIterator());
			walk.addTree(new EmptyTreeIterator());
			walk.setRecursive(true);
			DiffEntry.scan(walk, true);
		}
	}

	@Test
	public void shouldReportFileModeChange() throws Exception {
		writeTrashFile("a.txt", "content");
		try (Git git = new Git(db);
				TreeWalk walk = new TreeWalk(db)) {
			git.add().addFilepattern("a.txt").call();
			RevCommit c1 = git.commit().setMessage("initial commit").call();
			DirCache cache = db.lockDirCache();
			DirCacheEditor editor = cache.editor();
			walk.addTree(c1.getTree());
			walk.setRecursive(true);
			assertTrue(walk.next());

			editor.add(new PathEdit("a.txt") {
				@Override
				public void apply(DirCacheEntry ent) {
					ent.setFileMode(FileMode.EXECUTABLE_FILE);
					ent.setObjectId(walk.getObjectId(0));
				}
			});
			assertTrue(editor.commit());
			RevCommit c2 = git.commit().setMessage("second commit").call();
			walk.reset();
			walk.addTree(c1.getTree());
			walk.addTree(c2.getTree());
			List<DiffEntry> diffs = DiffEntry.scan(walk, false);
			assertEquals(1, diffs.size());
			DiffEntry diff = diffs.get(0);
			assertEquals(ChangeType.MODIFY,diff.getChangeType());
			assertEquals(diff.getOldId(), diff.getNewId());
			assertEquals("a.txt", diff.getOldPath());
			assertEquals(diff.getOldPath(), diff.getNewPath());
			assertEquals(FileMode.EXECUTABLE_FILE, diff.getNewMode());
			assertEquals(FileMode.REGULAR_FILE, diff.getOldMode());
		}
	}

	@Test
	public void shouldReportSubmoduleReplacedByFileMove() throws Exception {
		// Create a submodule
		FileRepository submoduleStandalone = createWorkRepository();
		JGitTestUtil.writeTrashFile(submoduleStandalone, "fileInSubmodule",
				"submodule");
		Git submoduleStandaloneGit = Git.wrap(submoduleStandalone);
		submoduleStandaloneGit.add().addFilepattern("fileInSubmodule").call();
		submoduleStandaloneGit.commit().setMessage("add file to submodule")
				.call();

		Repository submodule_db = Git.wrap(db).submoduleAdd()
				.setPath("modules/submodule")
				.setURI(submoduleStandalone.getDirectory().toURI().toString())
				.call();
		File submodule_trash = submodule_db.getWorkTree();
		addRepoToClose(submodule_db);
		writeTrashFile("fileInRoot", "root");
		Git rootGit = Git.wrap(db);
		rootGit.add().addFilepattern("fileInRoot").call();
		rootGit.commit().setMessage("add submodule and root file").call();
		// Dummy change on fileInRoot
		writeTrashFile("fileInRoot", "changed");
		rootGit.add().addFilepattern("fileInRoot").call();
		RevCommit firstCommit = rootGit.commit().setMessage("change root file")
				.call();
		// Remove the submodule again and move fileInRoot into that subfolder
		rootGit.rm().setCached(true).addFilepattern("modules/submodule").call();
		recursiveDelete(submodule_trash);
		JGitTestUtil.deleteTrashFile(db, "fileInRoot");
		// Move the fileInRoot file
		writeTrashFile("modules/submodule/fileInRoot", "changed");
		rootGit.rm().addFilepattern("fileInRoot").addFilepattern("modules/")
				.call();
		rootGit.add().addFilepattern("modules/").call();
		RevCommit secondCommit = rootGit.commit()
				.setMessage("remove submodule and move root file")
				.call();
		// Diff should report submodule having been deleted and file moved
		// (deleted and added)
		try (TreeWalk walk = new TreeWalk(db)) {
			walk.addTree(firstCommit.getTree());
			walk.addTree(secondCommit.getTree());
			walk.setRecursive(true);
			List<DiffEntry> diffs = DiffEntry.scan(walk);
			assertEquals(3, diffs.size());
			DiffEntry e = diffs.get(0);
			assertEquals(DiffEntry.ChangeType.DELETE, e.getChangeType());
			assertEquals("fileInRoot", e.getOldPath());
			e = diffs.get(1);
			assertEquals(DiffEntry.ChangeType.DELETE, e.getChangeType());
			assertEquals("modules/submodule", e.getOldPath());
			assertEquals(FileMode.GITLINK, e.getOldMode());
			e = diffs.get(2);
			assertEquals(DiffEntry.ChangeType.ADD, e.getChangeType());
			assertEquals("modules/submodule/fileInRoot", e.getNewPath());
		}

	}
}
