package com.atlassian.bitbucket.mesh.transaction;

import com.atlassian.bitbucket.mesh.AbstractWiredExecutionContextTest;
import com.atlassian.bitbucket.mesh.config.ApplicationConfig;
import com.atlassian.bitbucket.mesh.execution.ExecutionManager;
import com.atlassian.bitbucket.mesh.git.builder.GitProcessBuilderFactory;
import com.atlassian.bitbucket.mesh.git.builder.updateref.UpdateRefStdioHandler;
import com.atlassian.bitbucket.mesh.git.hook.*;
import com.atlassian.bitbucket.mesh.git.hook.handler.AbstractGitHookHandler;
import com.atlassian.bitbucket.mesh.git.ref.RefUpdate;
import com.atlassian.bitbucket.mesh.io.MoreFiles;
import com.atlassian.bitbucket.mesh.process.handler.LoggingStdioHandler;
import com.atlassian.bitbucket.mesh.process.handler.StringStdioHandler;
import com.atlassian.bitbucket.mesh.repository.RefHasher;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.atlassian.bitbucket.mesh.test.annotation.DirtiesRepo;
import com.atlassian.bitbucket.mesh.test.annotation.Repo;
import com.google.common.collect.ImmutableList;
import org.junit.Before;
import org.junit.Test;

import javax.annotation.Nonnull;
import javax.annotation.Resource;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import static org.hamcrest.Matchers.containsInAnyOrder;

@DirtiesRepo //"symbolic" is created in setUp, so every test in this class implicitly dirties the repository
public class GitReferenceTransactionHookHandlerTest extends AbstractWiredExecutionContextTest {

    private static final GitHookHandler PRE_RECEIVE = new AbstractGitHookHandler(GitHook.PRE_RECEIVE) {

        @Override
        public boolean handle(@Nonnull Repository repository, @Nonnull HookRequest request,
                              @Nonnull GitHookOutputWriter outputWriter) {
            return true;
        }
    };

    @Resource
    private GitProcessBuilderFactory builderFactory;
    @Resource
    private ApplicationConfig config;
    @Resource
    private GitHookService gitHookService;
    @Resource
    private RefHasher refHasher;
    @Repo(type = Repo.Type.MANAGED)
    private Repository repository;

    @Before
    public void setUp() {
        //Create a symbolic ref to verify that symbolic refs are NOT provided to the reference-transaction hooks
        //when the branch they reference is updated
        builderFactory.builder(repository).symbolicRef()
                .set("refs/heads/symbolic", "refs/heads/master")
                .build()
                .run();
    }

    @Test
    public void testOutputGitVersion() {
        //Out the actual version being used to make it super clear in the Git Matrix build which version
        //was picked up by Mesh.
        System.out.println("Testing reference-transaction hook behavior with git version " +
                builderFactory.builder().version()
                        .build(new StringStdioHandler())
                        .run());
    }

    @Test
    public void testCreateBranch() {
        createBranchAndVerifyCallbacks();
    }

    @Test
    public void testDeleteBranchUsingBranchDeleteOnlyLoose() {
        deleteBranchUsingBranchDeleteAndVerifyCallbacks(false);
    }

    @Test
    public void testDeleteBranchUsingBranchDeleteOnlyPacked() {
        packRefs(true);
        deleteBranchUsingBranchDeleteAndVerifyCallbacks(false);
    }

    @Test
    public void testDeleteBranchUsingBranchDeletePackedAndLoose() {
        packRefs(false);
        deleteBranchUsingBranchDeleteAndVerifyCallbacks(false);
    }

    @Test
    public void testDeleteBranchUsingPushOnlyLoose() {
        deleteBranchUsingPushAndVerifyCallbacks(false);
    }

    @Test
    public void testDeleteBranchUsingPushOnlyPacked() {
        packRefs(true);
        deleteBranchUsingPushAndVerifyCallbacks(false);
    }

    @Test
    public void testDeleteBranchUsingPushPackedAndLoose() {
        packRefs(false);
        deleteBranchUsingPushAndVerifyCallbacks(false);
    }

    @Test
    public void testDeleteBranchUsingUpdateRefOnlyLoose() {
        deleteBranchUsingUpdateRefAndVerifyCallbacks(false);
    }

