package it.com.atlassian.stash.rest.branches;

import com.atlassian.bitbucket.test.GitTestHelper;
import com.atlassian.bitbucket.test.ProcessTestHelper;
import com.atlassian.bitbucket.test.RepositoryTestHelper;
import com.atlassian.bitbucket.permission.Permission;
import com.google.common.base.Function;
import com.jayway.restassured.RestAssured;
import com.jayway.restassured.http.ContentType;
import com.jayway.restassured.specification.RequestSpecification;
import it.com.atlassian.stash.BaseRestTest;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.core.io.ClassPathResource;

import java.util.Map;

import static com.atlassian.bitbucket.hamcrest.MoreMatchers.containsInOrder;
import static com.atlassian.bitbucket.test.BranchModelTestHelper.*;
import static com.atlassian.bitbucket.test.DefaultFuncTestData.*;
import static com.atlassian.bitbucket.test.rest.JsonMatchers.*;
import static com.jayway.restassured.RestAssured.withArgs;
import static javax.ws.rs.core.Response.Status.*;
import static org.hamcrest.Matchers.*;

public class BranchModelResourceTest extends BaseRestTest {

    public static final String REPOSITORY_WITH_DATA = "BRANCH_MODEL_FULL";
    public static final String REPOSITORY_EMPTY = "BRANCH_MODEL_EMPTY";

    public static final String USER_WITH_READ_ACCESS = "hasreadaccess";
    public static final String USER_WITH_WRITE_ACCESS = "haswriteaccess";
    public static final String USER_WITHOUT_ACCESS = "hasnoaccess";

    /**
     * Create test repositories to use for all the tests. The contents of the repositories don't change so we
     * can create them once for the whole class.
     */
    @BeforeClass
    public static void createTestRepositoriesAndUsers() throws Exception {
        // repositories
        getClassTestContext().repository(getProject1(), REPOSITORY_WITH_DATA, new ClassPathResource(RepositoryTestHelper.DEFAULT_REPOSITORY),
                new Function<ProcessTestHelper, Object>() {
                    @Override
                    public Object apply(ProcessTestHelper input) {
                        return input.execute(GitTestHelper.getGitPath(), "branch", "develop");
                    }
                });
        getClassTestContext().repository(getProject1(), REPOSITORY_EMPTY);
        // users
        getClassTestContext().user(USER_WITH_READ_ACCESS)
                .repositoryPermission(getProject1(), REPOSITORY_WITH_DATA, USER_WITH_READ_ACCESS, Permission.REPO_READ)
                .repositoryPermission(getProject1(), REPOSITORY_EMPTY, USER_WITH_READ_ACCESS, Permission.REPO_READ);
        getClassTestContext().user(USER_WITH_WRITE_ACCESS)
                .repositoryPermission(getProject1(), REPOSITORY_WITH_DATA, USER_WITH_WRITE_ACCESS, Permission.REPO_WRITE)
                .repositoryPermission(getProject1(), REPOSITORY_EMPTY, USER_WITH_WRITE_ACCESS, Permission.REPO_WRITE);
        getClassTestContext().user(USER_WITHOUT_ACCESS);
    }

    /**
     * Remove branch model from repositories after each test.
     */
    @After
    public void cleanUpModel() {
        deleteBranchModel(REPOSITORY_WITH_DATA);
        deleteBranchModel(REPOSITORY_EMPTY);
    }

    @Test
    public void testGetExistingModelNonEmptyRepository() {
        createDefaultBranchModel(REPOSITORY_WITH_DATA);

        requestAs(USER_WITH_READ_ACCESS)
                .expect().statusCode(OK.getStatusCode())
                .log().ifError()
                .body("development.id", is("refs/heads/master"))
                .body("production", nullValue())
                .body("types[%d]", withArgs(0), isTypeWith("BUGFIX", "bugfix/"))
                .body("types[%d]", withArgs(1), isTypeWith("FEATURE", "feature/"))
                .body("types[%d]", withArgs(2), isTypeWith("HOTFIX", "hotfix/"))
                .body("types[%d]", withArgs(3), isTypeWith("RELEASE", "release/"))
                .get(getBranchModelPath(REPOSITORY_WITH_DATA));
    }

