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

import com.atlassian.bitbucket.content.DiffSegmentType;
import com.atlassian.bitbucket.content.DiffWhitespace;
import com.atlassian.bitbucket.test.PullRequestTestHelper;
import com.atlassian.bitbucket.test.TestContext;
import com.atlassian.bitbucket.test.rest.comment.RestCommentUtils;
import com.atlassian.bitbucket.permission.Permission;
import com.jayway.restassured.RestAssured;
import com.jayway.restassured.builder.ResponseSpecBuilder;
import com.jayway.restassured.specification.ResponseSpecification;
import org.apache.commons.lang3.mutable.MutableLong;
import org.hamcrest.text.pattern.PatternMatcher;
import org.hamcrest.text.pattern.Patterns;
import org.junit.BeforeClass;
import org.junit.Test;

import static javax.ws.rs.core.Response.Status.*;
import static org.hamcrest.Matchers.*;

public class PullRequestDiffResourceTest extends BasePullRequestResourceTest {

    private static final String PR_DIFF_TEST = "prdifftest";
    private static final String AUTHOR = PR_DIFF_TEST + "_prdifftest";

    @BeforeClass
    public static void setup() throws Exception {
        getClassTestContext().project(PR_DIFF_TEST)
                .repository(PR_DIFF_TEST, PR_DIFF_TEST, REPO_ZIP)
                .user(AUTHOR).projectPermission(PR_DIFF_TEST, AUTHOR, Permission.PROJECT_READ);
    }

    @Test
    public void testDiff() {
        openPullRequest(testContext, FQ_BASIC_BRANCHING_BRANCH, FQ_MASTER_BRANCH);

        RestAssured.expect()
                .spec(newDiffSpec())
                .body("diffs[0].hunks[0].segments[0].lines[0].commentIds", nullValue())
                .log().ifError()
                .given()
                .auth().preemptive().basic(AUTHOR, AUTHOR)
                .contentType("application/json")
                .get(pullRequest.getDiffUrl("Monthly_416.bak", null));
    }

    @Test
    public void testDiffAnonymously() {
        openPullRequest(testContext, FQ_BASIC_BRANCHING_BRANCH, FQ_MASTER_BRANCH);

        RestAssured.expect()
                .statusCode(UNAUTHORIZED.getStatusCode())
                .log().ifStatusCodeMatches(not(equalTo(UNAUTHORIZED.getStatusCode())))
                .given()
                .contentType("application/json")
                .get(pullRequest.getDiffUrl("Monthly_416.bak", null));
    }

    @Test
    public void testDiffForNonexistentPullRequest() {
        RestAssured.expect()
                .statusCode(NOT_FOUND.getStatusCode())
                .log().ifStatusCodeMatches(not(equalTo(NOT_FOUND.getStatusCode())))
                .given()
                .auth().preemptive().basic(AUTHOR, AUTHOR)
                .contentType("application/json")
                .get(PullRequestTestHelper.getUrl(PR_DIFF_TEST, PR_DIFF_TEST, Long.MAX_VALUE) + "/diff/Monthly_416.bak");
    }