    @Test
    public void testDeleteBranchUsingUpdateRefOnlyPacked() {
        packRefs(true);
        deleteBranchUsingUpdateRefAndVerifyCallbacks(false);
    }

    @Test
    public void testDeleteBranchUsingUpdateRefPackedAndLoose() {
        packRefs(false);
        deleteBranchUsingUpdateRefAndVerifyCallbacks(false);
    }

    @Test
    public void testDeleteBranchesUsingBranchDeleteSomePackedAndSomeLoose() {
        packSomeRefs();

        List<ReferenceTransaction> transactions = capturingTransactionCallbacks(
                handle -> handle.configure(builderFactory.builder(repository)
                        .command("branch")
                        .argument("-d")
                        .argument("branch_mod_merge")
                        .argument("out_of_order_branch"))
                        .build(new LoggingStdioHandler())
                        .run());

        if (config.getGit().getCompatibility().isRefDeleteCallbacksBroken()) {
            //On git versions lower than 2.37, git branch -d is broken in how it invokes the reference-transaction
            //hooks. We have a workaround that defers the packed-refs COMMITTED callback, but that has the side effect
            //that we get a single PREPARED containing all deleted refs, followed by a single COMMITTED callback.
            assertPreparedAndCommitted(transactions,
                    RefUpdate.ofDelete("refs/heads/branch_mod_merge", "6053a1eaa1c009dd11092d09a72f3c41af1b59ad"),
                    RefUpdate.ofDelete("refs/heads/out_of_order_branch", "2e10dd2d1d5eea9291b296e78312e8a703964a95"));
        } else {
            assertPreparedAndCommitted(transactions.subList(0, 2),
                    RefUpdate.ofDelete("refs/heads/branch_mod_merge", "6053a1eaa1c009dd11092d09a72f3c41af1b59ad"));
            assertPreparedAndCommitted(transactions.subList(2, 4),
                    RefUpdate.ofDelete("refs/heads/out_of_order_branch", "2e10dd2d1d5eea9291b296e78312e8a703964a95"));
        }
    }

    @DirtiesRepo
    @Test
    public void testDeleteBranchesUsingPushAtomicOnlyLoose() {
        deleteBranchesUsingPushAtomicAndVerifyCallbacks(false);
    }

    @DirtiesRepo
    @Test
    public void testDeleteBranchesUsingPushAtomicOnlyPacked() {
        packRefs(true);
        deleteBranchesUsingPushAtomicAndVerifyCallbacks(false);
    }

    @DirtiesRepo
    @Test
    public void testDeleteBranchesUsingPushAtomicPackedAndLoose() {
        packRefs(false);
        deleteBranchesUsingPushAtomicAndVerifyCallbacks(false);
    }

    @DirtiesRepo
    @Test
    public void testDeleteBranchesUsingPushAtomicSomePackedAndSomeLoose() {
        packSomeRefs();
        deleteBranchesUsingPushAtomicAndVerifyCallbacks(false);
    }

    @DirtiesRepo
    @Test
    public void testDeleteBranchesUsingPushOnlyLoose() {
        deleteBranchesUsingPushAndVerifyCallbacks(false);
    }

    @Test
    public void testDeleteBranchesUsingPushOnlyPacked() {
        packRefs(true);
        deleteBranchesUsingPushAndVerifyCallbacks(false);
    }

    @Test
    public void testDeleteBranchesUsingPushPackedAndLoose() {
        packRefs(false);
        deleteBranchesUsingPushAndVerifyCallbacks(false);
    }

    @Test
    public void testDeleteBranchesUsingPushSomePackedAndSomeLoose() {
        packSomeRefs();
        deleteBranchesUsingPushAndVerifyCallbacks(false);
    }

    @DirtiesRepo
    @Test
    public void testDeleteBranchesUsingUpdateRefOnlyLoose() {
        deleteBranchesUsingUpdateRefAndVerifyCallbacks(false);
    }

    @Test
    public void testDeleteBranchesUsingUpdateRefOnlyPacked() {
        packRefs(true);
        deleteBranchesUsingUpdateRefAndVerifyCallbacks(false);
    }

    @Test
    public void testDeleteBranchesUsingUpdateRefPackedAndLoose() {
        packRefs(false);
        deleteBranchesUsingUpdateRefAndVerifyCallbacks(false);
    }

