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

import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.rest.RestErrorMessage;
import com.atlassian.bitbucket.rest.RestErrors;
import com.atlassian.bitbucket.test.*;
import com.atlassian.bitbucket.test.RefRestrictionTestHelper.RestrictionType;
import com.atlassian.httpclient.api.HttpStatus;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.jayway.restassured.RestAssured;
import com.jayway.restassured.response.Response;
import com.jayway.restassured.specification.RequestSpecification;
import it.com.atlassian.stash.BaseRestTest;
import net.sf.json.JSONObject;
import org.apache.commons.lang3.StringUtils;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.rules.TestRule;

import javax.ws.rs.core.MediaType;
import java.util.List;

import static com.atlassian.bitbucket.test.DefaultFuncTestData.*;
import static com.atlassian.bitbucket.test.RefRestrictionTestHelper.RefMatcherType.BRANCH;
import static com.atlassian.bitbucket.test.RefRestrictionTestHelper.RefMatcherType.PATTERN;
import static com.atlassian.bitbucket.test.TagTestHelper.getGitTagBody;
import static com.atlassian.bitbucket.test.TestRuleOrderingSyntax.outerRules;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;

/**
 * This tests tests the v2 Branch Permissions API
 */
public class RefRestrictionResourceTest extends BaseRestTest {

    private static final String TAG_NAME = "some-tag-name";
    // Intentionally not annotated with @Rule as it is referred in the RuleChain below
    private RefRestrictionTestHelper restrictedRefs = new RefRestrictionTestHelper(
            getProject1(), getProject1Repository1());

    @Override
    protected List<TestRule> getRetryRuleChain() {
        return outerRules(super.getRetryRuleChain()).
               around(restrictedRefs);
    }

    @Test
    public void testCreateTagCanBeRestricted() {
        restrictedRefs.createRestrictedRef(PATTERN, "refs/tags/**", RestrictionType.READ_ONLY, ImmutableList.of(), null);

        RestAssured
                .given()
                    .auth().preemptive().basic(DefaultFuncTestData.getAdminUser(), DefaultFuncTestData.getAdminPassword())
                    .body(getGitTagBody(TAG_NAME, getProject1Repository1Commit1().id, "tag message"))
                    .contentType(MediaType.APPLICATION_JSON)
                .expect()
                    .statusCode(HttpStatus.BAD_REQUEST.code)
                    .body("errors[0].exceptionName", equalTo("com.atlassian.bitbucket.event.tag.TagCreationCanceledException"))
                .when()
                .post(tagsResourceUrl(getProject1(), getProject1Repository1()));
    }

    @Test
    public void testCreateGitTagCanBeRestricted() {
        restrictedRefs.createRestrictedRef(PATTERN, "refs/tags/**", RestrictionType.READ_ONLY, ImmutableList.of(), null);

        createGitTagRequest()
                .expect()
                    .statusCode(HttpStatus.BAD_REQUEST.code)
                    .body("errors[0].exceptionName", equalTo("com.atlassian.bitbucket.event.tag.TagCreationCanceledException"))
                .when()
                .post(gitTagsResourceUrl(getProject1(), getProject1Repository1()));
    }

    @Test
    public void testDeleteGitTagCanBeRestricted() {
        testContext.tag(getProject1(), getProject1Repository1(), TAG_NAME,
                getProject1Repository1Commit1().id, "message");

        restrictedRefs.createRestrictedRef(PATTERN, "refs/tags/**", RestrictionType.READ_ONLY, ImmutableList.of(), null);

        createGitTagRequest()
                .expect()
                    .statusCode(HttpStatus.BAD_REQUEST.code)
                    .body("errors[0].exceptionName", equalTo("com.atlassian.bitbucket.event.tag.TagDeletionCanceledException"))
                .when()
                .delete(gitTagsResourceUrl(getProject1(), getProject1Repository1()) + "/" + TAG_NAME);
    }

    @Test
    public void testRestrictRefByPattern() {
        restrictedRefs.createRestrictedRef(PATTERN, "**/globular/*", RestrictionType.READ_ONLY, ImmutableList.of("admin"), null);
    }

    @Test
    public void testRestrictRefByBranch() {
        restrictedRefs.createRestrictedRef(BRANCH, "refs/heads/master", RestrictionType.READ_ONLY, ImmutableList.of("admin"), null);
    }

    @Test
    public void testDeleteRestrictedRef() {
        int id = restrictedRefs.createRestrictedRef(BRANCH, "refs/heads/master", RestrictionType.READ_ONLY, ImmutableList.of("admin"), null);
        restrictedRefs.deleteRestrictedRef(id, 204);
    }

