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

import com.atlassian.bitbucket.mesh.AbstractWiredTest;
import com.atlassian.bitbucket.mesh.git.builder.GitProcessBuilderFactory;
import com.atlassian.bitbucket.mesh.process.handler.LoggingStdioHandler;
import com.atlassian.bitbucket.mesh.process.handler.StringStdioHandler;
import com.atlassian.bitbucket.mesh.repository.*;
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.common.collect.ImmutableMap;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;

import javax.annotation.Resource;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.atlassian.bitbucket.mesh.git.GitConstants.PATH_LOGS;
import static com.atlassian.bitbucket.mesh.io.MoreFiles.*;
import static org.hamcrest.Matchers.blankString;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;

public class ShinyGitPorcelainTest extends AbstractWiredTest {

    private static final Pattern PATTERN_COMMIT_SHA = Pattern.compile("\\[.+\\s([a-f0-9]{40})]\\s.*$");

    @Resource
    protected RepositoryManager repositoryManager;
    @Resource
    private ShinyGitPorcelain porcelain;
    @Repo
    private ManagedRepository repository;
    @Autowired
    private GitProcessBuilderFactory builderFactory;

    @DirtiesRepo
    @Test
    public void testStreamReflogs() throws IOException {
        //Subtract 1s to account for differences in time resolution provided by different file systems
        Instant minimumLastModified = Instant.now().minus(1, ChronoUnit.SECONDS);

        writeSharedReflog("refs/heads/master", "shared master reflog");
        writeReflog("refs/heads/master", "master reflog");
        writeSharedReflog("refs/heads/intermediate_branching", "shared intermediate_branching reflog");
        writeSharedReflog("refs/pull-requests/1/from", "shared pr#1 reflog");
        writeSharedReflog("refs/pull-requests/2/from", "shared pr#2 reflog");
        writeReflog("refs/heads/out_of_order_branch", "out_of_order reflog");
        writeReflog("refs/heads/basic_branching", "basic_branching reflog");

        ImmutableMap.Builder<String, Long> builder = ImmutableMap.builder();
        porcelain.streamReflogs(new StreamReflogsParameters.Builder(repository).build(), reflog -> {
            builder.put(reflog.getRefId(), reflog.getSize());
            assertTrue(reflog.getSize() > 0L);
            assertThat(reflog.getLastModified(), greaterThanOrEqualTo(minimumLastModified));
            return false;
        });

        assertEquals(ImmutableMap.of(
                "refs/heads/basic_branching", 22L,
                "refs/heads/intermediate_branching", 36L,
                "refs/heads/master", 13L,
                "refs/heads/out_of_order_branch", 19L,
                "refs/pull-requests/1/from", 18L,
                "refs/pull-requests/2/from", 18L),
                builder.build());

        //Now verify that the SatiableConsumer contract is respected
        AtomicInteger counter = new AtomicInteger();
        porcelain.streamReflogs(new StreamReflogsParameters.Builder(repository).build(), ref -> {
            counter.incrementAndGet();
            return true; // should stop iteration
        });

        assertEquals(1, counter.get());
    }

    // This test verifies the directory traversal for sparse checkouts.
    // Note: This test will run forever if the fix for BSERV-13949 is not implemented
    @Test
    public void testWorkTreeWithPristineCheckoutWillTraverseDirectories() throws IOException {
        Repository repository = repositoryManager.create(new CreateRepositoryParameters.Builder(
                BitbucketServerRepository.formatId("12233344445555543210", "1")).build());
        Path initialWorkTree = porcelain.workTree(new WorkTreeParameters.Builder(repository, null).build());

        //Create directories and files with the following structure:
        //  - a/a1/README.md
        //  - b/instructions.txt
        touch(resolve(mkdir(resolve(initialWorkTree, "a", "a1")), "README.md"));
        touch(resolve(mkdir(resolve(initialWorkTree, "b")), "instructions.txt"));

        //Add the new files
        builderFactory.builder()
                .workDir(initialWorkTree)
                .command("add")
                .argument("a/a1")
                .argument("b")
                .build(new StringStdioHandler())
                .run();

        //Create a commit
        String commitDetails = builderFactory.builder()
                .workDir(initialWorkTree)
                .command("commit")
                .author(new Person("Jane Doe", "jdoe@atlassian.com"))
                .argument("-m")
                .argument("Test commit")
                .withConfiguration("core.abbrev", 40)
                .build(new StringStdioHandler())
                .run();
        assertNotNull("git commit didn't write to stdout", commitDetails);

        //Fetch the commit from the initial work tree back into the repository
        builderFactory.builder(repository).fetch()
                .refspec("refs/heads/*:refs/heads/*")
                .repository(initialWorkTree.toString())
                .build(new LoggingStdioHandler())
                .run();

        Matcher matcher = PATTERN_COMMIT_SHA.matcher(commitDetails.split("\n")[0]);
        assertTrue("\"" + commitDetails + "\" doesn't follow the expected format", matcher.matches());
        String commitId = matcher.group(1);

        WorkTreeParameters request = new WorkTreeParameters.Builder(repository, commitId)
                .pristine(true)
                .build();
        Path workTree = porcelain.workTree(request);

        assertNotNull(workTree);
        assertPristine(workTree);

        String sparseCheckout = builderFactory.builder()
                .workDir(workTree)
                .config()
                .get("core.sparseCheckout")
                .build(new StringStdioHandler())
                .run();
        assertEquals("true", sparseCheckout);
    }

    private void assertPristine(Path workTree) {
        //Verify that the work tree is pristine, with no staged or unstaged changes
        String status = builderFactory.builder()
                .workDir(workTree)
                .status()
                .porcelain(true)
                .untracked("all")
                .build(new StringStdioHandler())
                .run();
        assertThat("The work tree should be pristine", status, blankString());
    }

    private void writeReflog(String refId, String content) throws IOException {
        Path path = repository.getPath().resolve(PATH_LOGS).resolve(refId);
        mkdir(path.getParent());
        write(path, content, StandardCharsets.UTF_8);
    }

    private void writeSharedReflog(String refId, String content) throws IOException {
        Path path = repository.getSharedObjects().getReflogDir(repository).resolve(refId);
        mkdir(path.getParent());
        write(path, content, StandardCharsets.UTF_8);
    }
}