package com.atlassian.bitbucket.mesh.git;

import com.atlassian.bitbucket.mesh.AbstractGrpcServiceTest;
import com.atlassian.bitbucket.mesh.MockResponseObserver;
import com.atlassian.bitbucket.mesh.io.MoreFiles;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.atlassian.bitbucket.mesh.repository.WorkTree;
import com.atlassian.bitbucket.mesh.repository.WorkTreeManager;
import com.atlassian.bitbucket.mesh.rpc.v1.*;
import com.atlassian.bitbucket.mesh.test.HasGitError;
import com.atlassian.bitbucket.mesh.test.annotation.Repo;
import com.google.common.base.Charsets;
import com.google.common.base.Splitter;
import com.google.protobuf.ByteString;
import io.grpc.ManagedChannel;
import io.grpc.StatusRuntimeException;
import io.grpc.stub.StreamObserver;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.Before;
import org.junit.Test;

import javax.annotation.Resource;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.util.Iterator;
import java.util.concurrent.TimeoutException;

public class GrpcWorkTreeServiceTest extends AbstractGrpcServiceTest {

    @Resource
    private ManagedChannel managedChannel;
    @Resource
    private WorkTreeManager workTreeManager;
    @Repo
    private Repository repository;

    private WorkTreeServiceGrpc.WorkTreeServiceBlockingStub stub;

    @Before
    public void setUp() throws Exception {
        stub = WorkTreeServiceGrpc.newBlockingStub(managedChannel);
    }

    @Test
    public void testClose() {
        String workTreeId = createWorkTree();
        RpcCloseWorkTreeRequest closeWorkTreeRequest = RpcCloseWorkTreeRequest.newBuilder()
                .setWorkTree(workTreeId)
                .build();
        WorkTree wt = workTreeManager.getById(workTreeId);

        stub.close(closeWorkTreeRequest);

        assertFalse(MoreFiles.dirExists(wt.getPath()));
    }

    @Test
    public void testCreate() {
        RpcCreateWorkTreeRequest request = RpcCreateWorkTreeRequest.newBuilder()
                .setRepository(repository.getId())
                .build();

        stub.create(request);
    }

    @Test
    public void testMkdir() {
        String workTreeId = createWorkTree();
        RpcWorkTreeMkdirRequest request = RpcWorkTreeMkdirRequest.newBuilder()
                .setWorkTree(workTreeId)
                .setPath(ByteString.copyFromUtf8("directory"))
                .build();
        stub.mkdir(request);

        assertTrue(MoreFiles.dirExists(workTreeManager.getById(workTreeId).getPath()));
    }

    @Test
    public void testMkdirAbsolutePath() {
        String absolutePath = "/directory";
        String workTreeId = createWorkTree();
        RpcWorkTreeMkdirRequest request = RpcWorkTreeMkdirRequest.newBuilder()
                .setWorkTree(workTreeId)
                .setPath(ByteString.copyFromUtf8(absolutePath))
                .build();

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class, () -> stub.mkdir(request));

