using System;
using System.IO;
using System.Linq;
using LibGit2Sharp.Tests.TestHelpers;
using Xunit;
using Xunit.Extensions;

namespace LibGit2Sharp.Tests
{
    public class MergeFixture : BaseFixture
    {
        [Fact]
        public void ANewRepoIsFullyMerged()
        {
            string repoPath = InitNewRepository();

            using (var repo = new Repository(repoPath))
            {
                Assert.Equal(true, repo.Index.IsFullyMerged);
            }
        }

        [Fact]
        public void AFullyMergedRepoOnlyContainsStagedIndexEntries()
        {
            string path = SandboxStandardTestRepo();
            using (var repo = new Repository(path))
            {
                Assert.Equal(true, repo.Index.IsFullyMerged);

                foreach (var entry in repo.Index)
                {
                    Assert.Equal(StageLevel.Staged, entry.StageLevel);
                }
            }
        }

        [Fact]
        public void SoftResetARepoWithUnmergedEntriesThrows()
        {
            var path = SandboxMergedTestRepo();
            using (var repo = new Repository(path))
            {
                Assert.Equal(false, repo.Index.IsFullyMerged);

                var headCommit = repo.Head.Tip;
                var firstCommitParent = headCommit.Parents.First();
                Assert.Throws<UnmergedIndexEntriesException>(
                    () => repo.Reset(ResetMode.Soft, firstCommitParent));
            }
        }

        [Fact]
        public void CommitAgainARepoWithUnmergedEntriesThrows()
        {
            var path = SandboxMergedTestRepo();
            using (var repo = new Repository(path))
            {
                Assert.Equal(false, repo.Index.IsFullyMerged);

                var author = Constants.Signature;
                Assert.Throws<UnmergedIndexEntriesException>(
                    () => repo.Commit("Try commit unmerged entries", author, author));
            }
        }

        [Fact]
        public void CanRetrieveTheBranchBeingMerged()
        {
            string path = SandboxStandardTestRepo();
            using (var repo = new Repository(path))
            {
                const string firstBranch = "9fd738e8f7967c078dceed8190330fc8648ee56a";
                const string secondBranch = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";

                Touch(repo.Info.Path, "MERGE_HEAD", string.Format("{0}{1}{2}{1}", firstBranch, "\n", secondBranch));

                Assert.Equal(CurrentOperation.Merge, repo.Info.CurrentOperation);
            }
        }

        [Theory]
        [InlineData(true)]
        [InlineData(false)]
        public void CanMergeRepoNonFastForward(bool shouldMergeOccurInDetachedHeadState)
        {
            const string firstBranchFileName = "first branch file.txt";
            const string secondBranchFileName = "second branch file.txt";
            const string sharedBranchFileName = "first+second branch file.txt";

            string path = SandboxStandardTestRepo();

            using (var repo = new Repository(path))
            {
                var firstBranch = repo.CreateBranch("FirstBranch");
                repo.Checkout(firstBranch);
                var originalTreeCount = firstBranch.Tip.Tree.Count;

                // Commit with ONE new file to both first & second branch (SecondBranch is created on this commit).
                AddFileCommitToRepo(repo, sharedBranchFileName);

                var secondBranch = repo.CreateBranch("SecondBranch");
                // Commit with ONE new file to first branch (FirstBranch moves forward as it is checked out, SecondBranch stays back one).
                AddFileCommitToRepo(repo, firstBranchFileName);

                if (shouldMergeOccurInDetachedHeadState)
                {
                    // Detaches HEAD
                    repo.Checkout(secondBranch.Tip);
                }
                else
                {
                    repo.Checkout(secondBranch);
                }

                // Commit with ONE new file to second branch (FirstBranch and SecondBranch now point to separate commits that both have the same parent commit).
                AddFileCommitToRepo(repo, secondBranchFileName);

                MergeResult mergeResult = repo.Merge(repo.Branches["FirstBranch"].Tip, Constants.Signature);

                Assert.Equal(MergeStatus.NonFastForward, mergeResult.Status);

                Assert.Equal(repo.Head.Tip, mergeResult.Commit);
                Assert.Equal(originalTreeCount + 3, mergeResult.Commit.Tree.Count);    // Expecting original tree count plussed by the 3 added files.
                Assert.Equal(2, mergeResult.Commit.Parents.Count());   // Merge commit should have 2 parents
                Assert.Equal(shouldMergeOccurInDetachedHeadState, repo.Info.IsHeadDetached);

                if (!shouldMergeOccurInDetachedHeadState)
                {
                    // Ensure HEAD is still attached and points to SecondBranch
                    Assert.Equal(repo.Refs.Head.TargetIdentifier, secondBranch.CanonicalName);
                }
            }
        }