    @Test
    public void testDiffSpecifyingContextLines() {
        openPullRequest(testContext, "refs/heads/branch_that_has_same_text_file_modified_on_both_clean_merge_src",
                "refs/heads/branch_that_has_same_text_file_modified_on_both_clean_merge_trgt");

        //To really be sure that specifying contextLines does something, we first need to run the diff without it
        RestAssured.expect()
                .statusCode(OK.getStatusCode())
                .body("fromHash", equalTo("7fa77c6e34c6c53de0bb880fc2b0b7924a601cd2")) //Target branch is always fixed
                .body("toHash", isSha1())                                              //Merge hash varies
                .body("contextLines", equalTo(10))
                .body("whitespace", equalTo(DiffWhitespace.SHOW.name()))
                .body("diffs.size()", equalTo(1))
                .body("diffs[0].source.toString", equalTo("file.txt"))
                .body("diffs[0].destination.toString", equalTo("file.txt"))
                .body("diffs[0].truncated", equalTo(false))
                .body("diffs[0].hunks.size()", equalTo(1))
                .body("diffs[0].hunks[0].sourceLine", equalTo(1))
                .body("diffs[0].hunks[0].sourceSpan", equalTo(3))
                .body("diffs[0].hunks[0].destinationLine", equalTo(1))
                .body("diffs[0].hunks[0].destinationSpan", equalTo(3))
                .body("diffs[0].hunks[0].truncated", equalTo(false))
                .body("diffs[0].hunks[0].segments.size()", equalTo(3))
                .body("diffs[0].hunks[0].segments[0].type", equalTo(DiffSegmentType.REMOVED.name()))
                .body("diffs[0].hunks[0].segments[0].truncated", equalTo(false))
                .body("diffs[0].hunks[0].segments[0].lines.size()", equalTo(1))
                .body("diffs[0].hunks[0].segments[0].lines[0].source", equalTo(1))
                .body("diffs[0].hunks[0].segments[0].lines[0].destination", equalTo(1))
                .body("diffs[0].hunks[0].segments[0].lines[0].line", equalTo("Line one"))
                .body("diffs[0].hunks[0].segments[0].lines[0].truncated", equalTo(false))
                .body("diffs[0].hunks[0].segments[1].type", equalTo(DiffSegmentType.ADDED.name()))
                .body("diffs[0].hunks[0].segments[1].truncated", equalTo(false))
                .body("diffs[0].hunks[0].segments[1].lines.size()", equalTo(1))
                .body("diffs[0].hunks[0].segments[1].lines[0].source", equalTo(2))
                .body("diffs[0].hunks[0].segments[1].lines[0].destination", equalTo(1))
                .body("diffs[0].hunks[0].segments[1].lines[0].line", equalTo("Line one modified"))
                .body("diffs[0].hunks[0].segments[1].lines[0].truncated", equalTo(false))
                .body("diffs[0].hunks[0].segments[2].type", equalTo(DiffSegmentType.CONTEXT.name()))
                .body("diffs[0].hunks[0].segments[2].truncated", equalTo(false))
                .body("diffs[0].hunks[0].segments[2].lines.size()", equalTo(2))
                .body("diffs[0].hunks[0].segments[2].lines[0].source", equalTo(2))
                .body("diffs[0].hunks[0].segments[2].lines[0].destination", equalTo(2))
                .body("diffs[0].hunks[0].segments[2].lines[0].line", equalTo("Line two"))
                .body("diffs[0].hunks[0].segments[2].lines[0].truncated", equalTo(false))
                .body("diffs[0].hunks[0].segments[2].lines[1].source", equalTo(3))
                .body("diffs[0].hunks[0].segments[2].lines[1].destination", equalTo(3))
                .body("diffs[0].hunks[0].segments[2].lines[1].line", equalTo("Line three modified"))
                .body("diffs[0].hunks[0].segments[2].lines[1].truncated", equalTo(false))
                .log().ifError()
                .given()
                .auth().preemptive().basic(AUTHOR, AUTHOR)
                .get(pullRequest.getDiffUrl("file.txt", null));

        //Once we've verified the shape of the diff with the default context, the next step is to verify that when
        //we specify contextLines on the URL it changes the output
        RestAssured.expect()
                .statusCode(OK.getStatusCode())
                .body("fromHash", equalTo("7fa77c6e34c6c53de0bb880fc2b0b7924a601cd2")) //Target branch is always fixed
                .body("toHash", isSha1())                                              //Merge hash varies
                .body("contextLines", equalTo(0))
                .body("whitespace", equalTo(DiffWhitespace.SHOW.name()))
                .body("diffs.size()", equalTo(1))
                .body("diffs[0].source.toString", equalTo("file.txt"))
                .body("diffs[0].destination.toString", equalTo("file.txt"))
                .body("diffs[0].truncated", equalTo(false))
                .body("diffs[0].hunks.size()", equalTo(1))
                .body("diffs[0].hunks[0].sourceLine", equalTo(1))
                .body("diffs[0].hunks[0].sourceSpan", equalTo(1))
                .body("diffs[0].hunks[0].destinationLine", equalTo(1))
                .body("diffs[0].hunks[0].destinationSpan", equalTo(1))
                .body("diffs[0].hunks[0].truncated", equalTo(false))
                .body("diffs[0].hunks[0].segments.size()", equalTo(2))
                .body("diffs[0].hunks[0].segments[0].type", equalTo(DiffSegmentType.REMOVED.name()))
                .body("diffs[0].hunks[0].segments[0].truncated", equalTo(false))
                .body("diffs[0].hunks[0].segments[0].lines.size()", equalTo(1))
                .body("diffs[0].hunks[0].segments[0].lines[0].source", equalTo(1))
                .body("diffs[0].hunks[0].segments[0].lines[0].destination", equalTo(1))
                .body("diffs[0].hunks[0].segments[0].lines[0].line", equalTo("Line one"))
                .body("diffs[0].hunks[0].segments[0].lines[0].truncated", equalTo(false))
                .body("diffs[0].hunks[0].segments[1].type", equalTo(DiffSegmentType.ADDED.name()))
                .body("diffs[0].hunks[0].segments[1].truncated", equalTo(false))
                .body("diffs[0].hunks[0].segments[1].lines.size()", equalTo(1))
                .body("diffs[0].hunks[0].segments[1].lines[0].source", equalTo(2))
                .body("diffs[0].hunks[0].segments[1].lines[0].destination", equalTo(1))
                .body("diffs[0].hunks[0].segments[1].lines[0].line", equalTo("Line one modified"))
                .body("diffs[0].hunks[0].segments[1].lines[0].truncated", equalTo(false))
                .log().ifError()
                .given()
                .auth().preemptive().basic(AUTHOR, AUTHOR)
                .get(pullRequest.getDiffUrl("file.txt", null) + "?contextLines=0");
    }