        assertThat(thrown, HasGitError.invalidWorkTreePath(absolutePath, RpcInvalidWorkTreePath.Reason.INVALID_LOCATION));
        assertFalse(MoreFiles.dirExists(Paths.get(absolutePath)));
    }

    @Test
    public void testMkdirInGitDirectory() {
        String workTreeId = createWorkTree();
        String relativePath = ".git/directory";
        RpcWorkTreeMkdirRequest request = RpcWorkTreeMkdirRequest.newBuilder()
                .setWorkTree(workTreeId)
                .setPath(ByteString.copyFromUtf8(relativePath))
                .build();

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class, () -> stub.mkdir(request));

        assertThat(thrown, HasGitError.invalidWorkTreePath(relativePath, RpcInvalidWorkTreePath.Reason.INVALID_LOCATION));
        WorkTree wt = workTreeManager.getById(workTreeId);
        assertFalse(MoreFiles.dirExists(wt.getPath().resolve(relativePath)));
    }

    @Test
    public void testMkdirWithBackReferences() {
        String workTreeId = createWorkTree();
        String relativePath = "directory/../../logs";
        RpcWorkTreeMkdirRequest request = RpcWorkTreeMkdirRequest.newBuilder()
                .setWorkTree(workTreeId)
                .setPath(ByteString.copyFromUtf8(relativePath))
                .build();

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class, () -> stub.mkdir(request));

        assertThat(thrown, HasGitError.invalidWorkTreePath(relativePath, RpcInvalidWorkTreePath.Reason.INVALID_LOCATION));
        WorkTree wt = workTreeManager.getById(workTreeId);
        assertFalse(MoreFiles.dirExists(wt.getPath().resolve(relativePath)));
    }

    @Test
    public void testWriteFile() throws InterruptedException, TimeoutException, IOException {
        String content = RandomStringUtils.randomAlphanumeric(16);
        String relativePath = "test-file.txt";
        String workTreeId = createWorkTree();

        MockResponseObserver<RpcWriteWorkTreeFileResponse> responseObserver = writeFile(workTreeId, relativePath,
                content);
        responseObserver.await();

        assertEquals(1, responseObserver.getFragments().size());
        Path savedFile = workTreeManager.getById(workTreeId).getPath().resolve(relativePath);
        assertTrue(Files.isRegularFile(savedFile));
        assertEquals(content, IOUtils.toString(savedFile.toUri(), StandardCharsets.UTF_8));
    }

    @Test
    public void testWriteFileAbsolutePath() throws InterruptedException, TimeoutException {
        String absolutePath = "/tmp/shadow";
        String workTreeId = createWorkTree();
        String content = "root:$6$XSdGOfgs$i45fzPAnGSFV15UXkJMH9DKsm6RYz8IjCAnr4zwKIpWpfEySczzMZG.:17737:0:99999:7:::";

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class, () -> {
            MockResponseObserver<RpcWriteWorkTreeFileResponse> responseObserver = writeFile(workTreeId, absolutePath,
                    content);
            responseObserver.await();
        });

        assertThat(thrown, HasGitError.invalidWorkTreePath(absolutePath, RpcInvalidWorkTreePath.Reason.INVALID_LOCATION));
        assertFalse(Files.exists(Paths.get(absolutePath)));
    }

    @Test
    public void testWriteFileInGitDirectory() {
        String relativePath = ".git/config";
        String workTreeId = createWorkTree();
        String content = "myConfigContents";

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class, () -> {
            MockResponseObserver<RpcWriteWorkTreeFileResponse> responseObserver = writeFile(workTreeId, relativePath,
                    content);
            responseObserver.await();
        });

        assertThat(thrown, HasGitError.invalidWorkTreePath(relativePath, RpcInvalidWorkTreePath.Reason.INVALID_LOCATION));
        assertFalse(Files.exists(Paths.get(relativePath)));
    }

    @Test
    public void testWriteFileWithBackReferences() {
        String workTreeId = createWorkTree();
        String relativePath = "directory/../../logs";
        String content = "nothing to see here";

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class, () -> {
            MockResponseObserver<RpcWriteWorkTreeFileResponse> responseObserver = writeFile(workTreeId, relativePath,
                    content);
            responseObserver.await();
        });

        assertThat(thrown, HasGitError.invalidWorkTreePath(relativePath, RpcInvalidWorkTreePath.Reason.INVALID_LOCATION));
        assertFalse(Files.exists(Paths.get(relativePath)));
    }

    private String createWorkTree() {
        RpcCreateWorkTreeRequest createRequest = RpcCreateWorkTreeRequest.newBuilder()
                .setRepository(repository.getId())
                .setExpiry(Instant.now().plusSeconds(60).toEpochMilli())
                .build();

        return stub.create(createRequest).getWorkTree();
    }

    private WorkTreeServiceGrpc.WorkTreeServiceStub stub() {
        return WorkTreeServiceGrpc.newStub(managedChannel);
    }

    private MockResponseObserver<RpcWriteWorkTreeFileResponse> writeFile(String workTree, String filename, String content) {
        MockResponseObserver<RpcWriteWorkTreeFileResponse> responseObserver = new MockResponseObserver<>();
        StreamObserver<RpcWriteWorkTreeFileFragment> observer = stub().writeFile(responseObserver);

        // Send request (OID and repository)
        observer.onNext(RpcWriteWorkTreeFileFragment.newBuilder()
                .setRequest(RpcWriteWorkTreeFileRequest.newBuilder()
                        .setWorkTree(workTree)
                        .setPath(ByteString.copyFromUtf8(filename)))
                .build());

        // Split content to ensure testing with multiple fragments
        Iterator<String> contentFragments = Splitter.fixedLength(7).split(content).iterator();
        while (contentFragments.hasNext()) {
            observer.onNext(RpcWriteWorkTreeFileFragment.newBuilder()
                    .setContent(RpcStreamChunk.newBuilder()
                            .setData(ByteString.copyFrom(contentFragments.next(), Charsets.UTF_8))
                            .setClosed(!contentFragments.hasNext()))
                    .build());
        }

        observer.onCompleted();
        return responseObserver;
    }
}