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

import com.atlassian.bitbucket.mesh.AbstractGrpcTest;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.atlassian.bitbucket.mesh.rpc.v1.PorcelainServiceGrpc;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcArchiveFormat;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcArchiveRequest;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcStreamFragment;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcStreamFragment.FragmentOneofCase;
import com.atlassian.bitbucket.mesh.test.annotation.Repo;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Streams;
import com.google.protobuf.ByteString;
import io.grpc.ManagedChannel;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveInputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.junit.Before;
import org.junit.Test;

import javax.annotation.Resource;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.zip.GZIPInputStream;

import static com.atlassian.bitbucket.mesh.rpc.util.ByteStringUtils.toByteStringsLazily;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.hamcrest.collection.IsIterableContainingInOrder.contains;

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

    @Resource
    private ManagedChannel managedChannel;
    @Repo
    private Repository repository;
    private PorcelainServiceGrpc.PorcelainServiceBlockingStub stub;

    @Before
    public void setup() {
        stub = PorcelainServiceGrpc.newBlockingStub(managedChannel);
    }

    @Test
    public void testArchiveTar() throws IOException {
        Iterator<RpcStreamFragment> response = stub.archive(RpcArchiveRequest.newBuilder()
                .setFormat(RpcArchiveFormat.ARCHIVE_FORMAT_TAR)
                .setPrefix("a path with spaces/")
                .setRepository(repository.getId())
                .setRev(ByteString.copyFromUtf8("HEAD"))
                .build()
        );

        List<String> fileNames = getFileNames(response, RpcArchiveFormat.ARCHIVE_FORMAT_TAR);

        assertThat(fileNames, contains(
                equalTo("a path with spaces/add_file/add_file.txt"),
                equalTo("a path with spaces/basic_branching/file.txt"),
                equalTo("a path with spaces/binary/B.zip"),
                equalTo("a path with spaces/binary/C.zip"),
                equalTo("a path with spaces/binary/D.zip"),
                equalTo("a path with spaces/branch_mod_merge/file.txt"),
                equalTo("a path with spaces/modification/mod_file.txt"),
                equalTo("a path with spaces/moved_dir/file.txt"),
                equalTo("a path with spaces/mv_file/moved_file.txt"),
                equalTo("a path with spaces/out_of_order/file.txt"),
                equalTo("a path with spaces/symlink/link.txt"),
                equalTo("a path with spaces/symlink/target.txt")));
    }

    @Test
    public void testArchiveForTarGz() throws IOException {
        Iterator<RpcStreamFragment> response = stub.archive(RpcArchiveRequest.newBuilder()
                .setFormat(RpcArchiveFormat.ARCHIVE_FORMAT_TGZ)
                .setRepository(repository.getId())
                .setRev(ByteString.copyFromUtf8("bf3346620ec9161977fc23dee1240a7991e64ca1"))
                .build()
        );

        List<String> fileNames = getFileNames(response, RpcArchiveFormat.ARCHIVE_FORMAT_TGZ);

        assertThat(fileNames, contains(
                equalTo("modification/mod_file.txt"),
                equalTo("out_of_order/file.txt")));
    }

    @Test
    public void testArchiveForZip() throws IOException {
        Iterator<RpcStreamFragment> response = stub.archive(RpcArchiveRequest.newBuilder()
                .addAllPaths(toByteStringsLazily(ImmutableList.of("add_file", "mv_file", "symlink")))
                .setFormat(RpcArchiveFormat.ARCHIVE_FORMAT_ZIP)
                .setRepository(repository.getId())
                .setRev(ByteString.copyFromUtf8("master"))
                .build()
        );

        List<String> fileNames = getFileNames(response, RpcArchiveFormat.ARCHIVE_FORMAT_ZIP);

        assertThat(fileNames, contains(
                equalTo("add_file/add_file.txt"),
                equalTo("mv_file/moved_file.txt"),
                equalTo("symlink/link.txt"),
                equalTo("symlink/target.txt")));
    }

    @Test
    public void testInvalidRef() {
        Iterator<RpcStreamFragment> response = stub.archive(RpcArchiveRequest.newBuilder()
                .setFormat(RpcArchiveFormat.ARCHIVE_FORMAT_ZIP)
                .setRepository(repository.getId())
                .setRev(ByteString.copyFromUtf8("--remote=ssh://127.0.0.1/"))
                .build());

        assertThat(getResponseError(response), startsWith("fatal: not a valid object name"));
    }

    @Test
    public void testMissingCommit() {
        Iterator<RpcStreamFragment> response = stub.archive(RpcArchiveRequest.newBuilder()
                .setFormat(RpcArchiveFormat.ARCHIVE_FORMAT_ZIP)
                .setRepository(repository.getId())
                .setRev(ByteString.copyFromUtf8("905b4f9708b5238ccecde1749dd02e9cdca34646"))
                .build());

        assertThat(getResponseError(response), startsWith("fatal: not a tree object"));
    }

    private static List<String> listArchive(ArchiveInputStream s) throws IOException {
        List<String> files = new ArrayList<>();
        try (ArchiveInputStream inputStream = s) {
            ArchiveEntry entry;
            while ((entry = inputStream.getNextEntry()) != null) {
                if (entry.isDirectory() || "pax_global_header".equals(entry.getName())) {
                    //For simplicity, we only look at the files in the archive. Additionally, commons-compress
                    //isn't PAX-aware so the pax_global_header added by Git (listing the archived commit) gets
                    //included as a file entry and needs to be ignored.
                    continue;
                }
                files.add(entry.getName());
            }
        }

        return files;
    }

    private List<String> getFileNames(Iterator<RpcStreamFragment> response,
                                      RpcArchiveFormat format) throws IOException {
        List<InputStream> fragments = Streams.stream(response)
                .filter(fragment -> FragmentOneofCase.STDOUT == fragment.getFragmentOneofCase())
                .map(fragment -> fragment.getStdout().getData())
                .filter(stdout -> !stdout.isEmpty())
                .map(ByteString::newInput)
                .collect(toList());
        InputStream inputStream = new SequenceInputStream(Collections.enumeration(fragments));

        List<String> files = new ArrayList<>();
        if (RpcArchiveFormat.ARCHIVE_FORMAT_TGZ == format) {
            files.addAll(listArchive(new TarArchiveInputStream(new GZIPInputStream(inputStream))));
        } else if (RpcArchiveFormat.ARCHIVE_FORMAT_ZIP == format) {
            files.addAll(listArchive(new ZipArchiveInputStream(inputStream)));
        } else {
            files.addAll(listArchive(new TarArchiveInputStream(inputStream)));
        }

        return files;
    }

    private String getResponseError(Iterator<RpcStreamFragment> response) {
        return Streams.stream(response)
                .filter(fragment -> FragmentOneofCase.STDERR == fragment.getFragmentOneofCase())
                .map(fragment -> fragment.getStderr().getData())
                .filter(stderr -> !stderr.isEmpty())
                .map(ByteString::toStringUtf8)
                .collect(joining())
                .toLowerCase();
    }
}