        [Fact]
        public void IsUpToDateMerge()
        {
            const string sharedBranchFileName = "first+second branch file.txt";

            string path = SandboxStandardTestRepo();
            using (var repo = new Repository(path))
            {
                var firstBranch = repo.CreateBranch("FirstBranch");
                repo.Checkout(firstBranch);

                // Commit with ONE new file to both first & second branch (SecondBranch is created on this commit).
                AddFileCommitToRepo(repo, sharedBranchFileName);

                var secondBranch = repo.CreateBranch("SecondBranch");

                repo.Checkout(secondBranch);

                MergeResult mergeResult = repo.Merge(repo.Branches["FirstBranch"].Tip, Constants.Signature);

                Assert.Equal(MergeStatus.UpToDate, mergeResult.Status);
            }
        }

        [Theory]
        [InlineData(true)]
        [InlineData(false)]
        public void CanFastForwardRepos(bool shouldMergeOccurInDetachedHeadState)
        {
            const string firstBranchFileName = "first branch file.txt";
            const string sharedBranchFileName = "first+second branch file.txt";

            string path = SandboxStandardTestRepo();
            using (var repo = new Repository(path))
            {
                // Reset the index and the working tree.
                repo.Reset(ResetMode.Hard);

                // Clean the working directory.
                repo.RemoveUntrackedFiles();

                var firstBranch = repo.CreateBranch("FirstBranch");
                repo.Checkout(firstBranch);

                // Commit with ONE new file to both first & second branch (SecondBranch is created on this commit).
                AddFileCommitToRepo(repo, sharedBranchFileName);

                var secondBranch = repo.CreateBranch("SecondBranch");

                // Commit with ONE new file to first branch (FirstBranch moves forward as it is checked out, SecondBranch stays back one).
                AddFileCommitToRepo(repo, firstBranchFileName);

                if (shouldMergeOccurInDetachedHeadState)
                {
                    // Detaches HEAD
                    repo.Checkout(secondBranch.Tip);
                }
                else
                {
                    repo.Checkout(secondBranch);
                }

                Assert.Equal(shouldMergeOccurInDetachedHeadState, repo.Info.IsHeadDetached);

                MergeResult mergeResult = repo.Merge(repo.Branches["FirstBranch"].Tip, Constants.Signature);

                Assert.Equal(MergeStatus.FastForward, mergeResult.Status);
                Assert.Equal(repo.Branches["FirstBranch"].Tip, mergeResult.Commit);
                Assert.Equal(repo.Branches["FirstBranch"].Tip, repo.Head.Tip);
                Assert.Equal(repo.Head.Tip, mergeResult.Commit);

                Assert.Equal(0, repo.RetrieveStatus().Count());
                Assert.Equal(shouldMergeOccurInDetachedHeadState, repo.Info.IsHeadDetached);

                if (!shouldMergeOccurInDetachedHeadState)
                {
                    // Ensure HEAD is still attached and points to SecondBranch
                    Assert.Equal(repo.Refs.Head.TargetIdentifier, secondBranch.CanonicalName);
                }
            }
        }

