/*
 * Copyright (C) 2012, Christian Halstrick <christian.halstrick@sap.com> 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.merge;

import static java.nio.charset.StandardCharsets.UTF_8;

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

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;

import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheEditor;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.errors.NoMergeBaseException;
import org.eclipse.jgit.errors.NoMergeBaseException.MergeBaseFailureReason;
import org.eclipse.jgit.internal.storage.file.FileRepository;
import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
import org.eclipse.jgit.junit.RepositoryTestCase;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.junit.TestRepository.BranchBuilder;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevBlob;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.junit.Before;
import org.junit.experimental.theories.DataPoints;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;

@RunWith(Theories.class)
public class CrissCrossMergeTest extends RepositoryTestCase {
	static int counter = 0;

	@DataPoints
	public static MergeStrategy[] strategiesUnderTest = new MergeStrategy[] {
			MergeStrategy.RECURSIVE, MergeStrategy.RESOLVE };

	public enum IndexState {
		Bare, Missing, SameAsHead, SameAsOther, SameAsWorkTree, DifferentFromHeadAndOtherAndWorktree
	}

	@DataPoints
	public static IndexState[] indexStates = IndexState.values();

	public enum WorktreeState {
		Bare, Missing, SameAsHead, DifferentFromHeadAndOther, SameAsOther;
	}

	@DataPoints
	public static WorktreeState[] worktreeStates = WorktreeState.values();

	private TestRepository<FileRepository> db_t;

	@Override
	@Before
	public void setUp() throws Exception {
		super.setUp();
		db_t = new TestRepository<>(db);
	}

	@Theory
	/**
	 * Merging m2,s2 from the following topology. In master and side different
	 * files are touched. No need to do a real content merge.
	 *
	 * <pre>
	 * m0--m1--m2
	 *   \   \/
	 *    \  /\
	 *     s1--s2
	 * </pre>
	 */
	public void crissCrossMerge(MergeStrategy strategy, IndexState indexState,
			WorktreeState worktreeState) throws Exception {
		if (!validateStates(indexState, worktreeState))
			return;
		// fill the repo
		BranchBuilder master = db_t.branch("master");
		RevCommit m0 = master.commit().add("m", ",m0").message("m0").create();
		RevCommit m1 = master.commit().add("m", "m1").message("m1").create();
		db_t.getRevWalk().parseCommit(m1);

		BranchBuilder side = db_t.branch("side");
		RevCommit s1 = side.commit().parent(m0).add("s", "s1").message("s1")
				.create();
		RevCommit s2 = side.commit().parent(m1).add("m", "m1")
				.message("s2(merge)").create();
		RevCommit m2 = master.commit().parent(s1).add("s", "s1")
				.message("m2(merge)").create();

		Git git = Git.wrap(db);
		git.checkout().setName("master").call();
		modifyWorktree(worktreeState, "m", "side");
		modifyWorktree(worktreeState, "s", "side");
		modifyIndex(indexState, "m", "side");
		modifyIndex(indexState, "s", "side");

		ResolveMerger merger = (ResolveMerger) strategy.newMerger(db,
				worktreeState == WorktreeState.Bare);
		if (worktreeState != WorktreeState.Bare)
			merger.setWorkingTreeIterator(new FileTreeIterator(db));
		try {
			boolean expectSuccess = true;
			if (!(indexState == IndexState.Bare
					|| indexState == IndexState.Missing
					|| indexState == IndexState.SameAsHead || indexState == IndexState.SameAsOther))
				// index is dirty
				expectSuccess = false;

			assertEquals(Boolean.valueOf(expectSuccess),
					Boolean.valueOf(merger.merge(new RevCommit[] { m2, s2 })));
			assertEquals(MergeStrategy.RECURSIVE, strategy);
			assertEquals("m1",
					contentAsString(db, merger.getResultTreeId(), "m"));
			assertEquals("s1",
					contentAsString(db, merger.getResultTreeId(), "s"));
		} catch (NoMergeBaseException e) {
			assertEquals(MergeStrategy.RESOLVE, strategy);
			assertEquals(e.getReason(),
					MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED);
		}
	}

	@Theory
	/**
	 * Merging m2,s2 from the following topology. m1 and s1 are the two root
	 * commits of the repo. In master and side different files are touched.
	 * No need to do a real content merge.
	 *
	 * <pre>
	 * m1--m2
	 *   \/
	 *   /\
	 * s1--s2
	 * </pre>
	 */
	public void crissCrossMerge_twoRoots(MergeStrategy strategy,
			IndexState indexState, WorktreeState worktreeState)
			throws Exception {
		if (!validateStates(indexState, worktreeState))
			return;
		// fill the repo
		BranchBuilder master = db_t.branch("master");
		BranchBuilder side = db_t.branch("side");
		RevCommit m1 = master.commit().add("m", "m1").message("m1").create();
		db_t.getRevWalk().parseCommit(m1);

		RevCommit s1 = side.commit().add("s", "s1").message("s1").create();
		RevCommit s2 = side.commit().parent(m1).add("m", "m1")
				.message("s2(merge)").create();
		RevCommit m2 = master.commit().parent(s1).add("s", "s1")
				.message("m2(merge)").create();

		Git git = Git.wrap(db);
		git.checkout().setName("master").call();
		modifyWorktree(worktreeState, "m", "side");
		modifyWorktree(worktreeState, "s", "side");
		modifyIndex(indexState, "m", "side");
		modifyIndex(indexState, "s", "side");

		ResolveMerger merger = (ResolveMerger) strategy.newMerger(db,
				worktreeState == WorktreeState.Bare);
		if (worktreeState != WorktreeState.Bare)
			merger.setWorkingTreeIterator(new FileTreeIterator(db));
		try {
			boolean expectSuccess = true;
			if (!(indexState == IndexState.Bare
					|| indexState == IndexState.Missing
					|| indexState == IndexState.SameAsHead || indexState == IndexState.SameAsOther))
				// index is dirty
				expectSuccess = false;

			assertEquals(Boolean.valueOf(expectSuccess),
					Boolean.valueOf(merger.merge(new RevCommit[] { m2, s2 })));
			assertEquals(MergeStrategy.RECURSIVE, strategy);
			assertEquals("m1",
					contentAsString(db, merger.getResultTreeId(), "m"));
			assertEquals("s1",
					contentAsString(db, merger.getResultTreeId(), "s"));
		} catch (NoMergeBaseException e) {
			assertEquals(MergeStrategy.RESOLVE, strategy);
			assertEquals(e.getReason(),
					MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED);
		}
	}

	@Theory
	/**
	 * Merging m2,s2 from the following topology. The same file is modified
	 * in both branches. The modifications should be mergeable. m2 and s2
	 * contain branch specific conflict resolutions. Therefore m2 and s2 don't contain the same content.
	 *
	 * <pre>
	 * m0--m1--m2
	 *   \   \/
	 *    \  /\
	 *     s1--s2
	 * </pre>
	 */
	public void crissCrossMerge_mergeable(MergeStrategy strategy,
			IndexState indexState, WorktreeState worktreeState)
			throws Exception {
		if (!validateStates(indexState, worktreeState))
			return;

		BranchBuilder master = db_t.branch("master");
		RevCommit m0 = master.commit().add("f", "1\n2\n3\n4\n5\n6\n7\n8\n9\n")
				.message("m0").create();
		RevCommit m1 = master.commit()
				.add("f", "1-master\n2\n3\n4\n5\n6\n7\n8\n9\n").message("m1")
				.create();
		db_t.getRevWalk().parseCommit(m1);

		BranchBuilder side = db_t.branch("side");
		RevCommit s1 = side.commit().parent(m0)
				.add("f", "1\n2\n3\n4\n5\n6\n7\n8\n9-side\n").message("s1")
				.create();
		RevCommit s2 = side.commit().parent(m1)
				.add("f", "1-master\n2\n3\n4\n5\n6\n7-res(side)\n8\n9-side\n")
				.message("s2(merge)").create();
		RevCommit m2 = master
				.commit()
				.parent(s1)
				.add("f", "1-master\n2\n3-res(master)\n4\n5\n6\n7\n8\n9-side\n")
				.message("m2(merge)").create();

		Git git = Git.wrap(db);
		git.checkout().setName("master").call();
		modifyWorktree(worktreeState, "f", "side");
		modifyIndex(indexState, "f", "side");

		ResolveMerger merger = (ResolveMerger) strategy.newMerger(db,
				worktreeState == WorktreeState.Bare);
		if (worktreeState != WorktreeState.Bare)
			merger.setWorkingTreeIterator(new FileTreeIterator(db));
		try {
			boolean expectSuccess = true;
			if (!(indexState == IndexState.Bare
					|| indexState == IndexState.Missing || indexState == IndexState.SameAsHead))
				// index is dirty
				expectSuccess = false;
			else if (worktreeState == WorktreeState.DifferentFromHeadAndOther
					|| worktreeState == WorktreeState.SameAsOther)
				expectSuccess = false;
			assertEquals(Boolean.valueOf(expectSuccess),
					Boolean.valueOf(merger.merge(new RevCommit[] { m2, s2 })));
			assertEquals(MergeStrategy.RECURSIVE, strategy);
			if (!expectSuccess)
				// if the merge was not successful skip testing the state of index and workingtree
				return;
			assertEquals(
					"1-master\n2\n3-res(master)\n4\n5\n6\n7-res(side)\n8\n9-side",
					contentAsString(db, merger.getResultTreeId(), "f"));
			if (indexState != IndexState.Bare)
				assertEquals(
						"[f, mode:100644, content:1-master\n2\n3-res(master)\n4\n5\n6\n7-res(side)\n8\n9-side\n]",
						indexState(LocalDiskRepositoryTestCase.CONTENT));
			if (worktreeState != WorktreeState.Bare
					&& worktreeState != WorktreeState.Missing)
				assertEquals(
						"1-master\n2\n3-res(master)\n4\n5\n6\n7-res(side)\n8\n9-side\n",
						read("f"));
		} catch (NoMergeBaseException e) {
			assertEquals(MergeStrategy.RESOLVE, strategy);
			assertEquals(e.getReason(),
					MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED);
		}
	}

	@Theory
	/**
	 * Merging m2,s2 from the following topology. The same file is modified
	 * in both branches. The modifications should be mergeable but only if the automerge of m1 and s1
	 * is choosen as parent. Choosing m0 as parent would not be sufficient (in contrast to the merge in
	 * crissCrossMerge_mergeable). m2 and s2 contain branch specific conflict resolutions. Therefore m2
	 * and s2 don't contain the same content.
	 *
	 * <pre>
	 * m0--m1--m2
	 *   \   \/
	 *    \  /\
	 *     s1--s2
	 * </pre>
	 */
	public void crissCrossMerge_mergeable2(MergeStrategy strategy,
			IndexState indexState, WorktreeState worktreeState)
			throws Exception {
		if (!validateStates(indexState, worktreeState))
			return;

		BranchBuilder master = db_t.branch("master");
		RevCommit m0 = master.commit().add("f", "1\n2\n3\n")
				.message("m0")
				.create();
		RevCommit m1 = master.commit().add("f", "1-master\n2\n3\n")
				.message("m1").create();
		db_t.getRevWalk().parseCommit(m1);

		BranchBuilder side = db_t.branch("side");
		RevCommit s1 = side.commit().parent(m0).add("f", "1\n2\n3-side\n")
				.message("s1").create();
		RevCommit s2 = side.commit().parent(m1)
				.add("f", "1-master\n2\n3-side-r\n")
				.message("s2(merge)")
				.create();
		RevCommit m2 = master.commit().parent(s1)
				.add("f", "1-master-r\n2\n3-side\n")
				.message("m2(merge)")
				.create();

		Git git = Git.wrap(db);
		git.checkout().setName("master").call();
		modifyWorktree(worktreeState, "f", "side");
		modifyIndex(indexState, "f", "side");

		ResolveMerger merger = (ResolveMerger) strategy.newMerger(db,
				worktreeState == WorktreeState.Bare);
		if (worktreeState != WorktreeState.Bare)
			merger.setWorkingTreeIterator(new FileTreeIterator(db));
		try {
			boolean expectSuccess = true;
			if (!(indexState == IndexState.Bare
					|| indexState == IndexState.Missing || indexState == IndexState.SameAsHead))
				// index is dirty
				expectSuccess = false;
			else if (worktreeState == WorktreeState.DifferentFromHeadAndOther
					|| worktreeState == WorktreeState.SameAsOther)
				expectSuccess = false;
			assertEquals(Boolean.valueOf(expectSuccess),
					Boolean.valueOf(merger.merge(new RevCommit[] { m2, s2 })));
			assertEquals(MergeStrategy.RECURSIVE, strategy);
			if (!expectSuccess)
				// if the merge was not successful skip testing the state of
				// index and workingtree
				return;
			assertEquals(
					"1-master-r\n2\n3-side-r",
					contentAsString(db, merger.getResultTreeId(), "f"));
			if (indexState != IndexState.Bare)
				assertEquals(
						"[f, mode:100644, content:1-master-r\n2\n3-side-r\n]",
						indexState(LocalDiskRepositoryTestCase.CONTENT));
			if (worktreeState != WorktreeState.Bare
					&& worktreeState != WorktreeState.Missing)
				assertEquals(
						"1-master-r\n2\n3-side-r\n",
						read("f"));
		} catch (NoMergeBaseException e) {
			assertEquals(MergeStrategy.RESOLVE, strategy);
			assertEquals(e.getReason(),
					MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED);
		}
	}

	@Theory
	/**
	 * Merging m2,s2 from the following topology. m1 and s1 are not mergeable
	 * without conflicts. The same file is modified in both branches. The
	 * modifications should be mergeable but only if the merge result of
	 * merging m1 and s1 is choosen as parent (including the conflict markers).
	 *
	 * <pre>
	 * m0--m1--m2
	 *   \   \/
	 *    \  /\
	 *     s1--s2
	 * </pre>
	 */
	public void crissCrossMerge_ParentsNotMergeable(MergeStrategy strategy,
			IndexState indexState, WorktreeState worktreeState)
			throws Exception {
		if (!validateStates(indexState, worktreeState))
			return;

		BranchBuilder master = db_t.branch("master");
		RevCommit m0 = master.commit().add("f", "1\n2\n3\n").message("m0")
				.create();
		RevCommit m1 = master.commit().add("f", "1\nx(master)\n2\n3\n")
				.message("m1").create();
		db_t.getRevWalk().parseCommit(m1);

		BranchBuilder side = db_t.branch("side");
		RevCommit s1 = side.commit().parent(m0)
				.add("f", "1\nx(side)\n2\n3\ny(side)\n")
				.message("s1").create();
		RevCommit s2 = side.commit().parent(m1)
				.add("f", "1\nx(side)\n2\n3\ny(side-again)\n")
				.message("s2(merge)")
				.create();
		RevCommit m2 = master.commit().parent(s1)
				.add("f", "1\nx(side)\n2\n3\ny(side)\n").message("m2(merge)")
				.create();

		Git git = Git.wrap(db);
		git.checkout().setName("master").call();
		modifyWorktree(worktreeState, "f", "side");
		modifyIndex(indexState, "f", "side");

		ResolveMerger merger = (ResolveMerger) strategy.newMerger(db,
				worktreeState == WorktreeState.Bare);
		if (worktreeState != WorktreeState.Bare)
			merger.setWorkingTreeIterator(new FileTreeIterator(db));
		try {
			boolean expectSuccess = true;
			if (!(indexState == IndexState.Bare
					|| indexState == IndexState.Missing || indexState == IndexState.SameAsHead))
				// index is dirty
				expectSuccess = false;
			else if (worktreeState == WorktreeState.DifferentFromHeadAndOther
					|| worktreeState == WorktreeState.SameAsOther)
				expectSuccess = false;
			assertEquals("Merge didn't return as expected: strategy:"
					+ strategy.getName() + ", indexState:" + indexState
					+ ", worktreeState:" + worktreeState + " . ",
					Boolean.valueOf(expectSuccess),
					Boolean.valueOf(merger.merge(new RevCommit[] { m2, s2 })));
			assertEquals(MergeStrategy.RECURSIVE, strategy);
			if (!expectSuccess)
				// if the merge was not successful skip testing the state of
				// index and workingtree
				return;
			assertEquals("1\nx(side)\n2\n3\ny(side-again)",
					contentAsString(db, merger.getResultTreeId(), "f"));
			if (indexState != IndexState.Bare)
				assertEquals(
						"[f, mode:100644, content:1\nx(side)\n2\n3\ny(side-again)\n]",
						indexState(LocalDiskRepositoryTestCase.CONTENT));
			if (worktreeState != WorktreeState.Bare
					&& worktreeState != WorktreeState.Missing)
				assertEquals("1\nx(side)\n2\n3\ny(side-again)\n", read("f"));
		} catch (NoMergeBaseException e) {
			assertEquals(MergeStrategy.RESOLVE, strategy);
			assertEquals(e.getReason(),
					MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED);
		}
	}

	@Theory
	/**
	 * Merging m2,s2 from the following topology. The same file is modified
	 * in both branches. The modifications should be mergeable but only if the automerge of m1 and s1
	 * is choosen as parent. On both branches delete and modify files untouched on the other branch.
	 * On both branches create new files. Make sure these files are correctly merged and
	 * exist in the workingtree.
	 *
	 * <pre>
	 * m0--m1--m2
	 *   \   \/
	 *    \  /\
	 *     s1--s2
	 * </pre>
	 */
	public void crissCrossMerge_checkOtherFiles(MergeStrategy strategy,
			IndexState indexState, WorktreeState worktreeState)
			throws Exception {
		if (!validateStates(indexState, worktreeState))
			return;

		BranchBuilder master = db_t.branch("master");
		RevCommit m0 = master.commit().add("f", "1\n2\n3\n").add("m.m", "0")
				.add("m.d", "0").add("s.m", "0").add("s.d", "0").message("m0")
				.create();
		RevCommit m1 = master.commit().add("f", "1-master\n2\n3\n")
				.add("m.c", "0").add("m.m", "1").rm("m.d").message("m1")
				.create();
		db_t.getRevWalk().parseCommit(m1);

		BranchBuilder side = db_t.branch("side");
		RevCommit s1 = side.commit().parent(m0).add("f", "1\n2\n3-side\n")
				.add("s.c", "0").add("s.m", "1").rm("s.d").message("s1")
				.create();
		RevCommit s2 = side.commit().parent(m1)
				.add("f", "1-master\n2\n3-side-r\n").add("m.m", "1")
				.add("m.c", "0").rm("m.d").message("s2(merge)").create();
		RevCommit m2 = master.commit().parent(s1)
				.add("f", "1-master-r\n2\n3-side\n").add("s.m", "1")
				.add("s.c", "0").rm("s.d").message("m2(merge)").create();

		Git git = Git.wrap(db);
		git.checkout().setName("master").call();
		modifyWorktree(worktreeState, "f", "side");
		modifyIndex(indexState, "f", "side");

		ResolveMerger merger = (ResolveMerger) strategy.newMerger(db,
				worktreeState == WorktreeState.Bare);
		if (worktreeState != WorktreeState.Bare)
			merger.setWorkingTreeIterator(new FileTreeIterator(db));
		try {
			boolean expectSuccess = true;
			if (!(indexState == IndexState.Bare
					|| indexState == IndexState.Missing || indexState == IndexState.SameAsHead))
				// index is dirty
				expectSuccess = false;
			else if (worktreeState == WorktreeState.DifferentFromHeadAndOther
					|| worktreeState == WorktreeState.SameAsOther)
				expectSuccess = false;
			assertEquals(Boolean.valueOf(expectSuccess),
					Boolean.valueOf(merger.merge(new RevCommit[] { m2, s2 })));
			assertEquals(MergeStrategy.RECURSIVE, strategy);
			if (!expectSuccess)
				// if the merge was not successful skip testing the state of
				// index and workingtree
				return;
			assertEquals(
					"1-master-r\n2\n3-side-r",
					contentAsString(db, merger.getResultTreeId(), "f"));
			if (indexState != IndexState.Bare)
				assertEquals(
						"[f, mode:100644, content:1-master-r\n2\n3-side-r\n][m.c, mode:100644, content:0][m.m, mode:100644, content:1][s.c, mode:100644, content:0][s.m, mode:100644, content:1]",
						indexState(LocalDiskRepositoryTestCase.CONTENT));
			if (worktreeState != WorktreeState.Bare
					&& worktreeState != WorktreeState.Missing) {
				assertEquals(
						"1-master-r\n2\n3-side-r\n",
						read("f"));
				assertTrue(check("s.c"));
				assertFalse(check("s.d"));
				assertTrue(check("s.m"));
				assertTrue(check("m.c"));
				assertFalse(check("m.d"));
				assertTrue(check("m.m"));
			}
		} catch (NoMergeBaseException e) {
			assertEquals(MergeStrategy.RESOLVE, strategy);
			assertEquals(e.getReason(),
					MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED);
		}
	}

	@Theory
	/**
	 * Merging m2,s2 from the following topology. The same file is modified
	 * in both branches. The modifications are not automatically
	 * mergeable. m2 and s2 contain branch specific conflict resolutions.
	 * Therefore m2 and s2 don't contain the same content.
	 *
	 * <pre>
	 * m0--m1--m2
	 *   \   \/
	 *    \  /\
	 *     s1--s2
	 * </pre>
	 */
	public void crissCrossMerge_nonmergeable(MergeStrategy strategy,
			IndexState indexState, WorktreeState worktreeState)
			throws Exception {
		if (!validateStates(indexState, worktreeState))
			return;

		BranchBuilder master = db_t.branch("master");
		RevCommit m0 = master.commit().add("f", "1\n2\n3\n4\n5\n6\n7\n8\n9\n")
				.message("m0").create();
		RevCommit m1 = master.commit()
				.add("f", "1-master\n2\n3\n4\n5\n6\n7\n8\n9\n").message("m1")
				.create();
		db_t.getRevWalk().parseCommit(m1);

		BranchBuilder side = db_t.branch("side");
		RevCommit s1 = side.commit().parent(m0)
				.add("f", "1\n2\n3\n4\n5\n6\n7\n8\n9-side\n").message("s1")
				.create();
		RevCommit s2 = side.commit().parent(m1)
				.add("f", "1-master\n2\n3\n4\n5\n6\n7-res(side)\n8\n9-side\n")
				.message("s2(merge)").create();
		RevCommit m2 = master.commit().parent(s1)
				.add("f", "1-master\n2\n3\n4\n5\n6\n7-conflict\n8\n9-side\n")
				.message("m2(merge)").create();

		Git git = Git.wrap(db);
		git.checkout().setName("master").call();
		modifyWorktree(worktreeState, "f", "side");
		modifyIndex(indexState, "f", "side");

		ResolveMerger merger = (ResolveMerger) strategy.newMerger(db,
				worktreeState == WorktreeState.Bare);
		if (worktreeState != WorktreeState.Bare)
			merger.setWorkingTreeIterator(new FileTreeIterator(db));
		try {
			assertFalse(merger.merge(new RevCommit[] { m2, s2 }));
			assertEquals(MergeStrategy.RECURSIVE, strategy);
			if (indexState == IndexState.SameAsHead
					&& worktreeState == WorktreeState.SameAsHead) {
				assertEquals(
						"[f, mode:100644, stage:1, content:1-master\n2\n3\n4\n5\n6\n7\n8\n9-side\n]"
								+ "[f, mode:100644, stage:2, content:1-master\n2\n3\n4\n5\n6\n7-conflict\n8\n9-side\n]"
								+ "[f, mode:100644, stage:3, content:1-master\n2\n3\n4\n5\n6\n7-res(side)\n8\n9-side\n]",
						indexState(LocalDiskRepositoryTestCase.CONTENT));
				assertEquals(
						"1-master\n2\n3\n4\n5\n6\n<<<<<<< OURS\n7-conflict\n=======\n7-res(side)\n>>>>>>> THEIRS\n8\n9-side\n",
						read("f"));
			}
		} catch (NoMergeBaseException e) {
			assertEquals(MergeStrategy.RESOLVE, strategy);
			assertEquals(e.getReason(),
					MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED);
		}
	}

	@Theory
	/**
	 * Merging m2,s2 which have three common predecessors.The same file is modified
	 * in all branches. The modifications should be mergeable. m2 and s2
	 * contain branch specific conflict resolutions. Therefore m2 and s2
	 * don't contain the same content.
	 *
	 * <pre>
	 *     m1-----m2
	 *    /  \/  /
	 *   /   /\ /
	 * m0--o1  x
	 *   \   \/ \
	 *    \  /\  \
	 *     s1-----s2
	 * </pre>
	 */
	public void crissCrossMerge_ThreeCommonPredecessors(MergeStrategy strategy,
			IndexState indexState, WorktreeState worktreeState)
			throws Exception {
		if (!validateStates(indexState, worktreeState))
			return;

		BranchBuilder master = db_t.branch("master");
		RevCommit m0 = master.commit().add("f", "1\n2\n3\n4\n5\n6\n7\n8\n9\n")
				.message("m0").create();
		RevCommit m1 = master.commit()
				.add("f", "1-master\n2\n3\n4\n5\n6\n7\n8\n9\n").message("m1")
				.create();
		BranchBuilder side = db_t.branch("side");
		RevCommit s1 = side.commit().parent(m0)
				.add("f", "1\n2\n3\n4\n5\n6\n7\n8\n9-side\n").message("s1")
				.create();
		BranchBuilder other = db_t.branch("other");
		RevCommit o1 = other.commit().parent(m0)
				.add("f", "1\n2\n3\n4\n5-other\n6\n7\n8\n9\n").message("o1")
				.create();

		RevCommit m2 = master
				.commit()
				.parent(s1)
				.parent(o1)
				.add("f",
						"1-master\n2\n3-res(master)\n4\n5-other\n6\n7\n8\n9-side\n")
				.message("m2(merge)").create();

		RevCommit s2 = side
				.commit()
				.parent(m1)
				.parent(o1)
				.add("f",
						"1-master\n2\n3\n4\n5-other\n6\n7-res(side)\n8\n9-side\n")
				.message("s2(merge)").create();

		Git git = Git.wrap(db);
		git.checkout().setName("master").call();
		modifyWorktree(worktreeState, "f", "side");
		modifyIndex(indexState, "f", "side");

		ResolveMerger merger = (ResolveMerger) strategy.newMerger(db,
				worktreeState == WorktreeState.Bare);
		if (worktreeState != WorktreeState.Bare)
			merger.setWorkingTreeIterator(new FileTreeIterator(db));
		try {
			boolean expectSuccess = true;
			if (!(indexState == IndexState.Bare
					|| indexState == IndexState.Missing || indexState == IndexState.SameAsHead))
				// index is dirty
				expectSuccess = false;
			else if (worktreeState == WorktreeState.DifferentFromHeadAndOther
					|| worktreeState == WorktreeState.SameAsOther)
				// workingtree is dirty
				expectSuccess = false;

			assertEquals(Boolean.valueOf(expectSuccess),
					Boolean.valueOf(merger.merge(new RevCommit[] { m2, s2 })));
			assertEquals(MergeStrategy.RECURSIVE, strategy);
			if (!expectSuccess)
				// if the merge was not successful skip testing the state of index and workingtree
				return;
			assertEquals(
					"1-master\n2\n3-res(master)\n4\n5-other\n6\n7-res(side)\n8\n9-side",
					contentAsString(db, merger.getResultTreeId(), "f"));
			if (indexState != IndexState.Bare)
				assertEquals(
						"[f, mode:100644, content:1-master\n2\n3-res(master)\n4\n5-other\n6\n7-res(side)\n8\n9-side\n]",
						indexState(LocalDiskRepositoryTestCase.CONTENT));
			if (worktreeState != WorktreeState.Bare
					&& worktreeState != WorktreeState.Missing)
				assertEquals(
						"1-master\n2\n3-res(master)\n4\n5-other\n6\n7-res(side)\n8\n9-side\n",
						read("f"));
		} catch (NoMergeBaseException e) {
			assertEquals(MergeStrategy.RESOLVE, strategy);
			assertEquals(e.getReason(),
					MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED);
		}
	}

	void modifyIndex(IndexState indexState, String path, String other)
			throws Exception {
		RevBlob blob;
		switch (indexState) {
		case Missing:
			setIndex(null, path);
			break;
		case SameAsHead:
			setIndex(contentId(Constants.HEAD, path), path);
			break;
		case SameAsOther:
			setIndex(contentId(other, path), path);
			break;
		case SameAsWorkTree:
			blob = db_t.blob(read(path));
			setIndex(blob, path);
			break;
		case DifferentFromHeadAndOtherAndWorktree:
			blob = db_t.blob(Integer.toString(counter++));
			setIndex(blob, path);
			break;
		case Bare:
			File file = new File(db.getDirectory(), "index");
			if (!file.exists())
				return;
			db.close();
			file.delete();
			db = new FileRepository(db.getDirectory());
			db_t = new TestRepository<>(db);
			break;
		}
	}

	private void setIndex(ObjectId id, String path)
			throws MissingObjectException, IOException {
		DirCache lockedDircache;
		DirCacheEditor dcedit;

		lockedDircache = db.lockDirCache();
		dcedit = lockedDircache.editor();
		try {
			if (id != null) {
				final ObjectLoader contLoader = db.newObjectReader().open(id);
				dcedit.add(new DirCacheEditor.PathEdit(path) {
					@Override
					public void apply(DirCacheEntry ent) {
						ent.setFileMode(FileMode.REGULAR_FILE);
						ent.setLength(contLoader.getSize());
						ent.setObjectId(id);
					}
				});
			} else
				dcedit.add(new DirCacheEditor.DeletePath(path));
		} finally {
			dcedit.commit();
		}
	}

	private ObjectId contentId(String revName, String path) throws Exception {
		RevCommit headCommit = db_t.getRevWalk().parseCommit(
				db.resolve(revName));
		db_t.parseBody(headCommit);
		return db_t.get(headCommit.getTree(), path).getId();
	}

	void modifyWorktree(WorktreeState worktreeState, String path, String other)
			throws Exception {
		switch (worktreeState) {
		case Missing:
			new File(db.getWorkTree(), path).delete();
			break;
		case DifferentFromHeadAndOther:
			write(new File(db.getWorkTree(), path),
					Integer.toString(counter++));
			break;
		case SameAsHead:
			try (FileOutputStream fos = new FileOutputStream(
					new File(db.getWorkTree(), path))) {
				db.newObjectReader().open(contentId(Constants.HEAD, path))
						.copyTo(fos);
			}
			break;
		case SameAsOther:
			try (FileOutputStream fos = new FileOutputStream(
					new File(db.getWorkTree(), path))) {
				db.newObjectReader().open(contentId(other, path)).copyTo(fos);
			}
			break;
		case Bare:
			if (db.isBare())
				return;
			File workTreeFile = db.getWorkTree();
			db.getConfig().setBoolean("core", null, "bare", true);
			db.getDirectory().renameTo(new File(workTreeFile, "test.git"));
			db = new FileRepository(new File(workTreeFile, "test.git"));
			db_t = new TestRepository<>(db);
		}
	}

	private boolean validateStates(IndexState indexState,
			WorktreeState worktreeState) {
		if (worktreeState == WorktreeState.Bare
				&& indexState != IndexState.Bare)
			return false;
		if (worktreeState != WorktreeState.Bare
				&& indexState == IndexState.Bare)
			return false;
		if (worktreeState != WorktreeState.DifferentFromHeadAndOther
				&& indexState == IndexState.SameAsWorkTree)
			// would be a duplicate: the combination WorktreeState.X and
			// IndexState.X already covered this
			return false;
		return true;
	}

	private String contentAsString(Repository r, ObjectId treeId, String path)
			throws MissingObjectException, IOException {
		AnyObjectId blobId;
		try (TreeWalk tw = new TreeWalk(r)) {
			tw.addTree(treeId);
			tw.setFilter(PathFilter.create(path));
			tw.setRecursive(true);
			if (!tw.next()) {
				return null;
			}
			blobId = tw.getObjectId(0);
		}

		StringBuilder result = new StringBuilder();
		try (ObjectReader or = r.newObjectReader();
				BufferedReader br = new BufferedReader(new InputStreamReader(
						or.open(blobId).openStream(), UTF_8))) {
			String line;
			boolean first = true;
			while ((line = br.readLine()) != null) {
				if (!first) {
					result.append('\n');
				}
				result.append(line);
				first = false;
			}
			return result.toString();
		}
	}
}