    @Test
    public void testGetExistingModelWithWriteAndAdminAccess() {
        createDefaultBranchModel(REPOSITORY_WITH_DATA);

        requestAs(USER_WITH_WRITE_ACCESS)
                .expect().statusCode(OK.getStatusCode())
                .log().ifError()
                .get(getBranchModelPath(REPOSITORY_WITH_DATA));
        requestAsAdmin()
                .expect().statusCode(OK.getStatusCode())
                .log().ifError()
                .get(getBranchModelPath(REPOSITORY_WITH_DATA));
    }

    @Test
    public void testGetExistingModelWithNoAccess() {
        createDefaultBranchModel(REPOSITORY_WITH_DATA);

        requestAs(USER_WITHOUT_ACCESS)
                .expect().statusCode(UNAUTHORIZED.getStatusCode())
                .log().ifStatusCodeMatches(not(UNAUTHORIZED.getStatusCode()))
                .get(getBranchModelPath(REPOSITORY_WITH_DATA));
    }

    @Test
    public void testGetNonExistingModel() {
        requestAs(USER_WITH_READ_ACCESS)
                .expect().statusCode(NOT_FOUND.getStatusCode())
                .log().ifStatusCodeMatches(not(NOT_FOUND.getStatusCode()))
                .get(getBranchModelPath(REPOSITORY_WITH_DATA));
    }

    @Test
    public void testGetModelForEmptyRepository() {
        createDefaultBranchModel(REPOSITORY_EMPTY);

        requestAs(USER_WITH_READ_ACCESS)
                .expect().statusCode(CONFLICT.getStatusCode())
                .log().ifStatusCodeMatches(not(CONFLICT.getStatusCode()))
                .get(getBranchModelPath(REPOSITORY_EMPTY));
    }

    @Test
    public void testPutAndGetDefaultConfigurationOnRepositoryWithoutBranchModelConfigured() {
        createDefaultBranchModel(REPOSITORY_WITH_DATA);

        requestAsAdmin()
                .expect().statusCode(OK.getStatusCode())
                .log().ifError()
                .body("development.refId", nullValue())
                .body("development.useDefault", is(true))
                .body("production", nullValue())
                .body("types[%d]", withArgs(0), isEnabledTypeWith("BUGFIX", "bugfix/"))
                .body("types[%d]", withArgs(1), isEnabledTypeWith("FEATURE", "feature/"))
                .body("types[%d]", withArgs(2), isEnabledTypeWith("HOTFIX", "hotfix/"))
                .body("types[%d]", withArgs(3), isEnabledTypeWith("RELEASE", "release/"))
                .get(getBranchModelConfigurationPath(REPOSITORY_WITH_DATA));
    }

    @Test
    public void testGetConfigurationWorksOnEmptyRepository() {
        createDefaultBranchModel(REPOSITORY_EMPTY);

        requestAsAdmin()
                .expect().statusCode(OK.getStatusCode())
                .log().ifError()
                .body("development.refId", nullValue())
                .body("development.useDefault", is(true))
                .body("production", nullValue())
                .body("types[%d]", withArgs(0), isEnabledTypeWith("BUGFIX", "bugfix/"))
                .body("types[%d]", withArgs(1), isEnabledTypeWith("FEATURE", "feature/"))
                .body("types[%d]", withArgs(2), isEnabledTypeWith("HOTFIX", "hotfix/"))
                .body("types[%d]", withArgs(3), isEnabledTypeWith("RELEASE", "release/"))
                .get(getBranchModelConfigurationPath(REPOSITORY_EMPTY));
    }

    @Test
    public void testGetConfigurationWithNoAdminPermission() {
        requestAs(USER_WITH_WRITE_ACCESS)
                .expect().statusCode(UNAUTHORIZED.getStatusCode())
                .log().ifStatusCodeMatches(not(UNAUTHORIZED.getStatusCode()))
                .get(getBranchModelConfigurationPath(REPOSITORY_WITH_DATA));
    }

    @Test
    public void testPutValidConfigurationWithNoEnabledTypes() {
        requestAsAdmin()
                .body(createBranchModelJson(
                        createBranchConfiguration("refs/heads/master", false),
                        null)
                ).expect().statusCode(CREATED.getStatusCode())
                .log().ifError()
                .put(getBranchModelConfigurationPath(REPOSITORY_WITH_DATA));
    }

