package com.atlassian.bitbucket.mesh.git.file;

import com.atlassian.bitbucket.mesh.AbstractGrpcTest;
import com.atlassian.bitbucket.mesh.MockResponseObserver;
import com.atlassian.bitbucket.mesh.git.GitAgent;
import com.atlassian.bitbucket.mesh.git.GitRepositories;
import com.atlassian.bitbucket.mesh.git.GitUtils;
import com.atlassian.bitbucket.mesh.git.builder.GitProcessBuilder;
import com.atlassian.bitbucket.mesh.git.builder.GitProcessBuilderFactory;
import com.atlassian.bitbucket.mesh.git.builder.catfile.EditFileMetadataBatchCatFileStdioHandler;
import com.atlassian.bitbucket.mesh.git.builder.lstree.ModeLsTreeStdioHandler;
import com.atlassian.bitbucket.mesh.git.commit.Commit;
import com.atlassian.bitbucket.mesh.git.commit.CommitManager;
import com.atlassian.bitbucket.mesh.git.commit.GetCommitParameters;
import com.atlassian.bitbucket.mesh.git.ref.Branch;
import com.atlassian.bitbucket.mesh.process.handler.BufferStdioHandler;
import com.atlassian.bitbucket.mesh.process.handler.NullStdioHandler;
import com.atlassian.bitbucket.mesh.process.handler.StringStdioHandler;
import com.atlassian.bitbucket.mesh.repository.CreateRepositoryParameters;
import com.atlassian.bitbucket.mesh.repository.DeleteRepositoryParameters;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.atlassian.bitbucket.mesh.repository.RepositoryManager;
import com.atlassian.bitbucket.mesh.rpc.v1.*;
import com.atlassian.bitbucket.mesh.test.HasGitError;
import com.atlassian.bitbucket.mesh.test.annotation.DirtiesRepo;
import com.atlassian.bitbucket.mesh.test.annotation.Repo;
import com.atlassian.bitbucket.mesh.util.Person;
import com.google.protobuf.ByteString;
import io.grpc.ManagedChannel;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.stub.StreamObserver;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import javax.annotation.Resource;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;

import static com.atlassian.bitbucket.mesh.git.GitConstants.CONFIG_CORE_SAFECRLF;
import static com.atlassian.bitbucket.mesh.git.file.DefaultEditFileManager.BUFFER_SIZE_BINARY;
import static com.atlassian.bitbucket.mesh.git.file.DefaultEditFileManager.BUFFER_SIZE_ENCODING;
import static java.nio.charset.StandardCharsets.UTF_16BE;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.text.IsEqualIgnoringCase.equalToIgnoringCase;

@SuppressWarnings("deprecation")
public class EditFileRequestTest extends AbstractGrpcTest {

    private static final RpcPerson AUTHOR = RpcPerson.newBuilder()
            .setName(ByteString.copyFromUtf8("Buggs Hunters"))
            .setEmail("bhunt@example.com")
            .build();
    private static final String BRANCH = "encodings";

    @Rule
    public final ExpectedException thrown = ExpectedException.none();

    @Resource
    private GitAgent agent;
    @Resource
    private GitProcessBuilderFactory builderFactory;
    @Resource
    private CommitManager commitManager;
    @Resource
    private ManagedChannel managedChannel;
    @Repo(GitRepositories.QA_RESOURCES)
    private Repository repository;
    @Resource
    private RepositoryManager repositoryManager;
    private CommitServiceGrpc.CommitServiceStub stub;

    @Before
    public void setup() {
        stub = CommitServiceGrpc.newStub(managedChannel);
    }