    @Test
    public void testDeleteBranchesUsingUpdateRefSomePackedAndSomeLoose() {
        packSomeRefs();
        deleteBranchesUsingUpdateRefAndVerifyCallbacks(false);
    }

    @DirtiesRepo
    @Test
    public void testDeleteTagUsingTagDeleteOnlyLoose() {
        deleteTagUsingTagDeleteAndVerifyCallbacks();
    }

    @DirtiesRepo
    @Test
    public void testDeleteTagUsingTagDeleteOnlyPacked() {
        packRefs(true);
        deleteTagUsingTagDeleteAndVerifyCallbacks();
    }

    @DirtiesRepo
    @Test
    public void testDeleteTagUsingTagDeletePackedAndLoose() {
        packRefs(false);
        deleteTagUsingTagDeleteAndVerifyCallbacks();
    }

    @DirtiesRepo
    @Test
    public void testForceDeleteBranchUsingBranchDeleteOnlyLoose() {
        deleteBranchUsingBranchDeleteAndVerifyCallbacks(true);
    }

    @Test
    public void testForceDeleteBranchUsingBranchDeleteOnlyPacked() {
        packRefs(true);
        deleteBranchUsingBranchDeleteAndVerifyCallbacks(true);
    }

    @Test
    public void testForceDeleteBranchUsingBranchDeletePackedAndLoose() {
        packRefs(false);
        deleteBranchUsingBranchDeleteAndVerifyCallbacks(true);
    }

    @Test
    public void testForceDeleteBranchUsingPushOnlyLoose() {
        deleteBranchUsingPushAndVerifyCallbacks(true);
    }

    @Test
    public void testForceDeleteBranchUsingPushOnlyPacked() {
        packRefs(true);
        deleteBranchUsingPushAndVerifyCallbacks(true);
    }

    @Test
    public void testForceDeleteBranchUsingPushPackedAndLoose() {
        packRefs(false);
        deleteBranchUsingPushAndVerifyCallbacks(true);
    }

    @Test
    public void testForceDeleteBranchUsingUpdateRefOnlyLoose() {
        deleteBranchUsingUpdateRefAndVerifyCallbacks(true);
    }

    @Test
    public void testForceDeleteBranchUsingUpdateRefOnlyPacked() {
        packRefs(true);
        deleteBranchUsingUpdateRefAndVerifyCallbacks(true);
    }

    @Test
    public void testForceDeleteBranchUsingUpdateRefPackedAndLoose() {
        packRefs(false);
        deleteBranchUsingUpdateRefAndVerifyCallbacks(true);
    }

    @Test
    public void testForceDeleteBranchesUsingPushAtomicOnlyLoose() {
        deleteBranchesUsingPushAtomicAndVerifyCallbacks(true);
    }

    @DirtiesRepo
    @Test
    public void testForceDeleteBranchesUsingPushAtomicOnlyPacked() {
        packRefs(true);
        deleteBranchesUsingPushAtomicAndVerifyCallbacks(true);
    }

    @DirtiesRepo
    @Test
    public void testForceDeleteBranchesUsingPushAtomicPackedAndLoose() {
        packRefs(false);
        deleteBranchesUsingPushAtomicAndVerifyCallbacks(true);
    }

    @DirtiesRepo
    @Test
    public void testForceDeleteBranchesUsingPushOnlyLoose() {
        deleteBranchesUsingPushAndVerifyCallbacks(true);
    }

    @Test
    public void testForceDeleteBranchesUsingPushOnlyPacked() {
        packRefs(true);
        deleteBranchesUsingPushAndVerifyCallbacks(true);
    }

    @Test
    public void testForceDeleteBranchesUsingPushPackedAndLoose() {
        packRefs(false);
        deleteBranchesUsingPushAndVerifyCallbacks(true);
    }

    @Test
    public void testForceDeleteBranchesUsingUpdateRefOnlyLoose() {
        deleteBranchesUsingUpdateRefAndVerifyCallbacks(true);
    }

    @Test
    public void testForceDeleteBranchesUsingUpdateRefOnlyPacked() {
        packRefs(true);
        deleteBranchesUsingUpdateRefAndVerifyCallbacks(true);
    }