    @Test
    public void testPutNewConfigurationUseDefault() {
        requestAsAdmin()
                .body(createBranchModelUseDefaultJson())
                .expect().statusCode(CREATED.getStatusCode())
                .log().ifStatusCodeMatches(not(CREATED.getStatusCode()))
                .body("development.refId", nullValue())
                .body("development.useDefault", is(true))
                .body("production", Matchers.nullValue())
                .body("types[%d]", withArgs(0), isEnabledTypeWith("BUGFIX", "bugfix/"))
                .body("types[%d]", withArgs(1), isEnabledTypeWith("FEATURE", "feature/"))
                .body("types[%d]", withArgs(2), isEnabledTypeWith("HOTFIX", "hotfix/"))
                .body("types[%d]", withArgs(3), isEnabledTypeWith("RELEASE", "release/"))
                .put(getBranchModelConfigurationPath(REPOSITORY_WITH_DATA));

    }

    @Test
    public void testPutToExistingConfigurationUseDefault() {
        // create model with non-default branch types
        requestAsAdmin()
                .body(createBranchModelJson(
                        createBranchConfiguration("refs/heads/master", false),
                        null,
                        createBranchTypeConfiguration("BUGFIX", "not-bugfix/", false),
                        createBranchTypeConfiguration("FEATURE", "not-feature/", false))
                ).expect().statusCode(CREATED.getStatusCode())
                .log().ifStatusCodeMatches(not(CREATED.getStatusCode()))
                .put(getBranchModelConfigurationPath(REPOSITORY_WITH_DATA));

        // now reset to default and make sure it happened
        requestAsAdmin()
                .body(createBranchModelUseDefaultJson())
                .expect().statusCode(OK.getStatusCode())
                .log().ifStatusCodeMatches(not(OK.getStatusCode()))
                .body("development.refId", nullValue())
                .body("development.useDefault", is(true))
                .body("production", Matchers.nullValue())
                .body("types[%d]", withArgs(0), isEnabledTypeWith("BUGFIX", "bugfix/"))
                .body("types[%d]", withArgs(1), isEnabledTypeWith("FEATURE", "feature/"))
                .body("types[%d]", withArgs(2), isEnabledTypeWith("HOTFIX", "hotfix/"))
                .body("types[%d]", withArgs(3), isEnabledTypeWith("RELEASE", "release/"))
                .put(getBranchModelConfigurationPath(REPOSITORY_WITH_DATA));
    }

    @Test
    public void testPutConfigurationWithNoAdminPermission() {
        requestAs(USER_WITH_WRITE_ACCESS)
                .expect().statusCode(UNAUTHORIZED.getStatusCode())
                .log().ifStatusCodeMatches(not(UNAUTHORIZED.getStatusCode()))
                .put(getBranchModelConfigurationPath(REPOSITORY_WITH_DATA));
    }

    @Test
    public void testPutConfigurationOnRepositoryWithExistingModel() {
        createDefaultBranchModel(REPOSITORY_WITH_DATA);

        requestAsAdmin()
                .expect().statusCode(OK.getStatusCode())
                .log().ifError()
                .put(getBranchModelConfigurationPath(REPOSITORY_WITH_DATA));
    }

    @Test
    public void testPutConfigurationWithNonExistingRefId() {
        requestAsAdmin()
                .body(createBranchModelJson(
                        createBranchConfiguration("refs/heads/nope", false),
                        null)
                ).expect().statusCode(BAD_REQUEST.getStatusCode())
                .body("errors", containsError(isRestErrorWithMessageThat(
                        containsInOrder("refs/heads/nope", "does not point to an existing branch"))))
                .log().ifStatusCodeMatches(not(BAD_REQUEST.getStatusCode()))
                .put(getBranchModelConfigurationPath(REPOSITORY_WITH_DATA));
    }

    @Test
    public void testPutConfigurationWithWrongBranchTypeId() {
        requestAsAdmin()
                .body(createBranchModelJson(
                        createBranchConfiguration("refs/heads/master", false),
                        null,
                        createBranchTypeConfiguration("INVALID", "prefix/", true))
                ).expect().statusCode(BAD_REQUEST.getStatusCode())
                .body("errors", containsError(isRestErrorThat(
                        is("types.INVALID"), // context
                        containsInOrder("Invalid branch type configuration", "INVALID"), // message
                        null
                )))
                .log().ifStatusCodeMatches(not(BAD_REQUEST.getStatusCode()))
                .put(getBranchModelConfigurationPath(REPOSITORY_WITH_DATA));
    }