    @DirtiesRepo
    @Test
    public void testCommitDetailsAfterEditing() throws Exception {
        String message = "\"three\" or \"four\"";
        String path = "encodings/README-UTF-8.md";
        String content = "Edited content";

        Branch branch = resolveBranch(BRANCH);
        RpcEditFileRequest.Builder editFileRequestBuilder =
                createEditFileRequestBuilder(branch.toBranch(), message, path, repository, branch.getId())
                        .setSourceCommitish(ByteString.copyFromUtf8(branch.getLatestCommit()));

        MockResponseObserver<RpcEditFileResponseFragment> responseObserver = new MockResponseObserver<>();
        StreamObserver<RpcEditFileFragment> observer = stub.editFile(responseObserver);
        List<RpcEditFileResponseFragment> fragments =
                getFragments(responseObserver, observer, editFileRequestBuilder, inputStream(content, UTF_8));

        RpcEditFileResponse response = fragments.get(1).getResponse();
        RpcCommit rpcCommit = response.getCommit();

        //Editing a file creates a Commit manually, rather than forking a git process to retrieve it. For
        //this test we want to ensure that manufactured commit matches what's really recorded in the SCM
        Commit commit = commitManager.getCommit(new GetCommitParameters.Builder(repository, rpcCommit.getId())
                .maxMessageLength(Integer.MAX_VALUE)
                .build());

        assertEquals(new Person(rpcCommit.getAuthor()), commit.getAuthor());
        assertEquals(rpcCommit.getAuthorEpoch(), commit.getAuthorTimestamp());
        assertEquals(new Person(rpcCommit.getCommitter()), commit.getCommitter());
        assertEquals(rpcCommit.getCommitterEpoch(), commit.getCommitterTimestamp());
        assertEquals(rpcCommit.getMessage().toStringUtf8(), commit.getMessage());
        assertEquals(rpcCommit.getParentsList(), commit.getParents());
    }

    @DirtiesRepo
    @Test
    public void testEditBinaryFile() throws Exception {
        String path = "SourceView/binaryfilerenamed.txt";
        byte[] fileContent = builder().catFile()
                .pretty()
                .object(BRANCH, path)
                .build(new BufferStdioHandler())
                .run();
        byte[] updatedContent = ArrayUtils.add(fileContent, (byte) 9);
        Branch branch = resolveBranch(BRANCH);

        RpcEditFileRequest.Builder builder =
                createEditFileRequestBuilder(branch.toBranch(), "junit: Editing file", path, repository, branch.getId())
                        .setSourceCommitish(ByteString.copyFromUtf8(branch.getLatestCommit()));

        MockResponseObserver<RpcEditFileResponseFragment> responseObserver = new MockResponseObserver<>();
        StreamObserver<RpcEditFileFragment> observer = stub.editFile(responseObserver);
        List<RpcEditFileResponseFragment> fragments =
                getFragments(responseObserver, observer, builder, new ByteArrayInputStream(updatedContent));

        RpcEditFileResponse response = fragments.get(1).getResponse();
        RpcCommit commit = response.getCommit();

        byte[] actualContent = builder().catFile()
                .pretty()
                .object(BRANCH, path)
                .build(new BufferStdioHandler())
                .run();
        assertArrayEquals(updatedContent, actualContent);

        boolean isExecutable = isExecutable(commit.getId(), path);
        assertThat("The edited file should not be executable", isExecutable, equalTo(false));
    }

    @DirtiesRepo
    @Test
    public void testEditExecutableFileRetainsPermissions() throws Exception {
        String path = "bin/hello.sh";
        Branch branch = resolveBranch(BRANCH);

        RpcEditFileRequest.Builder builder =
                createEditFileRequestBuilder(branch.toBranch(), "junit: Editing file", path, repository, branch.getId())
                        .setSourceCommitish(ByteString.copyFromUtf8(branch.getLatestCommit()));

        MockResponseObserver<RpcEditFileResponseFragment> responseObserver = new MockResponseObserver<>();
        StreamObserver<RpcEditFileFragment> observer = stub.editFile(responseObserver);
        List<RpcEditFileResponseFragment> fragments =
                getFragments(responseObserver, observer, builder, inputStream("echo \"Hi world\";", UTF_8));

        RpcEditFileResponse response = fragments.get(1).getResponse();
        RpcCommit commit = response.getCommit();

        boolean isExecutable = isExecutable(commit.getId(), path);
        assertThat("The edited file should still be executable", isExecutable, equalTo(true));
    }