    @Test
    public void testDiffWithComments() {
        openPullRequest(testContext, FQ_BASIC_BRANCHING_BRANCH, FQ_MASTER_BRANCH);

        MutableLong commentId = new MutableLong();
        RestAssured.expect()
                .spec(RestCommentUtils.createSpecBuilderWithNoReplies(CREATED, commentId, "A comment", AUTHOR).build())
                .log().ifError()
                .given()
                .auth().preemptive().basic(AUTHOR, AUTHOR)
                .contentType("application/json")
                .body(RestCommentUtils.createLineBody("A comment", "Monthly_416.bak", null, 1, DiffSegmentType.ADDED))
                .post(pullRequest.getCommentsUrl());

        RestAssured.expect()
                .spec(newDiffSpec())
                .body("diffs[0].hunks[0].segments[0].lines[0].commentIds", notNullValue())
                .body("diffs[0].hunks[0].segments[0].lines[0].commentIds.size()", equalTo(1))
                .body("diffs[0].hunks[0].segments[0].lines[0].commentIds[0]", equalTo(commentId.intValue()))
                .body("diffs[0].lineComments", notNullValue())
                .body("diffs[0].lineComments.size()", equalTo(1))
                .body("diffs[0].lineComments[0].id", equalTo(commentId.intValue()))
                .body("diffs[0].lineComments[0].text", equalTo("A comment"))
                .body("diffs[0].lineComments[0].html", equalTo("<p>A comment</p>\n"))
                .log().ifError()
                .given()
                .auth().preemptive().basic(AUTHOR, AUTHOR)
                .contentType("application/json")
                .get(pullRequest.getDiffUrlWithMarkup("Monthly_416.bak", null));
    }

    //Adds a comment and then verifies that, if comments are not requested, they are not included in the diff
    @Test
    public void testDiffWithCommentsWithoutRequestingComments() {
        openPullRequest(testContext, FQ_BASIC_BRANCHING_BRANCH, FQ_MASTER_BRANCH);

        MutableLong commentId = new MutableLong();
        RestAssured.expect()
                .spec(RestCommentUtils.createSpecBuilderWithNoReplies(CREATED, commentId, "A comment", AUTHOR).build())
                .log().ifError()
                .given()
                .auth().preemptive().basic(AUTHOR, AUTHOR)
                .contentType("application/json")
                .body(RestCommentUtils.createLineBody("A comment", "Monthly_416.bak", null, 1, DiffSegmentType.ADDED))
                .post(pullRequest.getCommentsUrl());

        RestAssured.expect()
                .spec(newDiffSpec())
                .body("diffs[0].hunks[0].segments[0].lines[0].commentIds", nullValue())
                .body("diffs[0].fileComments", nullValue())
                .body("diffs[0].lineComments", nullValue())
                .log().ifError()
                .given()
                .auth().preemptive().basic(AUTHOR, AUTHOR)
                .contentType("application/json")
                .get(pullRequest.getDiffUrlWithMarkup("Monthly_416.bak", null) + "&withComments=false");
    }

