package it.com.atlassian.stash.hosting;

import com.atlassian.bitbucket.hamcrest.RegexMatcher;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.test.*;
import com.atlassian.stash.hosting.HttpScmTransport;
import com.atlassian.stash.hosting.ScmTransport;
import com.atlassian.stash.hosting.SshKeyPair;
import com.atlassian.stash.hosting.SshScmTransport;
import com.google.common.collect.ImmutableList;
import org.junit.Before;
import org.junit.Test;

import java.io.File;
import java.util.Collection;
import java.util.Date;
import java.util.Optional;

import static java.util.Optional.ofNullable;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.*;
import static org.junit.runners.Parameterized.Parameters;

/**
 * Tests functionality offered by all {@link ScmTransport} implementations (e.g. clone/push/pull/error messaging).
 */
public class HostingTest extends BaseHostingTest {

    private static final String TEST_USER = "hosting-tester";
    private static final String REPOSITORY_NAME = "HostingTest testCloneThenPushThenPull -_- 1.2.3.4.5.6.7.8.9..git";
    private static final String REPOSITORY_SLUG = "hostingtest-testclonethenpushthenpull--_--1.2.3.4.5.6.7.8.9..git";

    private final String projectKey;
    private final ScmTransport transport;
    @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
    private final Optional<String> namespace;

    public HostingTest(ScmTransport transport, String namespace, String projectKey) {
        this.namespace = ofNullable(namespace);
        this.projectKey = projectKey;
        this.transport = transport;
    }

    @Parameters(name = "\"{0}\" on project \"{2}\" with namespace \"{1}\"")
    public static Collection<Object[]> data() throws Exception {
        HttpScmTransport http = new HttpScmTransport(TEST_USER, TEST_USER);
        SshScmTransport ssh = new SshScmTransport();

        //Verify a "normal" project key, as has been supported since 1.0
        String normal = DefaultFuncTestData.getProject1();
        //Verify a hyphenated project key, supported since 2.11
        String hyphenated = normal.replace('_', '-');

        return ImmutableList.of(
                new Object[]{ssh, null, normal},
                new Object[]{ssh, null, hyphenated},
                new Object[]{http, null, normal},
                new Object[]{http, null, hyphenated},
                // "rest" and "scm" were added to prevent regressing STASH-7553
                new Object[]{http, null, "rest"},
                new Object[]{http, null, "scm"},
                new Object[]{http, null, "plugins"},
                new Object[]{http, null, "projects"}
        );
    }

    @Before
    public void setup() throws Exception {
        if (!projectKey.equals(DefaultFuncTestData.getProject1())) {
            testContext.project(projectKey);
        }
        testContext.user(TEST_USER)
                .projectPermission(projectKey, TEST_USER, Permission.PROJECT_WRITE)
                .repository(projectKey, REPOSITORY_NAME)
                .accountKey(TEST_USER, SshKeyPair.get().getPublicKey());

        // Slug should be different from name to test url transform: CAV-501
        assertEquals(200, RepositoryTestHelper.getRepository(projectKey, REPOSITORY_SLUG).getStatusCode());
    }

    @Test
    public void testCloneThenPushThenPull() throws Exception {
        // create the repository
        File localRepo = tempFolder.newFolder("test-repo");
        HostingTestHelper.init(localRepo);

        // add a file, commit and push the repository to Stash
        HostingTestHelper.createAndAdd(localRepo, "test.txt", "sample text");
        HostingTestHelper.commit(localRepo, "test commit at " + new Date());
        transport.pushAllRefs(localRepo, namespace, projectKey, REPOSITORY_SLUG);

        // clone the repo from Stash
        File localClone = new File(tempFolder.getRoot(), "test-clone");
        transport.clone(localClone, namespace, projectKey, REPOSITORY_SLUG);

        // make a change to the clone and push
        HostingTestHelper.createAndAdd(localClone, "test-2.txt", "some other text");
        HostingTestHelper.commit(localClone, "test commit to clone at " + new Date());
        transport.pushAllRefs(localClone, namespace, projectKey, REPOSITORY_SLUG);

        // pull the change into the original repository
        transport.pull(localRepo, namespace, projectKey, REPOSITORY_SLUG);
        assertTrue("Newly committed test-2.txt was not pulled from Stash", new File(localRepo, "test-2.txt").exists());
    }