    @Test
    public void testForceDeleteBranchesUsingUpdateRefPackedAndLoose() {
        packRefs(false);
        deleteBranchesUsingUpdateRefAndVerifyCallbacks(true);
    }

    @Test
    public void testForceUpdateBranchOnlyLoose() {
        updateOutOfOrderBranch(true);
    }

    @Test
    public void testForceUpdateBranchOnlyPacked() {
        packRefs(true);
        updateOutOfOrderBranch(true);
    }

    @Test
    public void testForceUpdateBranchPackedAndLoose() {
        packRefs(false);
        updateOutOfOrderBranch(true);
    }

    @Test
    public void testMixedUpdateOnlyLoose() {
        mixedUpdateAndVerifyCallbacks(false);
    }

    @Test
    public void testMixedUpdateOnlyPacked() {
        packRefs(true);
        mixedUpdateAndVerifyCallbacks(false);
    }

    @Test
    public void testMixedUpdatePackedAndLoose() {
        packRefs(false);
        mixedUpdateAndVerifyCallbacks(false);
    }

    @Test
    public void testMixedUpdateSomePackedAndSomeLoose() {
        packSomeRefs();
        mixedUpdateAndVerifyCallbacks(false);
    }

    @DirtiesRepo
    @Test
    public void testMultipleGitCallsInSameContext() {
        CapturingReferenceTransactionListener listener = new CapturingReferenceTransactionListener();
        GitHookHandler handler = handler(listener);

        //First update master and out_of_order_branch
        try (HookRequestHandle handle = gitHookService.registerRequest(repository, handler)) {
            List<String> instructions = ImmutableList.of(
                    "delete refs/heads/master\0",
                    "update refs/heads/out_of_order_branch\09fbc34a0d905950131d73f352abe68520c6db2a3\0");

            handle.configure(builderFactory.builder(repository)
                    .updateRef()
                    .stdin()
                    .deref(false)
                    .nullTerminated(true))
                    .build(new UpdateRefStdioHandler(instructions.stream(), true))
                    .run();
        }
        assertPreparedAndCommitted(listener.getTransactions(),
                RefUpdate.ofDelete("refs/heads/master", "0a943a29376f2336b78312d99e65da17048951db"),
                RefUpdate.of("refs/heads/out_of_order_branch", "2e10dd2d1d5eea9291b296e78312e8a703964a95",
                        "9fbc34a0d905950131d73f352abe68520c6db2a3"));

        //Now update just out_of_order_branch with the _same_ handler and verify that we get the correct callbacks
        listener.reset();
        try (HookRequestHandle handle = gitHookService.registerRequest(repository, handler)) {
            handle.configure(builderFactory.builder(repository)
                    .updateRef()
                    .set("refs/heads/out_of_order_branch", "e26cab95a50f6e99d2c46c498a23cee307cc8e27"))
                    .build()
                    .run();
        }

        assertPreparedAndCommitted(listener.getTransactions(),
                RefUpdate.of("refs/heads/out_of_order_branch", "9fbc34a0d905950131d73f352abe68520c6db2a3",
                        "e26cab95a50f6e99d2c46c498a23cee307cc8e27"));
    }

    @Test
    public void testNonAtomicForcePush() {
        pushMultipleUpdatesAndVerifyCallbacks(true);
    }

    @Test
    public void testNonAtomicForcePushWithDeletes() {
        pushMultipleUpdatesAndVerifyCallbacksWithDeletes(true);
    }

    @Test
    public void testNonAtomicPush() {
        pushMultipleUpdatesAndVerifyCallbacks(false);
    }

    @Test
    public void testNonAtomicPushWithDeletes() {
        pushMultipleUpdatesAndVerifyCallbacksWithDeletes(false);
    }

    @Test
    public void testPackRefs() {
        List<ReferenceTransaction> transactions = capturingTransactionCallbacks(handle ->
                handle.configure(builderFactory.builder(repository)
                        .packRefs()
                        .all(true)
                        .prune(true))
                        .build(new LoggingStdioHandler())
                        .run());

        //pack-refs does not trigger a PREPARED callback, and COMMIT/ABORTED transactions without a PREPARED are not
        //propagated to the listeners because they have no publicly visible changes
        assertTrue(transactions.isEmpty());
    }

    @Test
    public void testUpdateBranchOnlyLoose() {
        updateOutOfOrderBranch(false);
    }