    @Test
    public void testEditFileInEmptyRepository() throws Exception {
        // initialize as an empty bare repository
        String repositoryId = "p/0001/h/09876543211234567890/r/empty";
        Repository empty = null;
        try {
            empty = repositoryManager.create(new CreateRepositoryParameters.Builder(repositoryId)
                    .build());
            RpcEditFileRequest.Builder builder = createEditFileRequestBuilder(
                    "junit: Editing file",
                    "test.txt",
                    empty,
                    "master");

            MockResponseObserver<RpcEditFileResponseFragment> responseObserver = new MockResponseObserver<>();
            StreamObserver<RpcEditFileFragment> observer = stub.editFile(responseObserver);
            List<RpcEditFileResponseFragment> fragments =
                    getFragments(responseObserver, observer, builder, inputStream("this is a test", UTF_8));

            RpcEditFileResponse response = fragments.get(1).getResponse();
            RpcCommit commit = response.getCommit();

            assertNotNull(commit);
            assertEquals("junit: Editing file\n", commit.getMessage().toStringUtf8());
        } finally {
            if (empty != null) {
                repositoryManager.delete(new DeleteRepositoryParameters.Builder(empty).build());
            }
        }
    }

    @DirtiesRepo
    @Test
    public void testEditFileOnNewBranch() throws Exception {
        RpcCommit commit = editFileOnNewBranch("brand-new-branch");

        assertNotNull(commit);

        Branch newBranch = resolveBranch("brand-new-branch");
        assertNotNull(newBranch);
        assertEquals(newBranch.getLatestCommit(), commit.getId());
        assertEquals(agent.resolveHead(repository).getLatestCommit(), commit.getParents(0));
        assertEquals("junit: Editing file\n", commit.getMessage().toStringUtf8());
    }

    @Test
    public void testEditFileOnNewBranchAlreadyExists() {
        StatusRuntimeException exception = assertEditFileOnNewBranchThrows("branch1");

        assertSame(Status.Code.INVALID_ARGUMENT, exception.getStatus().getCode());
        assertThat(exception,
                HasGitError.invalidBranchName("refs/heads/branch1", RpcInvalidBranchName.Reason.DUPLICATE));
    }

    @Test
    public void testEditFileOnNewBranchInvalidNameBlank() {
        String branchName = "              ";
        StatusRuntimeException exception = assertEditFileOnNewBranchThrows(branchName);

        assertSame(Status.Code.INVALID_ARGUMENT, exception.getStatus().getCode());
        assertThat(exception, HasGitError.invalidRefName(RpcInvalidRefName.Reason.BLANK, branchName));
    }

    @Test
    public void testEditFileOnNewBranchInvalidNameConflicting() {
        //The new branch should not be created because its name conflicts with the existing "branch1".
        StatusRuntimeException exception = assertEditFileOnNewBranchThrows("branch1/blarp");

        assertSame(Status.Code.INVALID_ARGUMENT, exception.getStatus().getCode());
        assertThat(exception,
                HasGitError.refNameConflict("refs/heads/branch1", "refs/heads/branch1/blarp"));
    }

    @Test
    public void testEditFileOnNewBranchInvalidNameConflicting2() {
        //The new branch should not be created because its name conflicts with the existing "foo/bar" branch.
        StatusRuntimeException exception = assertEditFileOnNewBranchThrows("foo");

        assertSame(Status.Code.INVALID_ARGUMENT, exception.getStatus().getCode());
        assertThat(exception, HasGitError.refNameConflict("refs/heads/foo/bar", "refs/heads/foo"));
    }

    @Test
    public void testEditFileOnNewBranchInvalidNameStartsWithHyphen() {
        StatusRuntimeException exception = assertEditFileOnNewBranchThrows(" --hack");

        assertSame(Status.Code.INVALID_ARGUMENT, exception.getStatus().getCode());
        assertThat(exception, HasGitError.invalidRefName(RpcInvalidRefName.Reason.INVALID, " --hack"));
    }

    @Test
    public void testEditFileOnNewBranchInvalidNameSuffix() {
        // .lock suffix is not allowed on ref names because it conflicts with git's ref locking mechanism
        StatusRuntimeException exception = assertEditFileOnNewBranchThrows("foo.lock");

        assertSame(Status.Code.INVALID_ARGUMENT, exception.getStatus().getCode());
        assertThat(exception, HasGitError.invalidRefName(RpcInvalidRefName.Reason.INVALID, "foo.lock"));
    }