        [Fact]
        public void ConflictingMergeRepos()
        {
            const string firstBranchFileName = "first branch file.txt";
            const string secondBranchFileName = "second branch file.txt";
            const string sharedBranchFileName = "first+second branch file.txt";

            string path = SandboxStandardTestRepo();
            using (var repo = new Repository(path))
            {
                var firstBranch = repo.CreateBranch("FirstBranch");
                repo.Checkout(firstBranch);

                // Commit with ONE new file to both first & second branch (SecondBranch is created on this commit).
                AddFileCommitToRepo(repo, sharedBranchFileName);

                var secondBranch = repo.CreateBranch("SecondBranch");
                // Commit with ONE new file to first branch (FirstBranch moves forward as it is checked out, SecondBranch stays back one).
                AddFileCommitToRepo(repo, firstBranchFileName);
                AddFileCommitToRepo(repo, sharedBranchFileName, "The first branches comment");  // Change file in first branch

                repo.Checkout(secondBranch);
                // Commit with ONE new file to second branch (FirstBranch and SecondBranch now point to separate commits that both have the same parent commit).
                AddFileCommitToRepo(repo, secondBranchFileName);
                AddFileCommitToRepo(repo, sharedBranchFileName, "The second branches comment");  // Change file in second branch

                MergeResult mergeResult = repo.Merge(repo.Branches["FirstBranch"].Tip, Constants.Signature);

                Assert.Equal(MergeStatus.Conflicts, mergeResult.Status);

                Assert.Null(mergeResult.Commit);
                Assert.Equal(1, repo.Index.Conflicts.Count());

                var conflict = repo.Index.Conflicts.First();
                var changes = repo.Diff.Compare(repo.Lookup<Blob>(conflict.Theirs.Id), repo.Lookup<Blob>(conflict.Ours.Id));

                Assert.False(changes.IsBinaryComparison);
            }
        }

        [Fact]
        public void ConflictingMergeReposBinary()
        {
            const string firstBranchFileName = "first branch file.bin";
            const string secondBranchFileName = "second branch file.bin";
            const string sharedBranchFileName = "first+second branch file.bin";

            string path = SandboxStandardTestRepo();
            using (var repo = new Repository(path))
            {
                var firstBranch = repo.CreateBranch("FirstBranch");
                repo.Checkout(firstBranch);

                // Commit with ONE new file to both first & second branch (SecondBranch is created on this commit).
                AddFileCommitToRepo(repo, sharedBranchFileName);

                var secondBranch = repo.CreateBranch("SecondBranch");
                // Commit with ONE new file to first branch (FirstBranch moves forward as it is checked out, SecondBranch stays back one).
                AddFileCommitToRepo(repo, firstBranchFileName);
                AddFileCommitToRepo(repo, sharedBranchFileName, "\0The first branches comment\0");  // Change file in first branch

                repo.Checkout(secondBranch);
                // Commit with ONE new file to second branch (FirstBranch and SecondBranch now point to separate commits that both have the same parent commit).
                AddFileCommitToRepo(repo, secondBranchFileName);
                AddFileCommitToRepo(repo, sharedBranchFileName, "\0The second branches comment\0");  // Change file in second branch

                MergeResult mergeResult = repo.Merge(repo.Branches["FirstBranch"].Tip, Constants.Signature);

                Assert.Equal(MergeStatus.Conflicts, mergeResult.Status);

                Assert.Equal(1, repo.Index.Conflicts.Count());

                Conflict conflict = repo.Index.Conflicts.First();

                var changes = repo.Diff.Compare(repo.Lookup<Blob>(conflict.Theirs.Id), repo.Lookup<Blob>(conflict.Ours.Id));

                Assert.True(changes.IsBinaryComparison);
            }
        }

        [Theory]
        [InlineData(true, FastForwardStrategy.Default, fastForwardBranchInitialId, MergeStatus.FastForward)]
        [InlineData(true, FastForwardStrategy.FastForwardOnly, fastForwardBranchInitialId, MergeStatus.FastForward)]
        [InlineData(false, FastForwardStrategy.Default, fastForwardBranchInitialId, MergeStatus.FastForward)]
        [InlineData(false, FastForwardStrategy.FastForwardOnly, fastForwardBranchInitialId, MergeStatus.FastForward)]
        public void CanFastForwardCommit(bool fromDetachedHead, FastForwardStrategy fastForwardStrategy, string expectedCommitId, MergeStatus expectedMergeStatus)
        {
            string path = SandboxMergeTestRepo();
            using (var repo = new Repository(path))
            {
                if(fromDetachedHead)
                {
                    repo.Checkout(repo.Head.Tip.Id.Sha);
                }

                Commit commitToMerge = repo.Branches["fast_forward"].Tip;

                MergeResult result = repo.Merge(commitToMerge, Constants.Signature, new MergeOptions() { FastForwardStrategy = fastForwardStrategy });

                Assert.Equal(expectedMergeStatus, result.Status);
                Assert.Equal(expectedCommitId, result.Commit.Id.Sha);
                Assert.False(repo.RetrieveStatus().Any());
                Assert.Equal(fromDetachedHead, repo.Info.IsHeadDetached);
            }
        }

