package com.atlassian.bitbucket.test;

import com.google.common.collect.Lists;
import com.jayway.restassured.RestAssured;
import com.jayway.restassured.response.Response;
import net.sf.json.JSONObject;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.junit.rules.ExternalResource;

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

import static org.hamcrest.Matchers.any;
import static org.hamcrest.Matchers.equalTo;

public class RefRestrictionTestHelper extends ExternalResource {

    private final String project;
    private final String repo;

    /**
     * List of restricted ref ids, used to clean up afterwards.
     */
    private List<Integer> createdRestrictedRefs = Lists.newArrayList();

    public RefRestrictionTestHelper(String project, String repo) {
        this.project = project;
        this.repo = repo;
    }

    @Override
    protected void after() {
        for (Integer id : createdRestrictedRefs) {
            deleteRestrictedRef(id, null);
        }
    }

    public void deleteAll() {
        for (JSONObject ref : getAll().getValues()) {
            deleteRestrictedRef(ref.getInt("id"), null);
        }
    }

    public ResponsePage<JSONObject> getAll() {
        return new ResponsePage<>(RestAssured.given()
                .auth().preemptive().basic(DefaultFuncTestData.getAdminUser(), DefaultFuncTestData.getAdminPassword())
                .contentType(MediaType.APPLICATION_JSON)
                .when().get(baseRestrictedRefUrl(project, repo)).body().jsonPath(), RestTestHelper.JSON_TRANSFORM);
    }

    /**
     * @param restrictedRefId the RestrictedRef to delete
     * @param expectedResponseCode the expected response code (null indicates any code is acceptable)
     */
    public void deleteRestrictedRef(int restrictedRefId, Integer expectedResponseCode) {
        deleteRestrictedRef(restrictedRefId, expectedResponseCode, DefaultFuncTestData.getAdminUser(),
                DefaultFuncTestData.getAdminPassword());
    }

    public void deleteRestrictedRef(int restrictedRefId, Integer expectedResponseCode, String username, String password) {
        Matcher<Integer> httpCodeMatcher = expectedResponseCode != null ? equalTo(expectedResponseCode) : any(Integer.class);
        // delete the ref
        RestAssured.expect().statusCode(httpCodeMatcher)
                .given().auth().preemptive().basic(username, password)
                .contentType(MediaType.APPLICATION_JSON)
                .when().delete(baseRestrictedRefUrl(project, repo) + "/" + restrictedRefId);
    }

    public Integer createRestrictedRef(RefMatcherType refMatcherType, String matcherId, RestrictionType restrictionType) {
        return createRestrictedRef(refMatcherType, matcherId, restrictionType, null, null, DefaultFuncTestData.getAdminUser(),
                DefaultFuncTestData.getAdminPassword());
    }

    public Integer createRestrictedRef(RefMatcherType refMatcherType, String matcherId, RestrictionType restrictionType, Collection<String> users, Collection<String> groups) {
        return createRestrictedRef(refMatcherType, matcherId, restrictionType, users, groups, DefaultFuncTestData.getAdminUser(),
                DefaultFuncTestData.getAdminPassword());
    }

    public Integer createRestrictedRef(RefMatcherType refMatcherType, String matcherId, RestrictionType restrictionType, Collection<String> users, Collection<String> groups,
                                       String username, String password) {
        IdMatcher idMatcher = new IdMatcher();

        // create the ref using the supplied parameters
        RestAssured.expect().statusCode(200)
                .body("id", idMatcher)
                .given().auth().preemptive().basic(username, password)
                .body(restrictedRefRequestBody(null, refMatcherType, matcherId, users, groups, restrictionType).toString())
                .contentType(MediaType.APPLICATION_JSON)
                .when().post(baseRestrictedRefUrl(project, repo));

        createdRestrictedRefs.add(idMatcher.getId());

        // get the created ref and check its values
        assertRestrictedRefValues(idMatcher.getId(), refMatcherType, matcherId);

        return idMatcher.getId();
    }

    public void updateRestrictedRef(int id, RefMatcherType refMatcherType, String value, Collection<String> users, Collection<String> groups) {
        updateRestrictedRef(id, refMatcherType, value, users, groups, DefaultFuncTestData.getAdminUser(),
                DefaultFuncTestData.getAdminPassword());
    }

    public void updateRestrictedRef(int id, RefMatcherType refMatcherType, String value, Collection<String> users, Collection<String> groups,
                String username, String password) {
        RestAssured.expect().statusCode(200)
                .body("id", equalTo(id))
                .given().auth().preemptive().basic(username, password)
                .body(restrictedRefRequestBody(id, refMatcherType, value, users, groups, RestrictionType.READ_ONLY).toString())
                .contentType(MediaType.APPLICATION_JSON)
                .when().post(baseRestrictedRefUrl(project, repo));

        createdRestrictedRefs.add(id);

        assertRestrictedRefValues(id, refMatcherType, value);
    }