    @Test
    public void testEditFileOnNewBranchInvalidNameTooLong() {
        String branchName = StringUtils.repeat("x", 101);
        StatusRuntimeException exception = assertEditFileOnNewBranchThrows(branchName);

        assertSame(Status.Code.INVALID_ARGUMENT, exception.getStatus().getCode());
        assertThat(exception, HasGitError.invalidRefName(111, RpcInvalidRefName.Reason.TOO_LONG, branchName));
    }

    @Test
    public void testEditFileOnNewBranchInvalidNameWhitespaces() {
        StatusRuntimeException exception = assertEditFileOnNewBranchThrows("refs/heads/brand new branch");

        assertSame(Status.Code.INVALID_ARGUMENT, exception.getStatus().getCode());
        assertThat(exception,
                HasGitError.invalidRefName(RpcInvalidRefName.Reason.WHITESPACE, "refs/heads/brand new branch"));
    }

    @DirtiesRepo
    @Test
    public void testEditFileShouldConvertCRLFToLFForFilesWithLFLineEndings() throws Exception {
        String safeCrlf = getSafeCrlfSetting();
        setSafeCrlfSetting("true");
        try {
            String path = "encodings/README-UTF-8.md";
            String actualContent = builder().catFile()
                    .pretty()
                    .object(BRANCH, path)
                    .build(new StringStdioHandler())
                    .run();

            String lfContent = actualContent.replaceAll("\n", "\r\n");
            String updatedContent =
                    new StringBuilder(lfContent).insert(lfContent.length() - 2, "Hello").toString();

            Branch branch = resolveBranch(BRANCH);
            RpcEditFileRequest.Builder builder =
                    createEditFileRequestBuilder(branch.toBranch(), "junit: Editing file", path, repository, branch.getId())
                            .setSourceCommitish(ByteString.copyFromUtf8(branch.getLatestCommit()));

            MockResponseObserver<RpcEditFileResponseFragment> responseObserver = new MockResponseObserver<>();
            StreamObserver<RpcEditFileFragment> observer = stub.editFile(responseObserver);
            getFragments(responseObserver, observer, builder, inputStream(updatedContent, UTF_8));

            assertFileContent(updatedContent.replace("\r\n", "\n"), path, UTF_8);
        } finally {
            setSafeCrlfSetting(safeCrlf);
        }
    }

    @DirtiesRepo
    @Test
    public void testEditFileShouldMaintainLineEndingsForFilesWithCRLFLineEndings() throws Exception {
        String path = "SourceView/unicode-bigending-windows-line-ending.txt";

        String actualContent = builder().catFile()
                .pretty()
                .object(BRANCH, path)
                .build(new StringStdioHandler(UTF_16BE))
                .run();
        String updatedContent = new StringBuilder(actualContent)
                .insert(actualContent.length() - 2, "Hello")
                .toString();

        Branch branch = resolveBranch(BRANCH);
        RpcEditFileRequest.Builder builder =
                createEditFileRequestBuilder(branch.toBranch(), "junit: Editing file", path, repository, branch.getId())
                        .setSourceCommitish(ByteString.copyFromUtf8(branch.getLatestCommit()));

        MockResponseObserver<RpcEditFileResponseFragment> responseObserver = new MockResponseObserver<>();
        StreamObserver<RpcEditFileFragment> observer = stub.editFile(responseObserver);
        getFragments(responseObserver, observer, builder, inputStream(updatedContent, UTF_16BE));

        assertFileContent(updatedContent, path, UTF_16BE);
    }

    @DirtiesRepo
    @Test
    public void testEditGB18030File() throws Exception {
        assertFileContentAndEncoding("encodings/gb18030.txt", "Some Edit", Charset.forName("GB18030"),
                Charset.forName("GB18030"));
    }

    @DirtiesRepo
    @Test
    public void testEditShouldFailWhenCharacterEncodingFails() throws Exception {
        thrown.expect(StatusRuntimeException.class);
        thrown.expectMessage("Failed to transcode from UTF-8 to windows-1252");

        assertFileContentAndEncoding("encodings/README-WINDOWS-1252.md", "你可能喜欢",
                Charset.forName("WINDOWS-1252"));
    }