    @Test
    public void testUpdateRestrictedRef() {
        // create a restricted ref
        int id = restrictedRefs.createRestrictedRef(BRANCH, "refs/heads/master", RestrictionType.READ_ONLY, ImmutableList.of("admin"), null);
        // now update it
        restrictedRefs.updateRestrictedRef(id, BRANCH, "refs/heads/master", null, ImmutableList.of("stash-users"));
    }

    @Test
    public void testGetRestrictedRef() {
        restrictedRefs.createRestrictedRef(BRANCH, "refs/heads/master", RestrictionType.READ_ONLY, ImmutableList.of("admin"), null);

        ResponsePage<JSONObject> page = restrictedRefs.getAll();

        assertEquals(page.getSize(), 1);
        assertEquals(page.isLastPage(), true);

        List<JSONObject> restrictions = page.getValues();
        assertEquals(restrictions.size(), 1);
        assertValidRestrictedRef(restrictions.get(0), 1, 0);
    }

    @Test
    public void testDeleteNonExistentRestrictedRefOk() {
        restrictedRefs.deleteRestrictedRef(42, 204);
    }

    @Test
    public void testValidationErrors() {
        testContext.user("johnny-no-license");
        GroupTestHelper.removeUserFromGroup(false, "stash-users", "johnny-no-license");

        Response response = restrictedRefs.createRestrictedRefExpectingBadRequest(
                null, null, ImmutableList.of("mr_x", "johnny-no-license"), ImmutableList.of("superfriends"));
        List<RestErrorMessage> errors = response.getBody().as(RestErrors.class).getErrors();
        assertThat(errors,
                hasItems(
                        new RestErrorMessage("matcher.id", "Matcher ID is required"),
                        new RestErrorMessage("users", "mr_x is not a valid user"),
                        new RestErrorMessage("users", "johnny-no-license is not a licensed user"),
                        new RestErrorMessage("groups", "superfriends is not a valid group"))
        );
        assertEquals("Incorrect number of validation errors.", 4, errors.size());

        response = restrictedRefs.createRestrictedRefExpectingBadRequest(BRANCH, null, null, null);
        errors = response.getBody().as(RestErrors.class).getErrors();
        assertThat(errors, hasItem(new RestErrorMessage("matcher.id", "Matcher ID is required", null)));
        assertEquals("Incorrect number of validation errors.", 1, errors.size());

        response = restrictedRefs.createRestrictedRefExpectingBadRequest(PATTERN, null, null, null);
        errors = response.getBody().as(RestErrors.class).getErrors();
        assertThat(errors, hasItem(new RestErrorMessage("matcher.id", "Matcher ID is required", null)));
        assertEquals("Incorrect number of validation errors.", 1, errors.size());

        //This value comes from AoRefRestriction.MAX_SUBJECT_ID_LENGTH
        int maxLength = 255;
        response = restrictedRefs.createRestrictedRefExpectingBadRequest(
                PATTERN, StringUtils.repeat('a', maxLength + 1), null, ImmutableList.of("stash-users"));
        errors = response.getBody().as(RestErrors.class).getErrors();
        assertThat(errors, hasItems(
                new RestErrorMessage("matcher.id", "Matcher ID is too long. Matcher IDs are currently restricted to " +
                        maxLength + " characters, you have used " + (maxLength + 1), null)
        ));
        assertEquals("Incorrect number of validation errors.", 1, errors.size());
    }

    @Test @Ignore("This test takes 20-30 seconds, not worth the extra build time for the moment.")
    public void testExceedRestrictedRefLimit() {
        for (int i = 0; i < 100; i++) {
            // first 100 refs are ok
            restrictedRefs.createRestrictedRef(i % 2 == 0 ? BRANCH : PATTERN, "branch" + i, RestrictionType.READ_ONLY, null, null);
        }
        // 101st is not
        Response response = restrictedRefs.createRestrictedRefExpectingBadRequest(BRANCH, "straw", null, null);
        List<RestErrorMessage> errors = response.getBody().as(RestErrors.class).getErrors();
        assertThat(errors,
                hasItems(new RestErrorMessage(null, "The maximum number of branch permissions (100) have already been " +
                        "defined for this repository", "com.atlassian.bitbucket.license.LimitExceededException"))
        );
    }

