package com.atlassian.bitbucket.test;

import com.atlassian.bitbucket.content.DiffSegmentType;
import com.atlassian.bitbucket.pull.PullRequestEntityType;
import com.atlassian.bitbucket.pull.PullRequestParticipantStatus;
import com.atlassian.bitbucket.pull.PullRequestState;
import com.atlassian.bitbucket.rest.pull.RestPullRequestParticipant;
import com.atlassian.bitbucket.test.rest.AbstractRestHelper;
import com.atlassian.bitbucket.test.rest.RestAuthentication;
import com.atlassian.bitbucket.test.rest.comment.RestCommentUtils;
import com.google.common.base.Functions;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.jayway.restassured.RestAssured;
import com.jayway.restassured.builder.ResponseSpecBuilder;
import com.jayway.restassured.path.json.JsonPath;
import com.jayway.restassured.response.Response;
import net.sf.json.JSONObject;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.mutable.MutableLong;

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

import static com.google.common.collect.Collections2.transform;
import static javax.ws.rs.core.Response.Status.CREATED;
import static javax.ws.rs.core.Response.Status.OK;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;

@SuppressWarnings("unused")
public class PullRequestTestHelper extends AbstractRestHelper {

    private final String projectKey;
    private final String repoSlug;
    private final long id;

    private PullRequestTestHelper(String username, String password, String projectKey, String repoSlug, long id) {
        super(RestAuthentication.authenticated(username, password));

        this.projectKey = projectKey;
        this.repoSlug = repoSlug;
        this.id = id;
    }

    public long getId() {
        return id;
    }

    public String getProjectKey() {
        return projectKey;
    }

    public String getRepoSlug() {
        return repoSlug;
    }

    public JsonPath get() {
        return request()
                .expect().statusCode(OK.getStatusCode())
                .when().get(getUrl())
                .body().jsonPath();
    }

    public String getUrl() {
        return getUrl(projectKey, repoSlug, id);
    }

    public String getWatchUrl() {
        return getUrl() + "/watch";
    }

    public String getParticipantsUrl() {
        return getUrl() + "/participants";
    }

    public String getDeleteParticipantsUrl(String username) {
        return getParticipantsUrl() + "?username=" + username;
    }

    public String getUpdatePullRequestStatusUrl(String userSlug) {
        return getParticipantsUrl() + "/" + userSlug;
    }

    public String getDiffUrl(String path, String srcPath) {
        return getUrl() + "/diff"
                + (StringUtils.isEmpty(path) ? "" : ("/" + path))
                + (StringUtils.isEmpty(srcPath) ? "" : ("?srcPath=" + srcPath));
    }

    public String getDiffUrlWithMarkup(String path, String srcPath) {
        return getDiffUrl(path, srcPath) + (srcPath == null ? "?" : "&") + "markup=true";
    }

    public String getCommentsUrl() {
        return getUrl() + "/comments";
    }

    public String getCommitCommentUrl(String commitId, String parentId) {
        return getCommitCommentUrl(projectKey, repoSlug, commitId, parentId);
    }

    public String getCommentsUrlWithMarkup() {
        return getCommentsUrl() + "?markup=true";
    }

    public String getCommentUrl(long id, int version) {
        return getCommentsUrl() + "/" + id + "?version=" + version;
    }

    public String getCommentUrlWithMarkup(long id, int version) {
        return getCommentUrl(id, version) + "&markup=true";
    }

    public String getCommitsUrl(boolean withCounts) {
        return getUrl() + "/commits?withCounts=" + withCounts;
    }

    /**
     * @return a URL which can be {@code POST}ed to to decline this pull request
     */
    public String getDeclineUrl() {
        return getDeclineUrl(projectKey, repoSlug, id);
    }

    /**
     * @return a URL which can be {@code POST}ed to to merge this pull request
     */
    public String getMergeUrl() {
        return getMergeUrl(projectKey, repoSlug, id);
    }

    /**
     * @return a URL which can be {@code POST}ed to to reopen this pull request
     */
    public String getReopenUrl() {
        return getReopenUrl(projectKey, repoSlug, id);
    }

    public String getActivitiesUrl() {
        return getActivitiesUrl(projectKey, repoSlug, id);
    }

    public String getActivitiesUrlStartingAt(PullRequestEntityType fromType, long fromId, int limit) {
        return getActivitiesUrlStartingAt(projectKey, repoSlug, id, fromType, fromId, limit);
    }

    public int getVersion() {
        return get().getInt("version");
    }

    public boolean isOpen() {
        return get().getString("state").equals("OPEN");
    }