    @Test
    public void testUpdateBranchOnlyPacked() {
        packRefs(true);
        updateOutOfOrderBranch(false);
    }

    @Test
    public void testUpdateBranchPackedAndLoose() {
        packRefs(false);
        updateOutOfOrderBranch(false);
    }

    private static void assertPreparedAndCommitted(List<ReferenceTransaction> transactions,
                                                   RefUpdate... refUpdates) {
        assertPreparedAndCommitted(transactions, true, refUpdates);
    }

    private static void assertPreparedAndCommitted(List<ReferenceTransaction> transactions, boolean atomic,
                                                   RefUpdate... refUpdates) {
        if (atomic) {
            assertEquals(2, transactions.size());

            ReferenceTransaction prepared = transactions.get(0);
            assertEquals(ReferenceTransactionState.PREPARED, prepared.getState());
            assertThat(prepared.getRefUpdates(), containsInAnyOrder(refUpdates));
            ReferenceTransaction committed = transactions.get(1);
            assertEquals(ReferenceTransactionState.COMMITTED, committed.getState());
            assertThat(committed.getRefUpdates(), containsInAnyOrder(refUpdates));
        } else {
            for (RefUpdate refUpdate : refUpdates) {
                List<ReferenceTransaction> refTransactions = transactions.stream()
                        .filter(referenceTx -> referenceTx.getRefUpdates().contains(refUpdate))
                        .collect(Collectors.toList());

                assertPreparedAndCommitted(refTransactions, true, refUpdate);
            }
        }
    }

    private List<ReferenceTransaction> capturingTransactionCallbacks(Consumer<HookRequestHandle> operation) {
        CapturingReferenceTransactionListener listener = new CapturingReferenceTransactionListener();
        //Execute the operation in a new execution context to ensure the context and ExecutionLocal cleanup logic is
        //also included in the test.
        executionContextRule.runInNewContext(() -> {
            try (HookRequestHandle handle = gitHookService.registerRequest(repository, handler(listener), PRE_RECEIVE)) {
                operation.accept(handle);
            }

            List<ReferenceTransaction> transactions = listener.getTransactions();
            if (!transactions.isEmpty()) {
                if (transactions.size() == 2) {
                    //If there was a single transaction (PREPARE+COMMITTED callback), verify that the PREPARE step
                    //captured the correct content hash. Not getting the expected hash here is an indication that we
                    //incorrectly suppressed the 'real' PREPARED+COMMITTED and invoked the PREPARED callback too late.
                    assertEquals("PREPARE callback captured incorrect content hash",
                            "3122c6a92d575279303e297fdcf1c9ea40d8b05fbeaaed0bb012ea610bedcdc2",
                            listener.getPreparedContentHash());
                }

                //Verify that the actual content hash matches the content hash calculated by combining the before hash
                //and the transaction hash
                assertEquals("The combined hash did not match the actual content hash during the COMMITTED callback!",
                        listener.getCommittedContentHashActual(), listener.getCommittedContentHashCombined());
                Repository repository = transactions.get(0).getRepository();
                assertEquals("The current content hash does not match the captured COMMITTED callback content hash!",
                        refHasher.calculateHash(repository), listener.getCommittedContentHashActual());
            }
        });

        return listener.getTransactions();
    }

    private void createBranchAndVerifyCallbacks() {
        List<ReferenceTransaction> transactions = capturingTransactionCallbacks(
                handle -> handle.configure(builderFactory.builder(repository)
                        .updateRef()
                        .set("refs/heads/new-branch", "0a943a29376f2336b78312d99e65da17048951db")
                        .deref(false))
                        .build()
                        .run());

        try {
            assertPreparedAndCommitted(transactions,
                    RefUpdate.ofAdd("refs/heads/new-branch", "0a943a29376f2336b78312d99e65da17048951db"));
        } finally {
            try {
                Files.deleteIfExists(repository.getPath().resolve("refs/heads/new-branch"));
            } catch (IOException e) {
                fail("Failed to delete loose new-branch ref: " + e.getMessage());
            }
        }
    }