    @Test
    public void testDiffWithNoPath() {
        openPullRequest(testContext, FQ_BASIC_BRANCHING_BRANCH, FQ_MASTER_BRANCH);

        RestAssured.expect()
                .statusCode(OK.getStatusCode())
                .body("diffs.size()", equalTo(1))
                .given()
                .auth().preemptive().basic(AUTHOR, AUTHOR)
                .contentType("application/json")
                .get(pullRequest.getDiffUrl(null, null));
    }

    @Test
    public void testDiffWithFilter() {
        openPullRequest(testContext, FQ_BASIC_BRANCHING_BRANCH, FQ_MASTER_BRANCH);

        RestAssured.expect()
                .statusCode(OK.getStatusCode())
                .body("diffs[0].hunks.size()", equalTo(1))
                .given()
                .auth().preemptive().basic(AUTHOR, AUTHOR)
                .contentType("application/json")
                .get(pullRequest.getDiffUrl(null, null) + "?filter=autem");

        RestAssured.expect()
                .statusCode(OK.getStatusCode())
                .body("diffs.size()", equalTo(0))
                .given()
                .auth().preemptive().basic(AUTHOR, AUTHOR)
                .contentType("application/json")
                .get(pullRequest.getDiffUrl(null, null) + "?filter=foo");
    }

    private static PatternMatcher isSha1() {
        return new PatternMatcher(Patterns.exactly(40, Patterns.anyCharacterIn("a-f0-9")));
    }

    private static ResponseSpecification newDiffSpec() {
        return new ResponseSpecBuilder().expectStatusCode(OK.getStatusCode())
                .expectBody("fromHash", equalTo("7b86ad1a05b4259b8fa54497a8be0bd359a405bd"))
                .expectBody("toHash", isSha1()) //The merge SHA1 varies
                .expectBody("contextLines", equalTo(10))
                .expectBody("whitespace", equalTo(DiffWhitespace.SHOW.name()))
                .expectBody("diffs", notNullValue())
                .expectBody("diffs.size()", equalTo(1))
                .expectBody("diffs[0].source", nullValue()) //present but set to null as opposed to absent
                .expectBody("diffs[0].destination", notNullValue())
                .expectBody("diffs[0].destination.components", notNullValue())
                .expectBody("diffs[0].destination.components.size()", equalTo(1))
                .expectBody("diffs[0].destination.components[0]", equalTo("Monthly_416.bak"))
                .expectBody("diffs[0].destination.name", equalTo("Monthly_416.bak"))
                .expectBody("diffs[0].destination.extension", equalTo("bak"))
                .expectBody("diffs[0].destination.parent", equalTo(""))
                .expectBody("diffs[0].hunks", notNullValue())
                .expectBody("diffs[0].hunks.size()", equalTo(1))
                .expectBody("diffs[0].hunks[0].sourceLine", equalTo(0))
                .expectBody("diffs[0].hunks[0].destinationLine", equalTo(1))
                .expectBody("diffs[0].hunks[0].segments", notNullValue())
                .expectBody("diffs[0].hunks[0].segments.size()", equalTo(1))
                .expectBody("diffs[0].hunks[0].segments[0].type", equalTo("ADDED"))
                .expectBody("diffs[0].hunks[0].segments[0].lines", notNullValue())
                .expectBody("diffs[0].hunks[0].segments[0].lines.size()", equalTo(1))
                .expectBody("diffs[0].hunks[0].segments[0].lines[0].source", equalTo(0))
                .expectBody("diffs[0].hunks[0].segments[0].lines[0].destination", equalTo(1))
                .expectBody("diffs[0].hunks[0].segments[0].lines[0].line", equalTo("Autem placeat ducimus consequatur ipsum vel laboriosam voluptate. Est tempora doloribus laboriosam. Quam esse adipisci. Doloribus maiores magni nulla repellat."))
                .expectBody("diffs[0].hunks[0].segments[0].lines[0].truncated", equalTo(false)).build();
    }

    private void openPullRequest(TestContext testContext, String from, String to) {
        pullRequest = testContext.pullRequest(new PullRequestTestHelper.Builder(AUTHOR, AUTHOR, PR_TITLE, PR_DESC,
                PR_DIFF_TEST, PR_DIFF_TEST, from,
                PR_DIFF_TEST, PR_DIFF_TEST, to));
    }
}