        [Theory]
        [InlineData(true, FastForwardStrategy.Default, MergeStatus.NonFastForward)]
        [InlineData(true, FastForwardStrategy.NoFastForward, MergeStatus.NonFastForward)]
        [InlineData(false, FastForwardStrategy.Default, MergeStatus.NonFastForward)]
        [InlineData(false, FastForwardStrategy.NoFastForward, MergeStatus.NonFastForward)]
        public void CanNonFastForwardMergeCommit(bool fromDetachedHead, FastForwardStrategy fastForwardStrategy, MergeStatus expectedMergeStatus)
        {
            string path = SandboxMergeTestRepo();
            using (var repo = new Repository(path))
            {
                if (fromDetachedHead)
                {
                    repo.Checkout(repo.Head.Tip.Id.Sha);
                }

                Commit commitToMerge = repo.Branches["normal_merge"].Tip;

                MergeResult result = repo.Merge(commitToMerge, Constants.Signature, new MergeOptions() { FastForwardStrategy = fastForwardStrategy });

                Assert.Equal(expectedMergeStatus, result.Status);
                Assert.False(repo.RetrieveStatus().Any());
                Assert.Equal(fromDetachedHead, repo.Info.IsHeadDetached);
            }
        }

        [Fact]
        public void MergeReportsCheckoutProgress()
        {
            string repoPath = SandboxMergeTestRepo();
            using (var repo = new Repository(repoPath))
            {
                Commit commitToMerge = repo.Branches["normal_merge"].Tip;

                bool wasCalled = false;

                MergeOptions options = new MergeOptions()
                {
                    OnCheckoutProgress = (path, completed, total) => wasCalled = true,
                };

                MergeResult result = repo.Merge(commitToMerge, Constants.Signature, options);

                Assert.True(wasCalled);
            }
        }

        [Fact]
        public void MergeReportsCheckoutNotifications()
        {
            string repoPath = SandboxMergeTestRepo();
            using (var repo = new Repository(repoPath))
            {
                Commit commitToMerge = repo.Branches["normal_merge"].Tip;

                bool wasCalled = false;
                CheckoutNotifyFlags actualNotifyFlags = CheckoutNotifyFlags.None;

                MergeOptions options = new MergeOptions()
                {
                    OnCheckoutNotify = (path, notificationType) => { wasCalled = true; actualNotifyFlags = notificationType; return true; },
                    CheckoutNotifyFlags = CheckoutNotifyFlags.Updated,
                };

                MergeResult result = repo.Merge(commitToMerge, Constants.Signature, options);

                Assert.True(wasCalled);
                Assert.Equal(CheckoutNotifyFlags.Updated, actualNotifyFlags);
            }
        }

        [Fact]
        public void FastForwardMergeReportsCheckoutProgress()
        {
            string repoPath = SandboxMergeTestRepo();
            using (var repo = new Repository(repoPath))
            {
                Commit commitToMerge = repo.Branches["fast_forward"].Tip;

                bool wasCalled = false;

                MergeOptions options = new MergeOptions()
                {
                    OnCheckoutProgress = (path, completed, total) => wasCalled = true,
                };

                MergeResult result = repo.Merge(commitToMerge, Constants.Signature, options);

                Assert.True(wasCalled);
            }
        }

        [Fact]
        public void FastForwardMergeReportsCheckoutNotifications()
        {
            string repoPath = SandboxMergeTestRepo();
            using (var repo = new Repository(repoPath))
            {
                Commit commitToMerge = repo.Branches["fast_forward"].Tip;

                bool wasCalled = false;
                CheckoutNotifyFlags actualNotifyFlags = CheckoutNotifyFlags.None;

                MergeOptions options = new MergeOptions()
                {
                    OnCheckoutNotify = (path, notificationType) => { wasCalled = true; actualNotifyFlags = notificationType; return true; },
                    CheckoutNotifyFlags = CheckoutNotifyFlags.Updated,
                };

                MergeResult result = repo.Merge(commitToMerge, Constants.Signature, options);

                Assert.True(wasCalled);
                Assert.Equal(CheckoutNotifyFlags.Updated, actualNotifyFlags);
            }
        }

