/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.cdancy.bitbucket.rest;

import com.cdancy.bitbucket.rest.auth.AuthenticationType;
import com.cdancy.bitbucket.rest.domain.admin.UserPage;
import com.cdancy.bitbucket.rest.domain.common.RequestStatus;
import com.cdancy.bitbucket.rest.domain.project.Project;
import com.cdancy.bitbucket.rest.domain.pullrequest.User;
import com.cdancy.bitbucket.rest.domain.repository.Repository;
import com.cdancy.bitbucket.rest.options.CreateProject;
import com.cdancy.bitbucket.rest.options.CreateRepository;
import com.google.common.base.Throwables;
import org.apache.commons.io.FileUtils;
import org.jclouds.util.Strings2;

import java.io.File;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.UUID;

import static com.google.common.io.BaseEncoding.base64;
import static org.assertj.core.api.Assertions.assertThat;

/**
 * Static methods for generating test data.
 */
public class TestUtilities extends BitbucketUtils {

    public static final String TEST_CREDENTIALS_SYSTEM_PROPERTY = "test.bitbucket.rest.credentials";
    public static final String TEST_CREDENTIALS_ENVIRONMENT_VARIABLE = TEST_CREDENTIALS_SYSTEM_PROPERTY.replaceAll("\\.", "_").toUpperCase();

    public static final String TEST_TOKEN_SYSTEM_PROPERTY = "test.bitbucket.rest.token";
    public static final String TEST_TOKEN_ENVIRONMENT_VARIABLE = TEST_TOKEN_SYSTEM_PROPERTY.replaceAll("\\.", "_").toUpperCase();

    private static final String GIT_COMMAND = "git";
    private static final char[] CHARS = "abcdefghijklmnopqrstuvwxyz".toCharArray();

    private static User defaultUser;
    private static File gitDirectory;

    /**
     * Get the default User from the passed credential String. Once user
     * is found we will cache for later usage.
     *
     * @param api Bitbucket api object.
     * @param auth the BitbucketAuthentication instance.
     * @return User or null if user can't be inferred.
     */
    public static synchronized User getDefaultUser(final BitbucketAuthentication auth, final BitbucketApi api) {
        if (defaultUser == null) {
            assertThat(auth).isNotNull();
            assertThat(api).isNotNull();

            if (auth.authType() == AuthenticationType.Basic) {
                final String username = new String(base64().decode(auth.authValue())).split(":")[0];
                final UserPage userPage = api.adminApi().listUsers(username, null, null);
                assertThat(userPage).isNotNull();
                assertThat(userPage.size() > 0).isTrue();
                for (final User user : userPage.values()) {
                    if (username.equals(user.slug())) {
                        defaultUser = user;
                        break;
                    }
                }
                assertThat(defaultUser).isNotNull();
            }
        }

        return defaultUser;
    }

    /**
     * Execute `args` at the `workingDir`.
     *
     * @param args list of arguments to pass to Process.
     * @param workingDir directory to execute Process within.
     * @return possible output of Process.
     * @throws Exception if Process could not be successfully executed.
     */
    public static String executionToString(final List<String> args, final Path workingDir) throws Exception {
        assertThat(args).isNotNull().isNotEmpty();
        assertThat(workingDir).isNotNull();
        assertThat(workingDir.toFile().isDirectory()).isTrue();

        final Process process = new ProcessBuilder(args)
                .redirectErrorStream(true)
                .directory(workingDir.toFile())
                .start();

        return Strings2.toStringAndClose(process.getInputStream());
    }

    /**
     * Generate a dummy file at the `baseDir` location.
     *
     * @param baseDir directory to generate the file under.
     * @return Path pointing at generated file.
     * @throws Exception if file could not be written.
     */
    public static Path initGeneratedFile(final Path baseDir) throws Exception {
        assertThat(baseDir).isNotNull();
        assertThat(baseDir.toFile().isDirectory()).isTrue();

        final String randomName = randomString();

        final List<String> lines = Arrays.asList(randomName);
        final Path file = Paths.get(new File(baseDir.toFile(), randomName + ".txt").toURI());
        return Files.write(file, lines, Charset.forName("UTF-8"));
    }