    @Test @Ignore("This test takes ~20 seconds, not worth the extra build time for the moment.")
    public void testExceedPermittedEntityLimit() {
        List<String> users = Lists.newArrayList();
        List<String> groups = Lists.newArrayList();
        for (int i = 0; i < 50; i++) {
            String name = "entity" + i;
            if (i % 2 == 0) {
                testContext.user(name);
                users.add(name);
            } else {
                testContext.group(name);
                groups.add(name);
            }
        }
        // 50 is ok
        restrictedRefs.createRestrictedRef(BRANCH, "branchin", RestrictionType.READ_ONLY, users, groups);

        // 51 is not
        String name = "straw";
        testContext.user(name);
        users.add(name);
        Response response = restrictedRefs.createRestrictedRefExpectingBadRequest(BRANCH, "camels-back", users, groups);
        List<RestErrorMessage> errors = response.getBody().as(RestErrors.class).getErrors();
        assertThat(errors,
                hasItems(new RestErrorMessage(null, "You can not permit more than 50 users and/or groups for a restricted " +
                        "resource", "com.atlassian.bitbucket.license.LimitExceededException"))
        );
    }

    @Test
    public void testAdministeringBranchPermissionsRequiresRepoAdmin() {
        String repoAdmin = "repoAdmin";
        testContext.user(repoAdmin);
        testContext.repositoryPermission(getProject1(),
                getProject1Repository1(), repoAdmin, Permission.REPO_ADMIN);

        // project admins can create restricted refs
        int id = restrictedRefs.createRestrictedRef(BRANCH, "no-probalo", RestrictionType.READ_ONLY, ImmutableList.of("admin", repoAdmin),
                ImmutableList.of("stash-users"), repoAdmin, repoAdmin);
        // .. and update them
        restrictedRefs.updateRestrictedRef(id, BRANCH, "no-probalo", ImmutableList.of("admin", repoAdmin), ImmutableList.of("stash-users"), repoAdmin, repoAdmin);
        // .. and delete them
        restrictedRefs.deleteRestrictedRef(id, 204);

        String nonAdmin = "nonAdmin";
        testContext.user(nonAdmin);
        testContext.projectPermission(getProject1(), nonAdmin, Permission.PROJECT_WRITE);

        // non project admins can't create restricted refs
        restrictedRefs.createRestrictedRefExpectingUnauthorized(BRANCH, "problem", ImmutableList.of("admin", repoAdmin),
                ImmutableList.of("stash-users"), nonAdmin, nonAdmin);
        // .. or update them
        id = restrictedRefs.createRestrictedRef(BRANCH, "no-probalo", RestrictionType.READ_ONLY, ImmutableList.of("admin", repoAdmin),
                ImmutableList.of("stash-users"), repoAdmin, repoAdmin); // create using legit project admin
        restrictedRefs.updateRestrictedRefExpectingUnauthorized(id, BRANCH, "problem", ImmutableList.of("admin", repoAdmin),
                ImmutableList.of("stash-users"), nonAdmin, nonAdmin);
        // .. or delete them
        restrictedRefs.deleteRestrictedRef(id, 401, nonAdmin, nonAdmin);
    }

    private void assertValidRestrictedRef(JSONObject value, int users, int groups) {
        assertTrue(value.containsKey("id"));
        assertTrue(value.containsKey("type"));
        assertTrue(value.containsKey("matcher"));

        JSONObject matcher = value.getJSONObject("matcher");
        assertTrue(matcher.containsKey("id"));
        assertTrue(matcher.containsKey("displayId"));
        assertTrue(matcher.containsKey("type"));
        assertTrue(matcher.containsKey("active"));

        JSONObject type = matcher.getJSONObject("type");
        assertTrue(type.containsKey("id"));
        assertTrue(type.containsKey("name"));

        assertTrue(value.containsKey("users"));
        assertEquals(value.getJSONArray("users").size(), users);
        assertTrue(value.containsKey("groups"));
        assertEquals(value.getJSONArray("groups").size(), groups);

    }

    private RequestSpecification createGitTagRequest() {
        return RestAssured
                .given()
                .auth().preemptive().basic(DefaultFuncTestData.getAdminUser(), DefaultFuncTestData.getAdminPassword())
                .body(getGitTagBody(TAG_NAME, getProject1Repository1Commit1().id, "tag message"))
                .contentType(MediaType.APPLICATION_JSON);
    }

    private static String gitTagsResourceUrl(String project, String repo) {
        return getRepositoryRestURL("git", "latest", project, repo) + "/tags";
    }

    private static String tagsResourceUrl(String project, String repo) {
        return getRepositoryRestURL(project, repo) + "/tags";
    }
}