    @Test
    public void testShallowClone() throws Exception {
        // create the repository
        File localRepo = tempFolder.newFolder("test-repo");
        HostingTestHelper.init(localRepo);

        // add a file, commit and push the repository to Stash
        HostingTestHelper.createAndAdd(localRepo, "test.txt", "sample text");
        HostingTestHelper.commit(localRepo, "test commit at " + new Date());
        transport.pushAllRefs(localRepo, namespace, projectKey, REPOSITORY_SLUG);

        // perform a shallow clone of the repo from Stash
        File localClone = new File(tempFolder.getRoot(), "test-clone");
        transport.clone(localClone, namespace, projectKey, REPOSITORY_SLUG, "--depth", "1");
        assertTrue("Shallow clone doesn't contain expected file", new File(localRepo, "test.txt").exists());
    }

    @Test
    public void testPushWhenLicenseExceededSucceedsWithinGracePeriod() throws Exception {
        LicenseTestHelper.doWithLicense(() -> {
            // create the repository (in Stash and locally on disk)
            File localRepo = tempFolder.newFolder("test-repo");
            HostingTestHelper.init(localRepo);

            // add a file, commit and push the repository to Stash
            HostingTestHelper.createAndAdd(localRepo, "test.txt", "sample text");
            HostingTestHelper.commit(localRepo, "test commit at " + new Date());
            transport.pushAllRefs(localRepo, namespace, projectKey, REPOSITORY_SLUG);

            // invalidate the license
            LicenseTestHelper.setLicense(LicenseTestHelper.SINGLE_USER_LICENSE.get());

            // clone the repo from Stash (should still be allowed with invalid license)
            File localClone = new File(tempFolder.getRoot(), "test-clone");
            transport.clone(localClone, namespace, projectKey, REPOSITORY_SLUG);

            // make a change to the clone and push (this should fail)
            HostingTestHelper.createAndAdd(localClone, "test-2.txt", "some other text");
            HostingTestHelper.commit(localClone, "test commit to clone at " + new Date());

            ProcessResult result = transport.pushAllRefs(localClone, namespace, projectKey, REPOSITORY_SLUG);

            return null;
        });
    }

    @Test
    public void testCloneAndPullWithInvalidLicense() throws Exception {
        LicenseTestHelper.doWithLicense(() -> {
            // create the repository (in Stash and locally on disk)
            File localRepo = tempFolder.newFolder("test-repo");
            HostingTestHelper.init(localRepo);

            // add a file, commit and push the repository to Stash
            HostingTestHelper.createAndAdd(localRepo, "test.txt", "sample text");
            HostingTestHelper.commit(localRepo, "test commit at " + new Date());
            transport.pushAllRefs(localRepo, namespace, projectKey, REPOSITORY_SLUG);

            // invalidate the license
            LicenseTestHelper.setLicense(LicenseTestHelper.SINGLE_USER_LICENSE.get());

            // clone the repo from Stash (should still be allowed with invalid license)
            File localClone = new File(tempFolder.getRoot(), "test-clone");
            transport.clone(localClone, namespace, projectKey, REPOSITORY_SLUG);
            transport.pull(localRepo, namespace, projectKey, REPOSITORY_SLUG);

            return null;
        });
    }