    /**
     * Initialize live test contents.
     *
     * @param endpoint Bitbucket endpoint.
     * @param credential Bitbucket credential string.
     * @param api Bitbucket api object.
     * @return GeneratedTestContents to use.
     */
    public static synchronized GeneratedTestContents initGeneratedTestContents(final String endpoint,
                                                                                final BitbucketAuthentication credential,
                                                                                final BitbucketApi api) {
        assertThat(endpoint).isNotNull();
        assertThat(credential).isNotNull();
        assertThat(api).isNotNull();

        // get possibly existing projectKey that user passed in
        String projectKey = System.getProperty("test.bitbucket.project");
        if (projectKey == null) {
            projectKey = randomStringLettersOnly();
        }

        // create test project if one does not already exist
        boolean projectPreviouslyExists = true;
        Project project = api.projectApi().get(projectKey);
        assertThat(project).isNotNull();
        if (!project.errors().isEmpty()) {

            projectPreviouslyExists = false;

            final CreateProject createProject = CreateProject.create(projectKey, null, null, null);
            project = api.projectApi().create(createProject);
            assertThat(project).isNotNull();
            assertThat(project.errors().isEmpty()).isTrue();
        }

        // create test repo that remains empty
        final String emptyRepoKey = randomStringLettersOnly();
        final CreateRepository createEmptyRepository = CreateRepository.create(emptyRepoKey, true);
        final Repository emptyRepository = api.repositoryApi().create(projectKey, createEmptyRepository);
        assertThat(emptyRepository).isNotNull();
        assertThat(emptyRepository.errors().isEmpty()).isTrue();

        // create test repo
        final String repoKey = randomStringLettersOnly();
        final CreateRepository createRepository = CreateRepository.create(repoKey, true);
        final Repository repository = api.repositoryApi().create(projectKey, createRepository);
        assertThat(repository).isNotNull();
        assertThat(repository.errors().isEmpty()).isTrue();

        final Path testDir = Paths.get(System.getProperty("test.bitbucket.basedir"));
        assertThat(testDir.toFile().exists()).isTrue();
        assertThat(testDir.toFile().isDirectory()).isTrue();

        final String randomName = randomString();
        gitDirectory = new File(testDir.toFile(), randomName);
        assertThat(gitDirectory.mkdirs()).isTrue();

        try {
            final String foundCredential = getDefaultUser(credential, api).slug();
            final URL endpointURL = new URL(endpoint);
            final int index = endpointURL.toString().indexOf(endpointURL.getHost());
            final String preCredentialPart = endpointURL.toString().substring(0, index);
            final String postCredentialPart = endpointURL.toString().substring(index, endpointURL.toString().length());

            final String generatedEndpoint = preCredentialPart
                    + foundCredential + "@"
                    + postCredentialPart + "/scm/"
                    + projectKey.toLowerCase() + "/"
                    + repoKey.toLowerCase() + ".git";

            generateGitContentsAndPush(generatedEndpoint);

        } catch (final Exception e) {
            throw Throwables.propagate(e);
        }

        final GeneratedTestContents gtc = new GeneratedTestContents(project, repository, emptyRepository.name(), projectPreviouslyExists);
        gtc.addRepoForDeletion(projectKey, emptyRepoKey);
        gtc.addRepoForDeletion(projectKey, repoKey);
        return gtc;
    }

    /**
     * Initialize git repository and add some randomly generated files.
     *
     * @param gitRepoURL git repository URL with embedded credentials.
     * @throws Exception if git repository could not be created or files added.
     */
    public static void generateGitContentsAndPush(final String gitRepoURL) throws Exception {

        // 1.) initialize git repository
        final String initGit = TestUtilities.executionToString(Arrays.asList(GIT_COMMAND, "init"), gitDirectory.toPath());
        System.out.println("git-init: " + initGit.trim());

        // 2.) create some random files and commit them
        for (int i = 0; i < 3; i++) {
            Path genFile = initGeneratedFile(gitDirectory.toPath());
            String addGit = TestUtilities.executionToString(Arrays.asList(GIT_COMMAND, "add", genFile.toFile().getPath()), gitDirectory.toPath());
            System.out.println("git-add-1: " + addGit.trim());
            String commitGit = TestUtilities.executionToString(Arrays.asList(GIT_COMMAND, "commit", "-m", "added"), gitDirectory.toPath());
            System.out.println("git-commit-1: " + commitGit.trim());

            // edit file again and create another commit
            genFile = Files.write(genFile, Arrays.asList(randomString()), Charset.forName("UTF-8"));
            addGit = TestUtilities.executionToString(Arrays.asList(GIT_COMMAND, "add", genFile.toFile().getPath()), gitDirectory.toPath());
            System.out.println("git-add-2: " + addGit.trim());
            commitGit = TestUtilities.executionToString(Arrays.asList(GIT_COMMAND, "commit", "-m", "added"), gitDirectory.toPath());
            System.out.println("git-commit-2: " + commitGit.trim());
            final String tagGit = TestUtilities
                    .executionToString(Arrays
                            .asList(GIT_COMMAND, "tag", "-a", "v0.0." + (i + 1), "-m", "\"generated version\""), gitDirectory.toPath());
            System.out.println("git-tag-commit: " + tagGit.trim());
        }

        // 3.) push changes to remote repository
        final String pushGit = TestUtilities.executionToString(Arrays.asList(GIT_COMMAND,
                "push",
                "--set-upstream",
                gitRepoURL,
                "master"), gitDirectory.toPath());
        System.out.println("git-push: " + pushGit);

        // 4.) create branch
        final String generatedBranchName = randomString();
        final String branchGit = TestUtilities.executionToString(Arrays.asList(GIT_COMMAND,
                "checkout", "-b",
                generatedBranchName),
                gitDirectory.toPath());
        System.out.println("git-branch: " + branchGit.trim());


        // 5.) generate random file for new branch
        final Path genFile = initGeneratedFile(gitDirectory.toPath());
        final String addGit = TestUtilities.executionToString(Arrays.asList(GIT_COMMAND, "add", genFile.toFile().getPath()), gitDirectory.toPath());
        System.out.println("git-branch-add: " + addGit.trim());
        final String commitGit = TestUtilities.executionToString(Arrays.asList(GIT_COMMAND, "commit", "-m", "added"), gitDirectory.toPath());
        System.out.println("git-branch-commit: " + commitGit.trim());

        // 6.) push branch
        final List<String> args = Arrays.asList(GIT_COMMAND, "push", "--tags", "-u", gitRepoURL, generatedBranchName);
        final String pushBranchGit = TestUtilities.executionToString(args, gitDirectory.toPath());
        System.out.println("git-branch-push: " + pushBranchGit);
    }