        [Fact]
        public void MergeCanDetectRenames()
        {
            // The environment is set up such that:
            // file b.txt is edited in the "rename" branch and
            // edited and renamed in the "rename_source" branch.
            // The edits are automergable.
            // We can rename "rename_source" into "rename"
            // if rename detection is enabled,
            // but the merge will fail with conflicts if this
            // change is not detected as a rename.

            string repoPath = SandboxMergeTestRepo();
            using (var repo = new Repository(repoPath))
            {
                Branch currentBranch = repo.Checkout("rename_source");
                Assert.NotNull(currentBranch);

                Branch branchToMerge = repo.Branches["rename"];

                MergeResult result = repo.Merge(branchToMerge, Constants.Signature);

                Assert.Equal(MergeStatus.NonFastForward, result.Status);
            }
        }

        [Fact]
        public void FastForwardNonFastForwardableMergeThrows()
        {
            string path = SandboxMergeTestRepo();
            using (var repo = new Repository(path))
            {
                Commit commitToMerge = repo.Branches["normal_merge"].Tip;
                Assert.Throws<NonFastForwardException>(() => repo.Merge(commitToMerge, Constants.Signature, new MergeOptions() { FastForwardStrategy = FastForwardStrategy.FastForwardOnly }));
            }
        }

        [Fact]
        public void CanForceFastForwardMergeThroughConfig()
        {
            string path = SandboxMergeTestRepo();
            using (var repo = new Repository(path))
            {
                repo.Config.Set("merge.ff", "only");

                Commit commitToMerge = repo.Branches["normal_merge"].Tip;
                Assert.Throws<NonFastForwardException>(() => repo.Merge(commitToMerge, Constants.Signature, new MergeOptions()));
            }
        }

        [Fact]
        public void CanMergeAndNotCommit()
        {
            string path = SandboxMergeTestRepo();
            using (var repo = new Repository(path))
            {
                Commit commitToMerge = repo.Branches["normal_merge"].Tip;

                MergeResult result = repo.Merge(commitToMerge, Constants.Signature, new MergeOptions() { CommitOnSuccess = false});

                Assert.Equal(MergeStatus.NonFastForward, result.Status);
                Assert.Equal(null, result.Commit);

                RepositoryStatus repoStatus = repo.RetrieveStatus();

                // Verify that there is a staged entry.
                Assert.Equal(1, repoStatus.Count());
                Assert.Equal(FileStatus.ModifiedInIndex, repo.RetrieveStatus("b.txt"));
            }
        }

        [Fact]
        public void CanForceNonFastForwardMerge()
        {
            string path = SandboxMergeTestRepo();
            using (var repo = new Repository(path))
            {
                Commit commitToMerge = repo.Branches["fast_forward"].Tip;

                MergeResult result = repo.Merge(commitToMerge, Constants.Signature, new MergeOptions() { FastForwardStrategy = FastForwardStrategy.NoFastForward });

                Assert.Equal(MergeStatus.NonFastForward, result.Status);
                Assert.Equal("f58f780d5a0ae392efd4a924450b1bbdc0577d32", result.Commit.Id.Sha);
                Assert.False(repo.RetrieveStatus().Any());
            }
        }

        [Fact]
        public void CanForceNonFastForwardMergeThroughConfig()
        {
            string path = SandboxMergeTestRepo();
            using (var repo = new Repository(path))
            {
                repo.Config.Set("merge.ff", "false");

                Commit commitToMerge = repo.Branches["fast_forward"].Tip;

                MergeResult result = repo.Merge(commitToMerge, Constants.Signature, new MergeOptions());

                Assert.Equal(MergeStatus.NonFastForward, result.Status);
                Assert.Equal("f58f780d5a0ae392efd4a924450b1bbdc0577d32", result.Commit.Id.Sha);
                Assert.False(repo.RetrieveStatus().Any());
            }
        }

