package org.jenkinsci.plugins.github_integration.junit;

import antlr.ANTLRException;
import com.cloudbees.plugins.credentials.CredentialsScope;
import com.cloudbees.plugins.credentials.SystemCredentialsProvider;
import com.coravy.hudson.plugins.github.GithubProjectProperty;
import com.github.kostyasha.github.integration.branch.GitHubBranchTrigger;
import com.github.kostyasha.github.integration.branch.events.GitHubBranchEvent;
import com.github.kostyasha.github.integration.branch.events.impl.GitHubBranchCreatedEvent;
import hudson.util.Secret;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.lib.TextProgressMonitor;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.RemoteConfig;
import org.eclipse.jgit.transport.URIish;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
import org.jenkinsci.plugins.github.GitHubPlugin;
import org.jenkinsci.plugins.github.config.GitHubPluginConfig;
import org.jenkinsci.plugins.github.config.GitHubServerConfig;
import org.jenkinsci.plugins.github.pullrequest.GitHubPRTrigger;
import org.jenkinsci.plugins.github.pullrequest.GitHubPRTriggerMode;
import org.jenkinsci.plugins.github.pullrequest.events.GitHubPREvent;
import org.jenkinsci.plugins.github.pullrequest.events.impl.GitHubPRCommitEvent;
import org.jenkinsci.plugins.github.pullrequest.events.impl.GitHubPROpenEvent;
import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl;
import org.junit.rules.TemporaryFolder;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.jvnet.hudson.test.JenkinsRule;
import org.kohsuke.github.GHRepository;
import org.kohsuke.github.GitHub;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;

import static com.jayway.awaitility.Awaitility.await;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.apache.commons.io.FileUtils.writeStringToFile;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.notNullValue;
import static org.jenkinsci.plugins.github.config.GitHubServerConfig.loginToGithub;
import static org.jenkinsci.plugins.github_integration.awaitility.GHBranchAppeared.ghBranchAppeared;
import static org.jenkinsci.plugins.github_integration.awaitility.GHFromServerConfigAppeared.ghAppeared;
import static org.jenkinsci.plugins.github_integration.awaitility.GHRepoAppeared.ghRepoAppeared;
import static org.jenkinsci.plugins.github_integration.awaitility.GHRepoDeleted.ghRepoDeleted;

/**
 * @author Kanstantsin Shautsou
 */
public class GHRule implements TestRule {
    private static final Logger LOG = LoggerFactory.getLogger(GHRule.class);
    public static final String BRANCH1 = "branch-1";
    public static final String BRANCH2 = "branch-2";

    public static final String GH_TOKEN = System.getProperty("GH_TOKEN", System.getenv("GH_TOKEN"));

    private GHRepository ghRepo;
    private GitHub gitHub;
    private Git git;

    @CheckForNull
    private GitHubServerConfig gitHubServerConfig;

    @Nonnull
    private static JenkinsRule jRule;

    @Nonnull
    private static TemporaryFolder temporaryFolder;
    private File gitRootDir;

    public GHRule(JenkinsRule jRule, TemporaryFolder temporaryFolder) {
        this.jRule = jRule;
        this.temporaryFolder = temporaryFolder;
    }

    /**
     * github connection from configured github-plugin from global configuration.
     */
    public GitHub getGitHub() {
        return gitHub;
    }

    /**
     * GitHub repository that was created in {@link #before}.
     */
    public GHRepository getGhRepo() {
        return ghRepo;
    }