    private void deleteBranchUsingBranchDeleteAndVerifyCallbacks(boolean force) {
        List<ReferenceTransaction> transactions = capturingTransactionCallbacks(
                handle -> handle.configure(builderFactory.builder(repository)
                        .branch()
                        .delete("branch_mod_merge")
                        .force(force))
                        .build()
                        .run());

        assertPreparedAndCommitted(transactions,
                RefUpdate.ofDelete("refs/heads/branch_mod_merge", "6053a1eaa1c009dd11092d09a72f3c41af1b59ad"));
    }

    private void deleteBranchUsingPushAndVerifyCallbacks(boolean force) {
        List<ReferenceTransaction> transactions = capturingTransactionCallbacks(
                handle -> handle.configure(builderFactory.builder(repository)
                        .push()
                        .repository(".")
                        .refspec(":branch_mod_merge")
                        .force(force))
                        .build(new StringStdioHandler())
                        .run());

        assertPreparedAndCommitted(transactions,
                RefUpdate.ofDelete("refs/heads/branch_mod_merge", "6053a1eaa1c009dd11092d09a72f3c41af1b59ad"));
    }

    private void deleteBranchUsingUpdateRefAndVerifyCallbacks(boolean force) {
        List<ReferenceTransaction> transactions = capturingTransactionCallbacks(
                handle -> handle.configure(builderFactory.builder(repository)
                        .updateRef()
                        .delete("refs/heads/branch_mod_merge")
                        .oldValue(force ? null : "6053a1eaa1c009dd11092d09a72f3c41af1b59ad"))
                        .build()
                        .run());

        assertPreparedAndCommitted(transactions,
                RefUpdate.ofDelete("refs/heads/branch_mod_merge", "6053a1eaa1c009dd11092d09a72f3c41af1b59ad"));
    }

    private void deleteBranchesUsingPushAndVerifyCallbacks(boolean force) {
        List<ReferenceTransaction> transactions = capturingTransactionCallbacks(
                handle -> handle.configure(builderFactory.builder(repository)
                        .push()
                        .repository(".")
                        .refspec(":branch_mod_merge")
                        .refspec(":basic_branching")
                        .force(force))
                        .build(new StringStdioHandler())
                        .run());

        assertEquals(4, transactions.size());
        assertPreparedAndCommitted(transactions.subList(0, 2),
                RefUpdate.ofDelete("refs/heads/basic_branching", "d6edcbf924697ab811a867421dab60d954ccad99"));
        assertPreparedAndCommitted(transactions.subList(2, 4),
                RefUpdate.ofDelete("refs/heads/branch_mod_merge", "6053a1eaa1c009dd11092d09a72f3c41af1b59ad"));
    }

    private void deleteBranchesUsingPushAtomicAndVerifyCallbacks(boolean force) {
        List<ReferenceTransaction> transactions = capturingTransactionCallbacks(
                handle -> handle.configure(builderFactory.builder(repository)
                        .push()
                        .atomic(true)
                        .repository(".")
                        .refspec(":branch_mod_merge")
                        .refspec(":basic_branching")
                        .force(force))
                        .build(new StringStdioHandler())
                        .run());

        assertEquals(2, transactions.size());
        assertPreparedAndCommitted(transactions,
                RefUpdate.ofDelete("refs/heads/basic_branching", "d6edcbf924697ab811a867421dab60d954ccad99"),
                RefUpdate.ofDelete("refs/heads/branch_mod_merge", "6053a1eaa1c009dd11092d09a72f3c41af1b59ad"));
    }

    private void deleteBranchesUsingUpdateRefAndVerifyCallbacks(boolean force) {
        List<String> instructions = ImmutableList.of(
                "delete refs/heads/basic_branching\0" + (force ? "" : "d6edcbf924697ab811a867421dab60d954ccad99"),
                "delete refs/heads/branch_mod_merge\0" + (force ? "" : "6053a1eaa1c009dd11092d09a72f3c41af1b59ad"));

        List<ReferenceTransaction> transactions = capturingTransactionCallbacks(
                handle -> handle.configure(builderFactory.builder(repository)
                        .updateRef()
                        .stdin()
                        .deref(false)
                        .nullTerminated(true))
                        .build(new UpdateRefStdioHandler(instructions.stream(), true))
                        .run());

        assertPreparedAndCommitted(transactions,
                RefUpdate.ofDelete("refs/heads/basic_branching", "d6edcbf924697ab811a867421dab60d954ccad99"),
                RefUpdate.ofDelete("refs/heads/branch_mod_merge", "6053a1eaa1c009dd11092d09a72f3c41af1b59ad")
        );
    }