    public Set<String> getAllParticipants() {
        Response response = request().expect().statusCode(OK.getStatusCode())
                .when().get(getParticipantsUrl());

        return Sets.newHashSet(Lists.transform(response.body().jsonPath().getList("values.user.name"), Functions.toStringFunction()));
    }

    private void decline(String username, String password, int version) {
        declinePullRequest(username, password, projectKey, repoSlug, id, version);
    }

    /**
     * Declines the request <i>if</i> it is open, <i>with</i> the current version number.
     */
    public void declineSafe() {
        if (exists() && isOpen()) {
            decline(DefaultFuncTestData.getAdminUser(), DefaultFuncTestData.getAdminPassword(), getVersion());
        }
    }

    public boolean exists() {
        return RestAssured
                .given()
                .auth().preemptive().basic(defaultAuthentication.getUsername(), defaultAuthentication.getPassword())
                .when()
                .get(getUrl()).getStatusCode() == 200;
    }

    public void mergeSafe() {
        if (exists() && isOpen()) {
            mergePullRequest(DefaultFuncTestData.getAdminUser(), DefaultFuncTestData.getAdminPassword(), projectKey, repoSlug, id, getVersion());
        }
    }

    private void update(JSONObject body) {
        request()
                .body(body.toString())
                .when().put(getUrl());
    }

    public void addReviewers(String... usernames) {
        HashMap<String, Object> body = get().get("");
        @SuppressWarnings("unchecked")
        List<JSONObject> reviewers = (List) body.get("reviewers");

        reviewers.addAll(Lists.newArrayList(transform(Arrays.asList(usernames), username -> {
            JSONObject userObject = new JSONObject();
            userObject.put("name", username);
            JSONObject wrapperObject = new JSONObject();
            wrapperObject.put("user", userObject);
            return wrapperObject;
        })));
        body.remove("author");
        body.remove("participants");
        update(JSONObject.fromObject(body));
    }

    public void setDescription(String description) {
        HashMap<String, Object> body = get().get("");
        body.remove("author");
        body.remove("participants");
        body.put("description", description);
        update(JSONObject.fromObject(body));
    }

    public void setStatusFor(String username, PullRequestParticipantStatus status) {
        RestAssured.expect()
                .statusCode(OK.getStatusCode())
                .body(RestPullRequestParticipant.USER, notNullValue())
                .body(RestPullRequestParticipant.USER + ".name", equalTo(username))
                .body("status", equalTo(status.name()))
                .log().ifError()
                .given()
                .auth().preemptive().basic(username, username)
                .contentType(MediaType.APPLICATION_JSON)
                .body(toJsonObject("status", status))
                .put(getUpdatePullRequestStatusUrl(username));
    }

    public Response getMergeStatus(int version) {
        return request()
                .expect().statusCode(OK.getStatusCode())
                .when().get(getMergeUrl(projectKey, repoSlug, id));
    }

    public List<String> getMergeVetoes(int version) {
        JsonPath json = getMergeStatus(version).jsonPath();

        Boolean canMerge = json.getBoolean("canMerge");
        if (Boolean.TRUE.equals(canMerge)) {
            return Collections.emptyList();
        } else {
            List<Map<String, String>> vetoes = json.getList("vetoes");
            return Lists.transform(vetoes, veto -> String.valueOf(veto.get("detailedMessage")));
        }
    }

    public Response addGeneralComment(String commentText) {
        return request()
                .body(RestCommentUtils.createBody(commentText))
                .expect()
                .statusCode(CREATED.getStatusCode())
                .body("text", equalTo(commentText))
                .log().ifError()
                .post(getCommentsUrl());
    }

    public Response addCommentReply(String commentText, long id) {
        return request()
                .body(RestCommentUtils.createReplyBody(commentText, id))
                .expect()
                .statusCode(CREATED.getStatusCode())
                .body("text", equalTo(commentText))
                .log().ifError()
                .post(getCommentsUrl());

    }

    public Response addCommitComment(String commitId, String commentText, String path, int line,
                                     DiffSegmentType lineType) {
        return addCommitComment(commitId, null, commentText, path, line, lineType);
    }

    public Response addCommitComment(String commitId, String parentId, String commentText, String path, int line,
                                     DiffSegmentType lineType) {
        JSONObject body = RestCommentUtils.createLineBody(commentText, path, null, line, lineType);

        JSONObject pullRequest = new JSONObject();
        pullRequest.put("id", getId());
        body.getJSONObject("anchor")
                .put("pullRequest", pullRequest);

        return request()
                .body(body)
                .expect()
                .statusCode(CREATED.getStatusCode())
                .body("text", equalTo(commentText))
                .log().ifError()
                .post(getCommitCommentUrl(commitId, parentId));
    }

    public Response addCommitFileComment(String commitId, String commentText, String path) {
        return addCommitFileComment(commitId, null, commentText, path);
    }