    @Override
    public Statement apply(final Statement base, final Description description) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                before(description);
                try {
                    base.evaluate();
                } finally {
                    after();
                }
            }
        };
    }

    private void after() {
        try {
            ghRepo.delete();
        } catch (IOException e) {
            LOG.error("Can't delete {}", ghRepo.getFullName(), e);
        }
    }


    public void before(Description description) throws IOException, GitAPIException, URISyntaxException, InterruptedException {
        String repoName = description.getClassName() + "-" + description.getMethodName();
        assertThat("Specify GH_TOKEN variable", GH_TOKEN, notNullValue());

        //reuse github client for GitHub preparation
        gitHubServerConfig = prepareGitHubPlugin();
        //FIXME no idea why github-plugin doesn't find configuration without delay, try delay
        await().timeout(20, SECONDS)
                .until(ghAppeared(gitHubServerConfig));

        gitHub = loginToGithub().apply(gitHubServerConfig);

        assertThat("Specify right GH_TOKEN variable!", gitHub, notNullValue());
        LOG.debug(gitHub.getRateLimit().toString());

        ghRepo = gitHub.getMyself().getRepository(repoName);
        if (ghRepo != null) {
            LOG.info("Deleting {}", ghRepo.getHtmlUrl());
            ghRepo.delete();
            await().pollInterval(3, SECONDS)
                    .timeout(120, SECONDS)
                    .until(ghRepoDeleted(gitHub, ghRepo.getFullName()));
        }

        ghRepo = gitHub.createRepository(repoName, "", "", true);
        LOG.info("Created {}", ghRepo.getHtmlUrl());

        await().pollInterval(2, SECONDS)
                .timeout(120, SECONDS)
                .until(ghRepoAppeared(gitHub, ghRepo.getFullName()));

        // prepare git
        gitRootDir = temporaryFolder.newFolder();

        git = Git.init().setDirectory(gitRootDir).call();

        writeStringToFile(new File(gitRootDir, "README.md"), "Test repo");
        git.add().addFilepattern(".").call();
        git.commit().setAll(true).setMessage("Initial commit").call();

        final RefSpec refSpec = new RefSpec("+refs/heads/*:refs/remotes/origin/*");

        final StoredConfig storedConfig = git.getRepository().getConfig();
        final RemoteConfig origin = new RemoteConfig(storedConfig, "origin");
        origin.addURI(new URIish(ghRepo.gitHttpTransportUrl()));
        origin.addPushRefSpec(refSpec);
        origin.update(storedConfig);
        storedConfig.save();

        commitFileToBranch(BRANCH1, BRANCH1 + ".file", "content", "commit for " + BRANCH1);

        commitFileToBranch(BRANCH2, BRANCH2 + ".file", "content", "commit for " + BRANCH2);

        git.checkout().setName("master").call();

        pushAll();
    }

    public void pushAll() throws GitAPIException {
        git.push()
                .setPushAll()
                .setProgressMonitor(new TextProgressMonitor())
                .setCredentialsProvider(new UsernamePasswordCredentialsProvider(GH_TOKEN, ""))
                .call();
    }

    /**
     * Prepare global GitHub plugin configuration.
     * Nothing specific to job.
     */
    public static GitHubServerConfig prepareGitHubPlugin() {
        // prepare global jRule settings
        final StringCredentialsImpl cred = new StringCredentialsImpl(
                CredentialsScope.GLOBAL,
                null,
                "description",
                Secret.fromString(GH_TOKEN)
        );

        SystemCredentialsProvider.getInstance().getCredentials().add(cred);

        final GitHubPluginConfig gitHubPluginConfig = GitHubPlugin.configuration();

        final List<GitHubServerConfig> gitHubServerConfigs = new ArrayList<>();
        final GitHubServerConfig gitHubServerConfig = new GitHubServerConfig(cred.getId());
        gitHubServerConfig.setManageHooks(false);
        gitHubServerConfig.setClientCacheSize(0);
        gitHubServerConfigs.add(gitHubServerConfig);

        gitHubPluginConfig.setConfigs(gitHubServerConfigs);

        return gitHubServerConfig;
    }

    public static GitHubPRTrigger getPreconfiguredPRTrigger() throws ANTLRException {
        final ArrayList<GitHubPREvent> gitHubPREvents = new ArrayList<>();
        gitHubPREvents.add(new GitHubPROpenEvent());
        gitHubPREvents.add(new GitHubPRCommitEvent());

        final GitHubPRTrigger gitHubPRTrigger = new GitHubPRTrigger("", GitHubPRTriggerMode.CRON, gitHubPREvents);
        gitHubPRTrigger.setPreStatus(true);

        return gitHubPRTrigger;
    }

    public static GitHubBranchTrigger getDefaultBranchTrigger() throws ANTLRException {
        final ArrayList<GitHubBranchEvent> githubEvents = new ArrayList<>();
        githubEvents.add(new GitHubBranchCreatedEvent());

        final GitHubBranchTrigger githubTrigger = new GitHubBranchTrigger("", GitHubPRTriggerMode.CRON, githubEvents);
        githubTrigger.setPreStatus(true);

        return githubTrigger;
    }


    public static GithubProjectProperty getPreconfiguredProperty(GHRepository ghRepo) {
        return new GithubProjectProperty(ghRepo.getHtmlUrl().toString());
    }

    public void commitFileToBranch(String branch, String fileName, String content, String commitMessage)
            throws IOException, GitAPIException {
        final String beforeBranch = git.getRepository().getBranch();
        final List<Ref> refList = git.branchList().call();
        boolean exist = false;
        for (Ref ref : refList) {
            if (ref.getName().endsWith(branch)) {
                exist = true;
                break;
            }
        }
        if (!exist) {
            git.branchCreate().setName(branch).call();
        }

        git.checkout().setName(branch).call();

        writeStringToFile(new File(gitRootDir, fileName), content);
        git.add().addFilepattern(".").call();
        git.commit().setAll(true).setMessage(commitMessage).call();
        git.push()
                .setPushAll()
                .setProgressMonitor(new TextProgressMonitor())
                .setCredentialsProvider(new UsernamePasswordCredentialsProvider(GH_TOKEN, ""))
                .call();
        git.checkout().setName(beforeBranch).call();

        await().pollInterval(3, SECONDS)
                .timeout(120, SECONDS)
                .until(ghBranchAppeared(getGhRepo(), branch));
    }

//    because org.jenkinsci.plugins.github.internal.GitHubLoginFunction$OkHttpConnector is private
//    public static void flushCache(GitHub gitHub) {
//        try {
//            OkHttpConnector okHttpConnector = (OkHttpConnector) gitHub.getConnector();
//            Field field = okHttpConnector.getClass().getField("urlFactory");
//            field.setAccessible(true);
//            OkUrlFactory urlFactory = (OkUrlFactory) field.get(okHttpConnector);
//            urlFactory.client().getCache().flush();
//        } catch (Exception ex) {
//            Throwables.propagate(ex);
//        }
//        LOG.debug("Flushed GitHub connector cache");
//    }
}