    @DirtiesRepo
    @Test
    public void testEditShouldFailWhenEncodingDetectionFails() throws Exception {
        thrown.expect(StatusRuntimeException.class);
        thrown.expectMessage("Failed to transcode from UTF-8 to IBM866");

        assertFileContentAndEncoding("encodings/short-gb18030.txt", "ABC", Charset.forName("GB18030"));
    }

    @Test
    public void testEditSymbolicLink() throws Exception {
        String content = "\n../Gar\rfield.txt\n";
        String path = "symlinks/symlink_huge.txt";

        Branch branch = resolveBranch(BRANCH);
        RpcEditFileRequest.Builder builder =
                createEditFileRequestBuilder(branch.toBranch(), "junit: Editing file", path, repository, branch.getId())
                        .setSourceCommitish(ByteString.copyFromUtf8(branch.getLatestCommit()));

        MockResponseObserver<RpcEditFileResponseFragment> responseObserver = new MockResponseObserver<>();
        StreamObserver<RpcEditFileFragment> observer = stub.editFile(responseObserver);
        List<RpcEditFileResponseFragment> fragments =
                getFragments(responseObserver, observer, builder, inputStream(content, UTF_8));

        RpcEditFileResponse response = fragments.get(1).getResponse();
        RpcCommit commit = response.getCommit();

        String actualFileContent = builder().catFile()
                .pretty()
                .object(BRANCH, path)
                .build(new StringStdioHandler())
                .run();

        assertEquals("../Garfield.txt", actualFileContent);

        String mode = builder().lsTree()
                .nullTerminated(true)
                .path(path)
                .tree(commit.getId())
                .build(new ModeLsTreeStdioHandler())
                .run();
        assertNotNull(mode);
        assertTrue("File should remain a symbolic link after update", GitUtils.isSymbolicLink(mode));
    }

    @DirtiesRepo
    @Test
    public void testEditUTF16File() throws Exception {
        assertFileContentAndEncoding("encodings/README-UTF-16.md", "Some Edit", UTF_16BE, UTF_16BE);
    }

    @DirtiesRepo
    @Test
    public void testEditUTF8File() throws Exception {
        assertFileContentAndEncoding("encodings/README-UTF-8.md", "Some Edit", UTF_8, UTF_8);
    }

    @DirtiesRepo
    @Test
    public void testEditUTF8ToGB18030Encoding() throws Exception {
        assertFileContentAndEncoding("encodings/gb18030.txt", "你可能喜欢", Charset.forName("GB18030"));
    }

    @DirtiesRepo
    @Test
    public void testEditUTF8ToUTF16Encoding() throws Exception {
        assertFileContentAndEncoding("encodings/README-UTF-16.md", "Some Edit", UTF_16BE);
    }

    @DirtiesRepo
    @Test
    public void testEditWindows1252File() throws Exception {
        assertFileContentAndEncoding("encodings/README-WINDOWS-1252.md", "Some Edit",
                Charset.forName("WINDOWS-1252"), Charset.forName("WINDOWS-1252"));
    }

