package com.atlassian.stash.internal.scm.git.command;

import com.atlassian.bitbucket.i18n.SimpleI18nService;
import com.atlassian.bitbucket.repository.*;
import com.atlassian.bitbucket.scm.Command;
import com.atlassian.bitbucket.scm.DefaultCommandExitHandler;
import com.atlassian.bitbucket.scm.ResolveRefsCommandParameters;
import com.atlassian.bitbucket.scm.annotation.DirtiesRepo;
import com.atlassian.bitbucket.scm.annotation.Repo;
import com.atlassian.bitbucket.scm.git.GitRefPattern;
import com.atlassian.bitbucket.scm.git.GitScm;
import com.atlassian.bitbucket.scm.git.command.GitCommandBuilderFactory;
import com.atlassian.stash.internal.scm.git.AbstractGitTest;
import com.atlassian.stash.internal.scm.git.InternalGitAgent;
import com.atlassian.stash.internal.scm.git.command.foreachref.HeadsForEachRefOutputHandler;
import com.atlassian.stash.internal.scm.git.util.ConfigurableGitScmConfig;
import com.google.common.collect.Sets;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.springframework.beans.factory.annotation.Autowired;

import java.io.File;
import java.util.*;
import java.util.concurrent.ExecutorService;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class ResolveRefsCommandTest extends AbstractGitTest {

    private static final Branch BASIC_BRANCHING = branch("basic_branching", "d6edcbf924697ab811a867421dab60d954ccad99");
    private static final Branch BRANCH_MOD_MERGE = branch("branch_mod_merge", "6053a1eaa1c009dd11092d09a72f3c41af1b59ad");
    private static final Branch MASTER = branch("master", "d6edcbf924697ab811a867421dab60d954ccad99");
    private static final Branch OUT_OF_ORDER = branch("out_of_order_branch", "2e10dd2d1d5eea9291b296e78312e8a703964a95");
    private static final Tag RETAGGED_SIGNED_TAG = tag("retagged_signed_tag",
            "9b4e781dea0769888fe270e06ad76675f73851b1", "0a943a29376f2336b78312d99e65da17048951db");
    private static final Tag SIGNED_TAG = tag("signed_tag",
            "12ebe2a58367347cd39f19f5a72f3cbec7b8f9a9", "0a943a29376f2336b78312d99e65da17048951db");

    @Rule
    public TemporaryFolder tmpFolder = new TemporaryFolder();
    @Autowired
    private InternalGitAgent agent;
    @Autowired
    private ConfigurableGitScmConfig config;
    @Autowired
    private GitCommandBuilderFactory commandBuilderFactory;
    @Autowired
    private ExecutorService executorService;
    @Repo
    private Repository repository;

    @Test
    public void testBranches() {
        Map<String, Ref> refs = runCommand(new ResolveRefsCommandParameters.Builder()
                .branchIds(MASTER.getId(), BASIC_BRANCHING.getDisplayId(), "no-such-branch")
                .build());

        assertEquals(2, refs.size());
        assertEquals(BASIC_BRANCHING, refs.get(BASIC_BRANCHING.getDisplayId()));
        assertEquals(MASTER, refs.get(MASTER.getId()));
    }

    @Test
    public void testTags() {
        Map<String, Ref> refs = runCommand(new ResolveRefsCommandParameters.Builder()
                .tagIds(SIGNED_TAG.getId(), RETAGGED_SIGNED_TAG.getDisplayId(), "no-such-tag")
                .build());

        assertEquals(2, refs.size());
        assertEquals(SIGNED_TAG, refs.get(SIGNED_TAG.getId()));
        assertEquals(RETAGGED_SIGNED_TAG, refs.get(RETAGGED_SIGNED_TAG.getDisplayId()));
    }

    @Test
    public void testMixed() {
        Map<String, Ref> refs = runCommand(new ResolveRefsCommandParameters.Builder()
                .branchIds(MASTER.getId(), BASIC_BRANCHING.getDisplayId())
                .branchId("no-such-branch")
                .tagIds(SIGNED_TAG.getId(), RETAGGED_SIGNED_TAG.getDisplayId())
                .tagId("no-such-tag")
                .refIds(OUT_OF_ORDER.getDisplayId())
                .refId("no-such-ref")
                .build());

        assertEquals(5, refs.size());
        assertEquals(BASIC_BRANCHING, refs.get(BASIC_BRANCHING.getDisplayId()));
        assertEquals(MASTER, refs.get(MASTER.getId()));
        assertEquals(OUT_OF_ORDER, refs.get(OUT_OF_ORDER.getDisplayId()));
        assertEquals(RETAGGED_SIGNED_TAG, refs.get(RETAGGED_SIGNED_TAG.getDisplayId()));
        assertEquals(SIGNED_TAG, refs.get(SIGNED_TAG.getId()));
    }

    @DirtiesRepo
    @Test
    public void testWithSha() {
        // create two branches:
        // - firstBranch (000aaa) which should be returned first because of alpha ordering
        // - problemBranch (57150c54c3) whose branch-name matches the hash of firstBranch
        // when we try to resolve "57150c54c3", we should get problemBranch and _not_ firstBranch
        Branch firstBranch = branch("000aaa", "57150c54c38d6570b2fd5e6d6cfc19476de44e84");
        Branch problemBranch = branch(firstBranch.getLatestCommit().substring(0, 10), "e26cab95a50f6e99d2c46c498a23cee307cc8e27");
        createBranch(firstBranch);
        createBranch(problemBranch);

        Map<String, Ref> refs = runCommand(new ResolveRefsCommandParameters.Builder()
                .branchIds("heads/master", BASIC_BRANCHING.getDisplayId())
                .branchId("no-such-branch")
                .tagIds(SIGNED_TAG.getId(), RETAGGED_SIGNED_TAG.getDisplayId())
                .tagId("no-such-tag")
                .refIds(OUT_OF_ORDER.getDisplayId())
                .refId("no-such-ref")
                .refIds("6053a1eaa", problemBranch.getDisplayId())
                .build());

        assertEquals(BASIC_BRANCHING, refs.get(BASIC_BRANCHING.getDisplayId()));
        assertEquals(MASTER, refs.get("heads/master"));
        assertEquals(RETAGGED_SIGNED_TAG, refs.get(RETAGGED_SIGNED_TAG.getDisplayId()));
        assertEquals(OUT_OF_ORDER, refs.get(OUT_OF_ORDER.getDisplayId()));
        assertEquals(SIGNED_TAG, refs.get(SIGNED_TAG.getId()));
        assertEquals(BRANCH_MOD_MERGE, refs.get("6053a1eaa"));
        // problem branch should be returned _instead_ of firstBranch
        assertEquals(problemBranch, refs.get(problemBranch.getDisplayId()));
        assertEquals(7, refs.size());

    }

    //@Test
    public void perfTest() throws Exception {
        File repoDir = new File("/Users/michael/tmp/confluence.git");
        Repository conf = mock(Repository.class);
        when(conf.getId()).thenReturn(999);
        when(conf.getScmId()).thenReturn(GitScm.ID);
        when(conf.getSlug()).thenReturn("conf");

        config.setRepositoryDir(conf, repoDir);

        String[] refs = {
                "master", "stable-issue/CONFDEV-32255", "upgraderefactor", // branches
                "deploy/5.7-D20141020T031534", "deploy-success-build-4404", "/confluence-project-5.4-OD-14", // tags
                "d1cb137", "c9e7602" // hashes
        }; // tags
        int iterations = 50;
        for (int i = 0; i < 5; ++i) {
            resolveRefsBulk(conf, refs);
            resolveRefsForEachRef(conf, refs);
        }

        long start = System.nanoTime();
        for (int i = 0; i < iterations; ++i) {
//            resolveRefsBulk(conf, refs);
        }

        double bulkTimeNs = ((System.nanoTime() - start) / iterations) / 1000000.0;
        start = System.nanoTime();
        for (int i = 0; i < iterations; ++i) {
            for (String ref : refs) {
                resolveRefsForEachRef(conf, ref);
            }
        }
        double forEachTimeNs = ((System.nanoTime() - start) / iterations) / 1000000.0;

        System.out.println("Bulk: " + bulkTimeNs + "ns, for-each: " + forEachTimeNs);
    }

    private void resolveRefsBulk(Repository repository, String...refs) {
        runCommand(repository, new ResolveRefsCommandParameters.Builder().refIds(Arrays.asList(refs)).build());
    }

    private void resolveRefsForEachRef(Repository repository, String...refs) {
        Set<String> toResolve = Sets.newHashSet(refs);
        HeadsForEachRefOutputHandler handler = new HeadsForEachRefOutputHandler(ref -> {
            toResolve.remove(ref.getDisplayId());
            toResolve.remove(ref.getId());
            return !toResolve.isEmpty();
        }, agent.getHead(repository));

        commandBuilderFactory.builder(repository).forEachRef()
                .format(handler.getFormat())
                .patterns(GitRefPattern.HEADS.getPath(), GitRefPattern.TAGS.getPath())
                .exitHandler(new DefaultCommandExitHandler(new SimpleI18nService()) {
                    @Override
                    protected boolean isError(String command, int exitCode, String stdErr, Throwable thrown) {
                        return false;
                    }
                })
                .build(handler)
                .call();
    }

    protected Command<Map<String, Ref>> buildCommand(Repository repository, ResolveRefsCommandParameters parameters) {
        return new ResolveRefsCommand(executorService, agent, commandBuilderFactory,
                new SimpleI18nService(SimpleI18nService.Mode.RETURN_KEYS), repository, parameters);
    }

    private static Branch branch(String name, String commitId) {
        return new SimpleBranch.Builder()
                .id(GitRefPattern.HEADS.qualify(name))
                .displayId(name)
                .latestCommit(commitId)
                .build();
    }

    private static Tag tag(String name, String hash, String commitId) {
        return new SimpleTag.Builder()
                .id(GitRefPattern.TAGS.qualify(name))
                .displayId(name)
                .hash(hash)
                .latestCommit(commitId)
                .build();
    }

    private void createBranch(Branch branch) {
        commandBuilderFactory.builder(repository).updateRef()
                .set(branch.getId(), branch.getLatestCommit())
                .build()
                .call();
    }

    private Map<String, Ref> runCommand(ResolveRefsCommandParameters parameters) {
        return runCommand(repository, parameters);
    }

    private Map<String, Ref> runCommand(Repository repository, ResolveRefsCommandParameters parameters) {
        return buildCommand(repository, parameters).call();
    }
}