    @Test
    public void testPullFromNonExistentAndRestrictedRepositories() throws Exception {
        // create the repository (in Stash and locally on disk)
        File localRepo = tempFolder.newFolder("test-repo");
        HostingTestHelper.init(localRepo);

        // add a file, commit and push the repository to Stash
        HostingTestHelper.createAndAdd(localRepo, "test.txt", "sample text");
        HostingTestHelper.commit(localRepo, "test commit at " + new Date());
        transport.pushAllRefs(localRepo, namespace, projectKey, REPOSITORY_SLUG);

        // cache the stderr value for a non-existent repository (to assert that it is the same for restricted repository)
        String stderrForMissingRepository = null;

        try {
            File localClone = new File(tempFolder.getRoot(), "test-clone");
            transport.clone(localClone, namespace, projectKey, "not-a-real-repository-slug");
            fail("Clone should have failed due to non-existent repository!");
        } catch (ProcessFailedException e) {
            assertRepositoryNotFound(e);
            stderrForMissingRepository = messageWithoutRepositoryUrl(e);
        }

        try {
            // revoke user's permission to read the repository and then attempt to clone it
            PermissionsTestHelper.revokeProjectPermissionForUser(projectKey, TEST_USER);
            try {
                File localClone = new File(tempFolder.getRoot(), "test-clone");
                transport.clone(localClone, namespace, projectKey, REPOSITORY_SLUG);
                fail("Pull should have failed for lack of repository READ permissions!");
            } catch (ProcessFailedException e) {
                assertRepositoryNotFound(e);
                assertEquals("stderr for missing & restricted repositories should be identical to prevent leaking repository names",
                        stderrForMissingRepository, messageWithoutRepositoryUrl(e));
            }
        } finally {
            PermissionsTestHelper.setProjectPermissionForUser(projectKey, TEST_USER, Permission.PROJECT_WRITE.name());
        }
    }

    @Test
    public void testPushToNonExistentAndRestrictedRepositories() throws Exception {
        // create the repository (in Stash and locally on disk)
        File localRepo = tempFolder.newFolder("test-repo");
        HostingTestHelper.init(localRepo);

        // add a file, commit and push the repository to Stash
        HostingTestHelper.createAndAdd(localRepo, "test.txt", "sample text");
        HostingTestHelper.commit(localRepo, "test commit at " + new Date());
        transport.pushAllRefs(localRepo, namespace, projectKey, REPOSITORY_SLUG);

        // cache the stderr value for a non-existent repository (to assert that it is the same for restricted repository)
        String stderrForMissingRepository = null;

        try {
            transport.pushAllRefs(localRepo, namespace, projectKey, "not-a-real-repository-slug");
            fail("Push should have failed due to non-existent repository!");
        } catch (ProcessFailedException e) {
            assertRepositoryNotFound(e);
            stderrForMissingRepository = messageWithoutRepositoryUrl(e);
        }

        try {
            // revoke user's permission to read the repository and then attempt to push to it
            PermissionsTestHelper.revokeProjectPermissionForUser(projectKey, TEST_USER);
            try {
                HostingTestHelper.createAndAdd(localRepo, "test2.txt", "more sample text");
                HostingTestHelper.commit(localRepo, "test commit at " + new Date());
                transport.pushAllRefs(localRepo, namespace, projectKey, REPOSITORY_SLUG);
                fail("Push should have failed for lack of repository READ permissions!");
            } catch (ProcessFailedException e) {
                assertRepositoryNotFound(e);
                assertEquals("stderr for missing & restricted repositories should be identical to prevent leaking repository names",
                        stderrForMissingRepository, messageWithoutRepositoryUrl(e));
            }

            // grant READ permission to the user and then attempt to push to it again
            PermissionsTestHelper.setProjectPermissionForUser(projectKey, TEST_USER, Permission.PROJECT_READ.name());
            try {
                transport.pushAllRefs(localRepo, namespace, projectKey, REPOSITORY_SLUG);
                fail("Push should have failed for lack of repository WRITE permissions!");
            } catch (ProcessFailedException e) {
                assertThat("Git client should output 'Insufficient permissions' message.",
                        e.getStdErr(), containsString("Insufficient permissions"));
            }

            // grant WRITE permission to the user - next push should succeed
            PermissionsTestHelper.setProjectPermissionForUser(projectKey, TEST_USER, Permission.PROJECT_WRITE.name());
            transport.pushAllRefs(localRepo, namespace, projectKey, REPOSITORY_SLUG);
        } finally {
            PermissionsTestHelper.setProjectPermissionForUser(projectKey, TEST_USER, Permission.PROJECT_WRITE.name());
        }
    }

    private void assertRepositoryNotFound(ProcessFailedException e) {
        assertThat("Git client should output 'repository not found' message.",
                e.getStdErr(), RegexMatcher.matchesRegex("(?i)repository.* not found"));
    }

    private static String messageWithoutRepositoryUrl(ProcessFailedException e) {
        String message = e.getStdErr();
        return message.replaceFirst("\\S*/scm/\\S*", "");
    }
}