    public void updateRestrictedRefExpectingUnauthorized(int id, RefMatcherType refMatcherType, String value, Collection<String> users, Collection<String> groups,
                                    String username, String password) {
        updateRestrictedRefExpectingCode(id, refMatcherType, value, users, groups, username, password, 401);
    }

    public void updateRestrictedRefExpectingCode(int id, RefMatcherType refMatcherType, String value, Collection<String> users, Collection<String> groups,
                                    String username, String password, int code) {
        RestAssured.expect().statusCode(code)
                .given()
                .auth().preemptive().basic(username, password)
                .body(restrictedRefRequestBody(id, refMatcherType, value, users, groups, RestrictionType.READ_ONLY).toString())
                .contentType(MediaType.APPLICATION_JSON)
                .when().post(baseRestrictedRefUrl(project, repo));
    }


    public Response createRestrictedRefExpectingBadRequest(RefMatcherType refMatcherType, String value, Collection<String> users, Collection<String> groups) {
        return createRestrictedRefExpectingHttpCode(refMatcherType, value, users, groups, DefaultFuncTestData.getAdminUser(),
                DefaultFuncTestData.getAdminPassword(), 400);
    }

    public Response createRestrictedRefExpectingUnauthorized(RefMatcherType refMatcherType, String value, Collection<String> users,
                                                         Collection<String> groups, String username, String password) {
        return createRestrictedRefExpectingHttpCode(refMatcherType, value, users, groups, username, password, 401);
    }

    public Response createRestrictedRefExpectingHttpCode(RefMatcherType refMatcherType, String value, Collection<String> users,
                                                     Collection<String> groups, String username, String password,
                                                     int httpCode) {
        return RestAssured.expect().statusCode(httpCode)
                .given().auth().preemptive().basic(username, password)
                .body(restrictedRefRequestBody(null, refMatcherType, value, users, groups, RestrictionType.READ_ONLY).toString())
                .contentType(MediaType.APPLICATION_JSON).
                when().post(baseRestrictedRefUrl(project, repo));
    }

    private void assertRestrictedRefValues(Integer id, RefMatcherType refMatcherType, String value) {
        RestAssured.expect().statusCode(200)
                .body("id", equalTo(id))
                .body("matcher.type.id", equalTo(refMatcherType.name()))
                .body("matcher.id", equalTo(value))
                .given().auth().preemptive().basic(DefaultFuncTestData.getAdminUser(), DefaultFuncTestData.getAdminPassword())
                .contentType(MediaType.APPLICATION_JSON)
                .when().get(baseRestrictedRefUrl(project, repo) + "/" + id);
    }

    private static String baseRestrictedRefUrl(String projectKey, String repositorySlug) {
        return DefaultFuncTestData.getRestURL("branch-permissions", "2.0") +
                "/projects/" + projectKey +
                "/repos/" + repositorySlug +
                "/restrictions";
    }

    private static JSONObject restrictedRefRequestBody(Integer id, RefMatcherType type, String value, Collection<String> users,
                                                       Collection<String> groups, RestrictionType restrictionType) {
        JSONObject obj = new JSONObject();
        obj.put("id", id);
        JSONObject matcher = new JSONObject();
        JSONObject refMatcherType = new JSONObject();
        refMatcherType.put("id", type == null ? null : type.name());
        matcher.put("type", refMatcherType);
        matcher.put("id", value);
        obj.put("matcher", matcher);
        if (users != null) {
            obj.put("users", users);
        }
        if (groups != null) {
            obj.put("groups", groups);
        }
        obj.put("type", restrictionType.getId());

        return obj;
    }

    public enum RefMatcherType {
        BRANCH,
        PATTERN,
        MODEL_CATEGORY,
        MODEL_BRANCH
    }

    public enum RestrictionType {
        READ_ONLY("read-only", true),
        NO_DELETES("no-deletes", false),
        FAST_FORWARD_ONLY("fast-forward-only", false),
        PULL_REQUEST_ONLY("pull-request-only", false);
        private final String id;
        private final boolean supportsPermissions;

        private RestrictionType(String id, boolean supportsPermissions) {
            this.id = id;
            this.supportsPermissions = supportsPermissions;
        }

        public String getId() {
            return id;
        }

        public boolean supportsPermissions() {
            return supportsPermissions;
        }
    }

    private static class IdMatcher extends BaseMatcher<Object> {

        private Integer id;

        public Integer getId() {
            return id;
        }

        @Override
        public boolean matches(Object o) {
            if (o instanceof Number) {
                id = ((Number) o).intValue();
                return true;
            }
            return false;
        }

        @Override
        public void describeTo(Description description) {
            any(Number.class).describeTo(description);
        }
    }
}