    public Response addCommitFileComment(String commitId, String parentId, String commentText, String path) {
        JSONObject body = RestCommentUtils.createFileBody(commentText, path, path);

        JSONObject pullRequest = new JSONObject();
        pullRequest.put("id", getId());
        body.getJSONObject("anchor")
                .put("pullRequest", pullRequest);

        return request()
                .body(body)
                .expect()
                .statusCode(CREATED.getStatusCode())
                .body("text", equalTo(commentText))
                .log().ifError()
                .post(getCommitCommentUrl(commitId, parentId));
    }

    public Response addDiffComment(String commentText, String path, int line, DiffSegmentType lineType) {
        return request()
                .body(RestCommentUtils.createLineBody(commentText, path, path, line, lineType))
                .expect()
                .statusCode(CREATED.getStatusCode())
                .body("text", equalTo(commentText))
                .log().ifError()
                .post(getCommentsUrl());
    }

    public Response addFileComment(String commentText, String path) {
        return request()
                .body(RestCommentUtils.createFileBody(commentText, path, path))
                .expect()
                .statusCode(CREATED.getStatusCode())
                .body("text", equalTo(commentText))
                .log().ifError()
                .post(getCommentsUrl());
    }

    @Override
    public String toString() {
        return "{projectKey: " + projectKey + ", repoSlug:" + repoSlug + ", id: " + id + "}";
    }

    public static String getUrl(String projectKey, String repoSlug) {
        return DefaultFuncTestData.getRestURL() + "/projects/" + projectKey + "/repos/" + repoSlug + "/pull-requests";
    }

    public static String getUrl(String projectKey, String repoSlug, Map<String, String> queryParameters) {
        StringBuilder builder = new StringBuilder();
        for (Map.Entry<String, String> parameter : queryParameters.entrySet()) {
            // No need to URL encode these. RestAssured will take care of it.
            builder.append(builder.length() == 0 ? "?" : "&")
                    .append(String.format("%s=%s", parameter.getKey(), parameter.getValue()));
        }

        return getUrl(projectKey, repoSlug) + builder.toString();
    }

    public static String getUrl(String projectKey, String repoSlug, long id) {
        return getUrl(projectKey, repoSlug) + "/" + id;
    }

    public static String getCommitsUrl(String projectKey, String repoSlug, long id, boolean withCounts) {
        return getUrl(projectKey, repoSlug, id) + "/commits?withCounts=" + withCounts;
    }

    public static String getActivitiesUrl(String projectKey, String repoSlug, long id) {
        return getUrl(projectKey, repoSlug, id) + "/activities";
    }

    public static String getActivitiesUrlStartingAt(String projectKey, String repoSlug, long id, PullRequestEntityType fromType, long fromId, int limit) {
        return getUrl(projectKey, repoSlug, id) + "/activities?fromType=" + fromType + "&fromId=" + fromId + "&limit=" + limit;
    }

    public static String getCommitCommentUrl(String projectKey, String repoSlug, String commitId, String parentId) {
        return DefaultFuncTestData.getRestURL() + "/projects/" + projectKey + "/repos/" + repoSlug
                + "/commits/" + commitId + (parentId == null ? "/comments" : "/comments?since=" + parentId);
    }

    /**
     * @param projectKey the key for the project containing the pull request's target repository
     * @param repoSlug   the slug for the pull request's target repository
     * @param id         the pull request's ID within its target repository
     * @return a URL which can be {@code POST}ed to to decline the specified pull request
     */
    public static String getDeclineUrl(String projectKey, String repoSlug, long id) {
        return getUrl(projectKey, repoSlug, id) + "/decline";
    }

    /**
     * @param projectKey the key for the project containing the pull request's target repository
     * @param repoSlug   the slug for the pull request's target repository
     * @param id         the pull request's ID within its target repository
     * @return a URL which can be {@code POST}ed to to merge the specified pull request
     */
    public static String getMergeUrl(String projectKey, String repoSlug, long id) {
        return getUrl(projectKey, repoSlug, id) + "/merge";
    }

    /**
     * @param projectKey the key for the project containing the pull request's target repository
     * @param repoSlug   the slug for the pull request's target repository
     * @param id         the pull request's ID within its target repository
     * @return a URL which can be {@code POST}ed to to reopen the specified pull request
     */
    public static String getReopenUrl(String projectKey, String repoSlug, long id) {
        return getUrl(projectKey, repoSlug, id) + "/reopen";
    }