    private void deleteTagUsingTagDeleteAndVerifyCallbacks() {
        List<ReferenceTransaction> transactions = capturingTransactionCallbacks(
                handle -> handle.configure(builderFactory.builder(repository)
                        .tag()
                        .delete("backdated_annotated_tag"))
                        .build()
                        .run());

        assertPreparedAndCommitted(transactions,
                RefUpdate.ofDelete("refs/tags/backdated_annotated_tag", "80aa5be5ecf39660f798858482254f7a2ae9110e"));
    }

    private GitHookHandler handler(ReferenceTransactionListener listener) {
        ExecutionManager executionManager = executionContextRule.getExecutionManager();
        return new GitReferenceTransactionHookHandler(config.getGit().getCompatibility(), builderFactory,
                executionManager, ImmutableList.of(listener), new DefaultUpdateRefsContextHolder(executionManager));
    }

    private void mixedUpdateAndVerifyCallbacks(boolean force) {
        List<ReferenceTransaction> transactions = capturingTransactionCallbacks(handle -> {
            List<String> instructions = ImmutableList.of(
                    "delete refs/heads/branch_mod_merge\0" + (force ? "" : "6053a1eaa1c009dd11092d09a72f3c41af1b59ad"),
                    "delete refs/heads/basic_branching\0" + (force ? "" : "d6edcbf924697ab811a867421dab60d954ccad99"),
                    "update refs/heads/out_of_order_branch\09fbc34a0d905950131d73f352abe68520c6db2a3\0" +
                            (force ? "" : "2e10dd2d1d5eea9291b296e78312e8a703964a95"),
                    "create refs/tags/retagged_2\09b4e781dea0769888fe270e06ad76675f73851b1");

            handle.configure(builderFactory.builder(repository)
                    .updateRef()
                    .stdin()
                    .deref(false)
                    .nullTerminated(true))
                    .build(new UpdateRefStdioHandler(instructions.stream(), true))
                    .run();
        });
        assertPreparedAndCommitted(transactions,
                RefUpdate.ofDelete("refs/heads/basic_branching", "d6edcbf924697ab811a867421dab60d954ccad99"),
                RefUpdate.ofDelete("refs/heads/branch_mod_merge", "6053a1eaa1c009dd11092d09a72f3c41af1b59ad"),
                RefUpdate.of("refs/heads/out_of_order_branch", "2e10dd2d1d5eea9291b296e78312e8a703964a95",
                        "9fbc34a0d905950131d73f352abe68520c6db2a3"),
                RefUpdate.ofAdd("refs/tags/retagged_2", "9b4e781dea0769888fe270e06ad76675f73851b1"));
    }

    private void packRefs(boolean prune) {
        builderFactory.builder(repository).packRefs()
                .all(true)
                .prune(prune)
                .build(new LoggingStdioHandler())
                .run();
    }