        [Fact]
        public void VerifyUpToDateMerge()
        {
            string path = SandboxMergeTestRepo();
            using (var repo = new Repository(path))
            {
                Commit commitToMerge = repo.Branches["master"].Tip;

                MergeResult result = repo.Merge(commitToMerge, Constants.Signature, new MergeOptions() { FastForwardStrategy = FastForwardStrategy.NoFastForward });

                Assert.Equal(MergeStatus.UpToDate, result.Status);
                Assert.Equal(null, result.Commit);
                Assert.False(repo.RetrieveStatus().Any());
            }
        }

        [Theory]
        [InlineData("refs/heads/normal_merge", FastForwardStrategy.Default, MergeStatus.NonFastForward)]
        [InlineData("normal_merge", FastForwardStrategy.Default, MergeStatus.NonFastForward)]
        [InlineData("625186280ed2a6ec9b65d250ed90cf2e4acef957", FastForwardStrategy.Default, MergeStatus.NonFastForward)]
        [InlineData("fast_forward", FastForwardStrategy.Default, MergeStatus.FastForward)]
        public void CanMergeCommittish(string committish, FastForwardStrategy strategy, MergeStatus expectedMergeStatus)
        {
            string path = SandboxMergeTestRepo();
            using (var repo = new Repository(path))
            {
                MergeResult result = repo.Merge(committish, Constants.Signature, new MergeOptions() { FastForwardStrategy = strategy });

                Assert.Equal(expectedMergeStatus, result.Status);
                Assert.False(repo.RetrieveStatus().Any());
            }
        }

        [Theory]
        [InlineData(true, FastForwardStrategy.FastForwardOnly)]
        [InlineData(false, FastForwardStrategy.FastForwardOnly)]
        [InlineData(true, FastForwardStrategy.NoFastForward)]
        [InlineData(false, FastForwardStrategy.NoFastForward)]
        public void MergeWithWorkDirConflictsThrows(bool shouldStage, FastForwardStrategy strategy)
        {
            // Merging the fast_forward branch results in a change to file
            // b.txt. In this test we modify the file in the working directory
            // and then attempt to perform a merge. We expect the merge to fail
            // due to checkout conflicts.
            string committishToMerge = "fast_forward";

            using (var repo = new Repository(SandboxMergeTestRepo()))
            {
                Touch(repo.Info.WorkingDirectory, "b.txt", "this is an alternate change");

                if (shouldStage)
                {
                    repo.Stage("b.txt");
                }

                Assert.Throws<CheckoutConflictException>(() => repo.Merge(committishToMerge, Constants.Signature, new MergeOptions() { FastForwardStrategy = strategy }));
            }
        }

        [Theory]
        [InlineData(CheckoutFileConflictStrategy.Ours)]
        [InlineData(CheckoutFileConflictStrategy.Theirs)]
        public void CanSpecifyConflictFileStrategy(CheckoutFileConflictStrategy conflictStrategy)
        {
            const string conflictFile = "a.txt";
            const string conflictBranchName = "conflicts";

            string path = SandboxMergeTestRepo();
            using (var repo = new Repository(path))
            {
                Branch branch = repo.Branches[conflictBranchName];
                Assert.NotNull(branch);

                MergeOptions mergeOptions = new MergeOptions()
                {
                    FileConflictStrategy = conflictStrategy,
                };

                MergeResult result = repo.Merge(branch, Constants.Signature, mergeOptions);
                Assert.Equal(MergeStatus.Conflicts, result.Status);

                // Get the information on the conflict.
                Conflict conflict = repo.Index.Conflicts[conflictFile];

                Assert.NotNull(conflict);
                Assert.NotNull(conflict.Theirs);
                Assert.NotNull(conflict.Ours);

                // Get the blob containing the expected content.
                Blob expectedBlob = null;
                switch(conflictStrategy)
                {
                    case CheckoutFileConflictStrategy.Theirs:
                        expectedBlob = repo.Lookup<Blob>(conflict.Theirs.Id);
                        break;
                    case CheckoutFileConflictStrategy.Ours:
                        expectedBlob = repo.Lookup<Blob>(conflict.Ours.Id);
                        break;
                    default:
                        throw new Exception("Unexpected FileConflictStrategy");
                }

                Assert.NotNull(expectedBlob);

                // Check the content of the file on disk matches what is expected.
                string expectedContent = expectedBlob.GetContentText(new FilteringOptions(conflictFile));
                Assert.Equal(expectedContent, File.ReadAllText(Path.Combine(repo.Info.WorkingDirectory, conflictFile)));
            }
        }