    @Test
    public void testPutConfigurationWithEmptyBranchTypePrefix() {
        requestAsAdmin()
                .body(createBranchModelJson(
                        createBranchConfiguration("refs/heads/master", false),
                        null,
                        createBranchTypeConfiguration("BUGFIX", "", true))
                ).expect().statusCode(BAD_REQUEST.getStatusCode())
                .body("errors", containsError(isRestErrorWithMessageThat(
                        containsInOrder("Prefix", "must not be blank"))))
                .log().ifStatusCodeMatches(not(BAD_REQUEST.getStatusCode()))
                .put(getBranchModelConfigurationPath(REPOSITORY_WITH_DATA));
    }

    @Test
    @Ignore
    public void testPutConfigurationWithInvalidPrefix() {
        requestAsAdmin()
                .body(createBranchModelJson(
                        createBranchConfiguration("refs/heads/master", false),
                        null,
                        createBranchTypeConfiguration("BUGFIX", "cannot/start/.with/dot", true),
                        createBranchTypeConfiguration("FEATURE", "feature", true))
                ).expect().statusCode(BAD_REQUEST.getStatusCode())
                .body("errors", containsError(isRestErrorWithMessageThat(containsInOrder(
                        "cannot/start/.with/dot", "is not a valid git branch prefix"))))
                .log().ifStatusCodeMatches(not(BAD_REQUEST.getStatusCode()))
                .put(getBranchModelConfigurationPath(REPOSITORY_WITH_DATA));
    }

    @Test
    public void testPutConfigurationPrefixTooLong() {
        requestAsAdmin()
                .body(createBranchModelJson(
                        createBranchConfiguration("refs/heads/master", false),
                        null,
                        createBranchTypeConfiguration("BUGFIX", "cannot/be/longer/than/30/characters", true),
                        createBranchTypeConfiguration("FEATURE", "feature", true))
                ).expect().statusCode(BAD_REQUEST.getStatusCode())
                .body("errors", containsError(isRestErrorWithMessageThat(containsInOrder(
                        "cannot/be/longer/than/30/characters", "cannot be longer than 30 characters"))))
                .log().ifStatusCodeMatches(not(BAD_REQUEST.getStatusCode()))
                .put(getBranchModelConfigurationPath(REPOSITORY_WITH_DATA));
    }

    @Test
    public void testPutConfigurationWithOverlappingPrefixes() {
        requestAsAdmin()
                .body(createBranchModelJson(
                        createBranchConfiguration("refs/heads/master", false),
                        null,
                        createBranchTypeConfiguration("BUGFIX", "prefix", true),
                        createBranchTypeConfiguration("FEATURE", "prefix2", true))
                ).expect().statusCode(BAD_REQUEST.getStatusCode())
                .body("errors", containsError(isRestErrorWithMessageThat(containsInOrder(
                        "Overlapping prefixes", "'prefix'", "'prefix2'"))))
                .log().ifStatusCodeMatches(not(BAD_REQUEST.getStatusCode()))
                .put(getBranchModelConfigurationPath(REPOSITORY_WITH_DATA));
    }

    @Test
    public void testCustomConfigurationIsPreservedAfterReEnabling() {
        createDefaultBranchModel(REPOSITORY_WITH_DATA);

        // update with custom values
        requestAsAdmin()
                .body(createBranchModelJson(
                        createBranchConfiguration("refs/heads/master", false),
                        createBranchConfiguration(null, true),
                        createBranchTypeConfiguration("BUGFIX", "a-bugfix", true),
                        createBranchTypeConfiguration("FEATURE", "a-feature", true),
                        createBranchTypeConfiguration("HOTFIX", "a-hotfix", false))
                ).expect().statusCode(OK.getStatusCode())
                .log().ifError()
                .put(getBranchModelConfigurationPath(REPOSITORY_WITH_DATA));

        // disable and re-enable the model
        deleteBranchModel(REPOSITORY_WITH_DATA);
        createDefaultBranchModel(REPOSITORY_WITH_DATA);

        // it should still contain the custom data
        requestAsAdmin()
                .expect().statusCode(OK.getStatusCode())
                .log().ifError()
                .body("development.refId", is("refs/heads/master"))
                .body("development.useDefault", is(false))
                .body("production.refId", isEmptyOrNullString())
                .body("production.useDefault", is(true))
                .body("types[%d]", withArgs(0), isEnabledTypeWith("BUGFIX", "a-bugfix"))
                .body("types[%d]", withArgs(1), isEnabledTypeWith("FEATURE", "a-feature"))
                .body("types[%d]", withArgs(2), isDisabledTypeWith("HOTFIX", "a-hotfix"))
                .body("types[%d]", withArgs(3), isDisabledEmptyType("RELEASE"))
                .get(getBranchModelConfigurationPath(REPOSITORY_WITH_DATA));
    }