    private void packSomeRefs() {
        try {
            //Delete one loose ref, pack everything and recreate the loose ref
            Path looseRef = MoreFiles.resolve(repository.getPath(), "refs", "heads", "branch_mod_merge");
            Files.delete(looseRef);
            packRefs(true);
            MoreFiles.write(looseRef, "6053a1eaa1c009dd11092d09a72f3c41af1b59ad");
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    private void pushMultipleUpdatesAndVerifyCallbacks(boolean force) {
        List<ReferenceTransaction> transactions = capturingTransactionCallbacks(
                handle -> handle.configure(builderFactory.builder(repository)
                        .push()
                        .repository(".")
                        .refspecs("0a943a29376f2336b78312d99e65da17048951db:refs/heads/branch_mod_merge")
                        .refspecs("d6edcbf924697ab811a867421dab60d954ccad99:refs/heads/backup/basic_branching")
                        .refspecs("9b4e781dea0769888fe270e06ad76675f73851b1:refs/tags/backup/retagged_signed_tag")
                        .force(force))
                        .build(new StringStdioHandler())
                        .run());

        assertPreparedAndCommitted(transactions, false,
                RefUpdate.of("refs/heads/branch_mod_merge",
                        "6053a1eaa1c009dd11092d09a72f3c41af1b59ad", "0a943a29376f2336b78312d99e65da17048951db"),
                RefUpdate.ofAdd("refs/heads/backup/basic_branching", "d6edcbf924697ab811a867421dab60d954ccad99"),
                RefUpdate.ofAdd("refs/tags/backup/retagged_signed_tag", "9b4e781dea0769888fe270e06ad76675f73851b1"));
    }

    private void pushMultipleUpdatesAndVerifyCallbacksWithDeletes(boolean force) {
        List<ReferenceTransaction> transactions = capturingTransactionCallbacks(
                handle -> handle.configure(builderFactory.builder(repository)
                        .push()
                        .repository(".")
                        .refspec(":out_of_order_branch")
                        .refspec(":refs/tags/backdated_annotated_tag")
                        .refspecs("0a943a29376f2336b78312d99e65da17048951db:refs/heads/branch_mod_merge")
                        .refspecs("d6edcbf924697ab811a867421dab60d954ccad99:refs/heads/backup/basic_branching")
                        .refspecs("9b4e781dea0769888fe270e06ad76675f73851b1:refs/tags/backup/retagged_signed_tag")
                        .force(force))
                        .build(new StringStdioHandler())
                        .run());

        assertPreparedAndCommitted(transactions, false,
                RefUpdate.ofDelete("refs/heads/out_of_order_branch", "2e10dd2d1d5eea9291b296e78312e8a703964a95"),
                RefUpdate.of("refs/heads/branch_mod_merge",
                        "6053a1eaa1c009dd11092d09a72f3c41af1b59ad", "0a943a29376f2336b78312d99e65da17048951db"),
                RefUpdate.ofAdd("refs/heads/backup/basic_branching", "d6edcbf924697ab811a867421dab60d954ccad99"),
                RefUpdate.ofAdd("refs/tags/backup/retagged_signed_tag", "9b4e781dea0769888fe270e06ad76675f73851b1"));
    }

    private void updateOutOfOrderBranch(boolean force) {
        List<ReferenceTransaction> transactions = capturingTransactionCallbacks(
                handle -> handle.configure(builderFactory.builder(repository)
                        .updateRef()
                        .set("refs/heads/out_of_order_branch", "e26cab95a50f6e99d2c46c498a23cee307cc8e27")
                        .oldValue(force ? null : "2e10dd2d1d5eea9291b296e78312e8a703964a95"))
                        .build()
                        .run());

        assertPreparedAndCommitted(transactions,
                RefUpdate.of("refs/heads/out_of_order_branch", "2e10dd2d1d5eea9291b296e78312e8a703964a95",
                        "e26cab95a50f6e99d2c46c498a23cee307cc8e27"));
    }

    private class CapturingReferenceTransactionListener implements ReferenceTransactionListener {

        private String committedContentHashActual;
        private String committedContentHashCombined;
        private String preparedContentHash;
        private ImmutableList.Builder<ReferenceTransaction> transactionBuilder = ImmutableList.builder();
        private String transactionHash;

        @Nonnull
        public String getCommittedContentHashActual() {
            return committedContentHashActual;
        }

        @Nonnull
        public String getCommittedContentHashCombined() {
            return committedContentHashCombined;
        }

        @Nonnull
        public String getPreparedContentHash() {
            return preparedContentHash;
        }

        @Nonnull
        public List<ReferenceTransaction> getTransactions() {
            return transactionBuilder.build();
        }

        @Override
        public void onReferenceTransaction(@Nonnull ReferenceTransaction referenceTransaction) {
            transactionBuilder.add(referenceTransaction);
            if (referenceTransaction.getState() == ReferenceTransactionState.PREPARED) {
                preparedContentHash = refHasher.calculateHash(referenceTransaction.getRepository());
                transactionHash = refHasher.calculateHash(referenceTransaction.getRefUpdates());
            } else if (referenceTransaction.getState() == ReferenceTransactionState.COMMITTED) {
                committedContentHashActual = refHasher.calculateHash(repository);
                committedContentHashCombined = refHasher.combineHashes(preparedContentHash, transactionHash);
            }
        }

        void reset() {
            committedContentHashActual = null;
            committedContentHashCombined = null;
            preparedContentHash = null;
            transactionBuilder = ImmutableList.builder();
            transactionHash = null;
        }
    }
}