        [Theory]
        [InlineData(MergeFileFavor.Ours)]
        [InlineData(MergeFileFavor.Theirs)]
        public void MergeCanSpecifyMergeFileFavorOption(MergeFileFavor fileFavorFlag)
        {
            const string conflictFile = "a.txt";
            const string conflictBranchName = "conflicts";

            string path = SandboxMergeTestRepo();
            using (var repo = InitIsolatedRepository(path))
            {
                Branch branch = repo.Branches[conflictBranchName];
                Assert.NotNull(branch);

                var status = repo.RetrieveStatus();
                MergeOptions mergeOptions = new MergeOptions()
                {
                    MergeFileFavor = fileFavorFlag,
                };

                MergeResult result = repo.Merge(branch, Constants.Signature, mergeOptions);

                Assert.Equal(MergeStatus.NonFastForward, result.Status);

                // Verify that the index and working directory are clean
                Assert.True(repo.Index.IsFullyMerged);
                Assert.False(repo.RetrieveStatus().IsDirty);

                // Get the blob containing the expected content.
                Blob expectedBlob = null;
                switch (fileFavorFlag)
                {
                    case MergeFileFavor.Theirs:
                        expectedBlob = repo.Lookup<Blob>("3dd9738af654bbf1c363f6c3bbc323bacdefa179");
                        break;
                    case MergeFileFavor.Ours:
                        expectedBlob = repo.Lookup<Blob>("610b16886ca829cebd2767d9196f3c4378fe60b5");
                        break;
                    default:
                        throw new Exception("Unexpected MergeFileFavor");
                }

                Assert.NotNull(expectedBlob);

                // Verify the index has the expected contents
                IndexEntry entry = repo.Index[conflictFile];
                Assert.NotNull(entry);
                Assert.Equal(expectedBlob.Id, entry.Id);

                // Verify the content of the file on disk matches what is expected.
                string expectedContent = expectedBlob.GetContentText(new FilteringOptions(conflictFile));
                Assert.Equal(expectedContent, File.ReadAllText(Path.Combine(repo.Info.WorkingDirectory, conflictFile)));
            }
        }

        [Theory]
        [InlineData("refs/heads/normal_merge", FastForwardStrategy.Default, MergeStatus.NonFastForward)]
        [InlineData("fast_forward", FastForwardStrategy.Default, MergeStatus.FastForward)]
        public void CanMergeBranch(string branchName, FastForwardStrategy strategy, MergeStatus expectedMergeStatus)
        {
            string path = SandboxMergeTestRepo();
            using (var repo = new Repository(path))
            {
                Branch branch = repo. Branches[branchName];
                MergeResult result = repo.Merge(branch, Constants.Signature, new MergeOptions() { FastForwardStrategy = strategy });

                Assert.Equal(expectedMergeStatus, result.Status);
                Assert.False(repo.RetrieveStatus().Any());
            }
        }

        [Fact]
        public void CanMergeIntoOrphanedBranch()
        {
            string path = SandboxMergeTestRepo();
            using (var repo = new Repository(path))
            {
                repo.Refs.Add("HEAD", "refs/heads/orphan", true);

                // Remove entries from the working directory
                foreach(var entry in repo.RetrieveStatus())
                {
                    repo.Unstage(entry.FilePath);
                    repo.Remove(entry.FilePath, true);
                }

                // Assert that we have an empty working directory.
                Assert.False(repo.RetrieveStatus().Any());

                MergeResult result = repo.Merge("master", Constants.Signature);

                Assert.Equal(MergeStatus.FastForward, result.Status);
                Assert.Equal(masterBranchInitialId, result.Commit.Id.Sha);
                Assert.False(repo.RetrieveStatus().Any());
            }
        }