    public static Response declinePullRequest(String username, String password, String projectKey, String repoSlug, long id, int version) {
        return RestAssured.expect().statusCode(OK.getStatusCode())
                .log().ifError()
                .body("id", equalTo((int) id))
                .body("version", equalTo(version + 1))
                .body("state", equalTo(PullRequestState.DECLINED.name()))
                .given()
                .auth().preemptive().basic(username, password)
                .body(toJsonObject("version", version))
                .contentType(MediaType.APPLICATION_JSON)
                .when()
                .post(getDeclineUrl(projectKey, repoSlug, id));
    }

    public static Response reopenPullRequest(String username, String password, String projectKey, String repoSlug, long id, int version) {
        return RestAssured.expect().statusCode(OK.getStatusCode())
                .body("id", equalTo((int) id))
                .body("version", equalTo(version + 1))
                .body("state", equalTo(PullRequestState.OPEN.name()))
                .given()
                .auth().preemptive().basic(username, password)
                .body(toJsonObject("version", version))
                .contentType(MediaType.APPLICATION_JSON)
                .when()
                .post(getReopenUrl(projectKey, repoSlug, id));
    }

    public static Response mergePullRequest(String username, String password, String projectKey, String repoSlug, long id, int version) {
        return RestAssured.expect().statusCode(OK.getStatusCode())
                .body("id", equalTo((int) id))
                .body("version", equalTo(version + 2)) // 1 for locking the PR, 1 for unlocking and changing state
                .body("state", equalTo(PullRequestState.MERGED.name()))
                .given()
                .auth().preemptive().basic(username, password)
                .body(toJsonObject("version", version))
                .contentType(MediaType.APPLICATION_JSON)
                .when()
                .post(getMergeUrl(projectKey, repoSlug, id));
    }

    public static Response getMergeStatus(String username, String password, String projectKey, String repoSlug, long id) {
        return RestAssured.expect().statusCode(OK.getStatusCode())
                .given()
                .auth().preemptive().basic(username, password)
                .contentType(MediaType.APPLICATION_JSON)
                .when()
                .get(getMergeUrl(projectKey, repoSlug, id));
    }

    public static Response createPullRequest(
            MutableLong id,
            String username, String password,
            String title, String description,
            String fromProjectKey, String fromRepoSlug, String fromBranchId,
            String toProjectKey, String toRepoSlug, String toBranchId,
            String... reviewers) {

        return PullRequestRestTestHelper.createPullRequest(id, username, password, title, description,
                fromProjectKey, fromRepoSlug, fromBranchId,
                toProjectKey, toRepoSlug, toBranchId,
                new ResponseSpecBuilder().expectStatusCode(CREATED.getStatusCode()).build(),
                reviewers);
    }

    public static PullRequestTestHelper get(String username, String password, String projectKey, String repoSlug, long id) {
        return new PullRequestTestHelper(username, password, projectKey, repoSlug, id);
    }

    public static JSONObject toJsonObject(String key, Object value) {
        JSONObject json = new JSONObject();
        json.put(key, value);

        return json;
    }

    public static class Builder {

        private String username;
        private String password;
        private String title;
        private String description;
        private String sourceProject;
        private String sourceRepo;
        private String sourceBranch;
        private String targetProject;
        private String targetRepo;
        private String targetBranch;
        private Set<String> reviewers = Sets.newHashSet();

        public Builder(String username, String password, String title, String description,
                       String sourceProject, String sourceRepo, String sourceBranch,
                       String targetProject, String targetRepo, String targetBranch) {
            this.username = username;
            this.password = password;
            this.title = title;
            this.description = description;
            this.sourceProject = sourceProject;
            this.sourceRepo = sourceRepo;
            this.sourceBranch = sourceBranch;
            this.targetProject = targetProject;
            this.targetRepo = targetRepo;
            this.targetBranch = targetBranch;
        }

        // This is default level permissions because it should ONLY be called by the TestContext (so it is cleaned up properly)
        PullRequestTestHelper create() {
            MutableLong id = new MutableLong();
            createPullRequest(id, username, password, title, description, sourceProject, sourceRepo, sourceBranch,
                    targetProject, targetRepo, targetBranch, reviewers.toArray(new String[reviewers.size()]));
            return new PullRequestTestHelper(username, password, targetProject, targetRepo, id.longValue());
        }

        public Builder title(String title) {
            this.title = title;
            return this;
        }

        public Builder description(String description) {
            this.description = description;
            return this;
        }

        public Builder reviewers(String... usernames) {
            reviewers.addAll(Arrays.asList(usernames));
            return this;
        }

        public Builder sourceBranch(String sourceBranch) {
            this.sourceBranch = sourceBranch;
            return this;
        }

        public Builder targetBranch(String targetBranch) {
            this.targetBranch = targetBranch;
            return this;
        }

        public String getTargetProject() {
            return targetProject;
        }

        public String getTargetRepo() {
            return targetRepo;
        }
    }
}