    @Test
    public void testDeleteConfigurationWithNoAdminPermission() {
        createDefaultBranchModel(REPOSITORY_WITH_DATA);

        requestAs(USER_WITH_WRITE_ACCESS)
                .expect().statusCode(UNAUTHORIZED.getStatusCode())
                .log().ifStatusCodeMatches(not(UNAUTHORIZED.getStatusCode()))
                .delete(getBranchModelConfigurationPath(REPOSITORY_WITH_DATA));
    }

    @Test
    public void testBranchModelEnabledForNewRepositories() {
        testContext.repository(getProject1(), "newrepo");

        requestAsAdmin()
                .expect().statusCode(OK.getStatusCode())
                .log().ifError()
                .body("development.refId", nullValue())
                .body("development.useDefault", is(true))
                .body("production", nullValue())
                .body("types[%d]", withArgs(0), isEnabledTypeWith("BUGFIX", "bugfix/"))
                .body("types[%d]", withArgs(1), isEnabledTypeWith("FEATURE", "feature/"))
                .body("types[%d]", withArgs(2), isEnabledTypeWith("HOTFIX", "hotfix/"))
                .body("types[%d]", withArgs(3), isEnabledTypeWith("RELEASE", "release/"))
                .get(getBranchModelConfigurationPath("newrepo"));
    }


    private static RequestSpecification requestAsAdmin() {
        return requestAs(getAdminUser(), getAdminPassword());
    }

    private static RequestSpecification requestAs(String username) {
        return requestAs(username, username);
    }

    private static RequestSpecification requestAs(String username, String password) {
        return RestAssured.given().auth().preemptive().basic(username, password).contentType(ContentType.JSON);
    }

    private static void createDefaultBranchModel(String repositoryKey) {
        requestAsAdmin()
                .expect().statusCode(CREATED.getStatusCode())
                .log().ifError()
                .put(getBranchModelConfigurationPath(repositoryKey));
    }

    private static void deleteBranchModel(String repositoryKey) {
        requestAsAdmin()
                .expect().statusCode(NO_CONTENT.getStatusCode())
                .log().ifStatusCodeMatches(not(NO_CONTENT.getStatusCode()))
                .delete(getBranchModelConfigurationPath(repositoryKey));
    }


    private static Matcher<? super Map<? extends String, ?>> isTypeWith(String id, String prefix) {
        Matcher<Map<? extends String, ?>> withId = hasJsonEntry("id", id);
        if (prefix != null) {
            return allOf(withId, hasJsonEntry("prefix", prefix));
        } else {
            return withId;
        }
    }

    private static Matcher<?> isTypeWith(String id, String prefix, boolean enabled) {
        return allOf(isTypeWith(id, prefix), hasJsonEntry("enabled", enabled));
    }

    private static Matcher<?> isEnabledTypeWith(String id, String prefix) {
        return isTypeWith(id, prefix, true);
    }

    private static Matcher<?> isDisabledTypeWith(String id, String prefix) {
        return isTypeWith(id, prefix, false);
    }

    private static Matcher<?> isDisabledEmptyType(String id) {
        return isDisabledTypeWith(id, null);
    }

    private static String getBranchModelPath(String repoKey) {
        return getRepositoryRestURL("branch-utils", "latest", getProject1(), repoKey) + "/branchmodel";
    }

    private static String getBranchModelConfigurationPath(String repoKey) {
        return getBranchModelPath(repoKey) + "/configuration";
    }
}