        [Fact]
        public void CanMergeTreeIntoSameTree()
        {
            string path = SandboxMergeTestRepo();
            using (var repo = new Repository(path))
            {
                var master = repo.Branches["master"].Tip;

                var result = repo.ObjectDatabase.MergeCommits(master, master, null);
                Assert.Equal(MergeTreeStatus.Succeeded, result.Status);
                Assert.Equal(0, result.Conflicts.Count());
            }
        }

        [Fact]
        public void CanMergeTreeIntoTreeFromUnbornBranch()
        {
            string path = SandboxMergeTestRepo();
            using (var repo = new Repository(path))
            {
                repo.Refs.UpdateTarget("HEAD", "refs/heads/unborn");

                Touch(repo.Info.WorkingDirectory, "README", "Yeah!\n");
                repo.Index.Clear();
                repo.Stage("README");

                repo.Commit("A new world, free of the burden of the history", Constants.Signature, Constants.Signature);

                var master = repo.Branches["master"].Tip;
                var branch = repo.Branches["unborn"].Tip;

                var result = repo.ObjectDatabase.MergeCommits(master, branch, null);
                Assert.Equal(MergeTreeStatus.Succeeded, result.Status);
                Assert.NotNull(result.Tree);
                Assert.Equal(0, result.Conflicts.Count());
            }
        }

        [Fact]
        public void CanMergeCommitsAndDetectConflicts()
        {
            string path = SandboxMergeTestRepo();
            using (var repo = new Repository(path))
            {
                repo.Refs.UpdateTarget("HEAD", "refs/heads/unborn");

                repo.Index.Replace(repo.Lookup<Commit>("conflicts"));

                repo.Commit("A conflicting world, free of the burden of the history", Constants.Signature, Constants.Signature);

                var master = repo.Branches["master"].Tip;
                var branch = repo.Branches["unborn"].Tip;

                var result = repo.ObjectDatabase.MergeCommits(master, branch, null);
                Assert.Equal(MergeTreeStatus.Conflicts, result.Status);
                Assert.Null(result.Tree);
                Assert.NotEqual(0, result.Conflicts.Count());
            }
        }

        [Fact]
        public void CanMergeFastForwardTreeWithoutConflicts()
        {
            string path = SandboxMergeTestRepo();
            using (var repo = new Repository(path))
            {
                var master = repo.Lookup<Commit>("master");
                var branch = repo.Lookup<Commit>("fast_forward");

                var result = repo.ObjectDatabase.MergeCommits(master, branch, null);
                Assert.Equal(MergeTreeStatus.Succeeded, result.Status);
                Assert.NotNull(result.Tree);
                Assert.Equal(0, result.Conflicts.Count());
            }
        }

        [Fact]
        public void CanIdentifyConflictsInMergeCommits()
        {
            string path = SandboxMergeTestRepo();
            using (var repo = new Repository(path))
            {
                var master = repo.Lookup<Commit>("master");
                var branch = repo.Lookup<Commit>("conflicts");

                var result = repo.ObjectDatabase.MergeCommits(master, branch, null);

                Assert.Equal(MergeTreeStatus.Conflicts, result.Status);

                Assert.Null(result.Tree);
                Assert.Equal(1, result.Conflicts.Count());

                var conflict = result.Conflicts.First();
                Assert.Equal(new ObjectId("8e9daea300fbfef6c0da9744c6214f546d55b279"), conflict.Ancestor.Id);
                Assert.Equal(new ObjectId("610b16886ca829cebd2767d9196f3c4378fe60b5"), conflict.Ours.Id);
                Assert.Equal(new ObjectId("3dd9738af654bbf1c363f6c3bbc323bacdefa179"), conflict.Theirs.Id);
            }
        }

        private Commit AddFileCommitToRepo(IRepository repository, string filename, string content = null)
        {
            Touch(repository.Info.WorkingDirectory, filename, content);

            repository.Stage(filename);

            return repository.Commit("New commit", Constants.Signature, Constants.Signature);
        }

        // Commit IDs of the checked in merge_testrepo
        private const string masterBranchInitialId = "83cebf5389a4adbcb80bda6b68513caee4559802";
        private const string fastForwardBranchInitialId = "4dfaa1500526214ae7b33f9b2c1144ca8b6b1f53";
    }
}