    @Test
    public void testWriteToAbsoluteFile() {
        Branch branch = resolveBranch(BRANCH);
        RpcEditFileRequest.Builder builder = createEditFileRequestBuilder(
                branch.toBranch(),
                "Creating a file outside of the working directory",
                "/tmp/edit-file-test",
                repository,
                branch.getId());

        MockResponseObserver<RpcEditFileResponseFragment> responseObserver = new MockResponseObserver<>();
        StreamObserver<RpcEditFileFragment> observer = stub.editFile(responseObserver);

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class,
                () -> getFragments(responseObserver, observer, builder, inputStream("test", UTF_8)));
        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.invalidEditFile(RpcInvalidEditFile.Reason.EDIT_FILE_PATH));

        assertFalse(Files.exists(Paths.get(builder.getPath().toStringUtf8())));
    }

    @Test
    public void testWriteToGitDirectoryFile() {
        Branch branch = resolveBranch(BRANCH);
        RpcEditFileRequest.Builder builder = createEditFileRequestBuilder(
                branch.toBranch(),
                "Writing to the .git directory",
                ".git/hooks/pre-commit",
                repository,
                branch.getId());

        MockResponseObserver<RpcEditFileResponseFragment> responseObserver = new MockResponseObserver<>();
        StreamObserver<RpcEditFileFragment> observer = stub.editFile(responseObserver);

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class,
                () -> getFragments(responseObserver, observer, builder, inputStream("test", UTF_8)));
        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.invalidEditFile(RpcInvalidEditFile.Reason.EDIT_FILE_PATH));
    }

    @Test
    public void testWriteToRelativeFile() {
        Branch branch = resolveBranch(BRANCH);
        RpcEditFileRequest.Builder builder = createEditFileRequestBuilder(
                branch.toBranch(),
                "Creating a file outside of the working directory",
                "../../edit-file-test",
                repository,
                branch.getId());

        MockResponseObserver<RpcEditFileResponseFragment> responseObserver = new MockResponseObserver<>();
        StreamObserver<RpcEditFileFragment> observer = stub.editFile(responseObserver);

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class,
                () -> getFragments(responseObserver, observer, builder, inputStream("test", UTF_8)));
        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.invalidEditFile(RpcInvalidEditFile.Reason.EDIT_FILE_PATH));
    }

    private StatusRuntimeException assertEditFileOnNewBranchThrows(String targetBranchName) {
        Branch branch = agent.resolveHead(repository);

        RpcEditFileRequest.Builder builder = createEditFileRequestBuilder(
                branch.toBranch(),
                "junit: Editing file",
                "test.txt",
                repository,
                targetBranchName);
        MockResponseObserver<RpcEditFileResponseFragment> responseObserver = new MockResponseObserver<>();
        StreamObserver<RpcEditFileFragment> observer = stub.editFile(responseObserver);

        return assertThrows(StatusRuntimeException.class,
                () -> getFragments(responseObserver, observer, builder, inputStream("this is a test", UTF_8)));
    }

    private void assertFileContent(String content, String path, Charset charset) {
        String actualFileContent = builder().catFile()
                .pretty()
                .object(BRANCH, path)
                .build(new StringStdioHandler(charset))
                .run();
        assertEquals(content, actualFileContent);
    }

    private void assertFileContentAndEncoding(String path, String edit, Charset charset) throws Exception {
        assertFileContentAndEncoding(path, edit, UTF_8, charset);
    }

    private void assertFileContentAndEncoding(String path, String editContent, Charset srcCharset, Charset dstCharset)
            throws Exception {
        String actualContent = builder().catFile()
                .pretty()
                .object(BRANCH, path)
                .build(new StringStdioHandler(dstCharset))
                .run();
        String updatedContent =
                new StringBuilder(actualContent).insert(actualContent.length() - 2, editContent).toString();

        Branch branch = resolveBranch(BRANCH);
        RpcEditFileRequest.Builder builder =
                createEditFileRequestBuilder(branch.toBranch(), "junit: Editing file", path, repository, branch.getId())
                        .setSourceCommitish(ByteString.copyFromUtf8(branch.getLatestCommit()));

        MockResponseObserver<RpcEditFileResponseFragment> responseObserver = new MockResponseObserver<>();
        StreamObserver<RpcEditFileFragment> observer = stub.editFile(responseObserver);
        getFragments(responseObserver, observer, builder, inputStream(updatedContent, srcCharset));

        assertFileContent(updatedContent, path, dstCharset);
        assertThat(dstCharset.displayName(), equalToIgnoringCase(fileEncoding(branch.getLatestCommit(), branch, path)));
    }

    private GitProcessBuilder builder() {
        return builderFactory.builder(repository);
    }

    private EditFileMetadata contentMetadata(String path, String initialCommit, Branch branch) {
        return builder().catFile()
                .batch()
                .build(new EditFileMetadataBatchCatFileStdioHandler(BUFFER_SIZE_BINARY, BUFFER_SIZE_ENCODING,
                        initialCommit, branch, path))
                .run();
    }

    private RpcEditFileRequest.Builder createEditFileRequestBuilder(String message, String path,
                                                                    Repository repository, String targetBranch) {
        return RpcEditFileRequest.newBuilder()
                .setAuthor(AUTHOR)
                .setMessage(ByteString.copyFromUtf8(message))
                .setPath(ByteString.copyFromUtf8(path))
                .setRepository(repository.getId())
                .setTargetBranch(ByteString.copyFromUtf8(targetBranch));
    }

    private RpcEditFileRequest.Builder createEditFileRequestBuilder(RpcBranch branch, String message,
                                                                    String path, Repository repository,
                                                                    String targetBranch) {
        return RpcEditFileRequest.newBuilder()
                .setAuthor(AUTHOR)
                .setBranch(branch)
                .setMessage(ByteString.copyFromUtf8(message))
                .setPath(ByteString.copyFromUtf8(path))
                .setRepository(repository.getId())
                .setTargetBranch(ByteString.copyFromUtf8(targetBranch));
    }

    private RpcCommit editFileOnNewBranch(String branchName) throws Exception {
        Branch branch = agent.resolveHead(repository);

        RpcEditFileRequest.Builder builder = createEditFileRequestBuilder(
                branch.toBranch(),
                "junit: Editing file",
                "test.txt",
                repository,
                branchName);
        MockResponseObserver<RpcEditFileResponseFragment> responseObserver = new MockResponseObserver<>();
        StreamObserver<RpcEditFileFragment> observer = stub.editFile(responseObserver);

        List<RpcEditFileResponseFragment> fragments =
                getFragments(responseObserver, observer, builder, inputStream("this is a test", UTF_8));
        return fragments.get(1).getResponse().getCommit();
    }

    private String fileEncoding(String initialCommit, Branch branch, String path) {
        return contentMetadata(path, initialCommit, branch).getEncoding();
    }

    private List<RpcEditFileResponseFragment> getFragments(MockResponseObserver<RpcEditFileResponseFragment> responseObserver,
                                                           StreamObserver<RpcEditFileFragment> observer,
                                                           RpcEditFileRequest.Builder builder,
                                                           InputStream content) throws Exception {
        observer.onNext(RpcEditFileFragment.newBuilder()
                .setRequest(builder)
                .build());

        observer.onNext(RpcEditFileFragment.newBuilder()
                .setContent(RpcFileContent.newBuilder()
                        .setClosed(true)
                        .setData(ByteString.readFrom(content)))
                .build());

        observer.onNext(RpcEditFileFragment.newBuilder()
                .setUpdateRef(RpcUpdateRef.newBuilder()
                        .build())
                .build());

        observer.onCompleted();
        responseObserver.await();

        return responseObserver.getFragments();
    }

    private String getSafeCrlfSetting() {
        return builderFactory.builder()
                .command("config")
                .argument("--global")
                .argument(CONFIG_CORE_SAFECRLF)
                .completionHandler(summary -> {
                    //git exits with 1 exit code if the config value is not set
                    if (summary.getExitCode() != 1 && summary.getExitCode() != 0) {
                        throw new IllegalStateException("Unexpected git failure: " + summary.getStderr());
                    }
                })
                .build(new StringStdioHandler())
                .run();
    }

    private InputStream inputStream(String content, Charset charset) {
        return new ByteArrayInputStream(content.getBytes(charset));
    }

    private boolean isExecutable(String commitId, String path) {
        String mode = builder().lsTree()
                .nullTerminated(true)
                .tree(commitId)
                .path(path)
                .build(new ModeLsTreeStdioHandler())
                .run();

        assertNotNull(mode);
        return GitUtils.isExecutable(mode) == Boolean.TRUE;
    }

    private Branch resolveBranch(String branch) {
        return agent.resolveBranch(repository, branch);
    }

    private void setSafeCrlfSetting(String value) {
        GitProcessBuilder builder = builderFactory.builder()
                .command("config")
                .argument("--global");
        if (StringUtils.isEmpty(value)) {
            builder.argument("--unset")
                    .argument(CONFIG_CORE_SAFECRLF);
        } else {
            builder.argument(CONFIG_CORE_SAFECRLF)
                    .argument(value);
        }

        builder.build(new NullStdioHandler<>())
                .run();
    }
}