    /**
     * Terminate live test contents.
     *
     * @param api BitbucketApi object
     * @param generatedTestContents to terminate.
     */
    public static synchronized void terminateGeneratedTestContents(final BitbucketApi api,
            final GeneratedTestContents generatedTestContents) {
        assertThat(api).isNotNull();
        assertThat(generatedTestContents).isNotNull();

        final Project project = generatedTestContents.project;
        final Repository repository = generatedTestContents.repository;

        // delete main repository
        final RequestStatus success = api.repositoryApi().delete(project.key(), repository.name());
        assertThat(success).isNotNull();
        assertThat(success.value()).isTrue();
        assertThat(success.errors()).isEmpty();

        // delete all attached repos
        for (final String[] mapping : generatedTestContents.projectRepoMapping) {

            final String projectKey = mapping[0];
            final String repoKey = mapping[1];

            final RequestStatus successInner = api.repositoryApi().delete(projectKey, repoKey);
            assertThat(successInner).isNotNull();
            if (!successInner.errors().isEmpty()) {
                if (!successInner.errors().get(0).context().contains("does not exist")) {
                    throw new RuntimeException("Failed deleting repo: " + successInner.errors().get(0).context());
                }
            } else {
                assertThat(successInner.value()).isTrue();
                assertThat(successInner.errors()).isEmpty();
            }
        }

        // delete project
        if (!generatedTestContents.projectPreviouslyExists) {
            final RequestStatus deleteStatus = api.projectApi().delete(project.key());
            assertThat(deleteStatus).isNotNull();
            assertThat(deleteStatus.value()).isTrue();
            assertThat(deleteStatus.errors()).isEmpty();
        }

        // delete the local git directory
        assertThat(FileUtils.deleteQuietly(gitDirectory)).isTrue();
    }

    /**
     * Generate a random String with letters only.
     *
     * @return random String.
     */
    public static String randomStringLettersOnly() {
        final StringBuilder sb = new StringBuilder();
        final Random random = new Random();
        for (int i = 0; i < 10; i++) {
            final char randomChar = CHARS[random.nextInt(CHARS.length)];
            sb.append(randomChar);
        }
        return sb.toString().toUpperCase();
    }

    /**
     * Generate a random String with numbers and letters.
     *
     * @return random String.
     */
    public static String randomString() {
        return UUID.randomUUID().toString().replaceAll("-", "");
    }

    /**
     * Find credentials (Basic, Bearer, or Anonymous) from system/environment.
     *
     * @return BitbucketCredentials
     */
    public static BitbucketAuthentication inferTestAuthentication() {

        // 1.) Check for "Basic" auth credentials.
        final BitbucketAuthentication.Builder inferAuth = BitbucketAuthentication.builder();
        String authValue = BitbucketUtils
                .retriveExternalValue(TEST_CREDENTIALS_SYSTEM_PROPERTY,
                        TEST_CREDENTIALS_ENVIRONMENT_VARIABLE);
        if (authValue != null) {
            inferAuth.credentials(authValue);
        } else {

            // 2.) Check for "Bearer" auth token.
            authValue = BitbucketUtils
                    .retriveExternalValue(TEST_TOKEN_SYSTEM_PROPERTY,
                            TEST_TOKEN_ENVIRONMENT_VARIABLE);
            if (authValue != null) {
                inferAuth.token(authValue);
            }
        }

        // 3.) If neither #1 or #2 find anything "Anonymous" access is assumed.
        return inferAuth.build();
    }

    private TestUtilities() {
        throw new UnsupportedOperationException("Purposefully not implemented");
    }
}