package it.com.atlassian.stash.rest.build.status;

import com.atlassian.bitbucket.test.DefaultFuncTestData;
import com.jayway.restassured.RestAssured;
import com.jayway.restassured.http.ContentType;
import it.com.atlassian.stash.BaseRestTest;
import net.sf.json.JSONObject;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.StringUtils;
import org.junit.Test;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;

import static com.jayway.restassured.RestAssured.withArgs;
import static org.hamcrest.Matchers.*;

public class BuildStatusResourceTest extends BaseRestTest {

    private final SecureRandom rand = new SecureRandom();

    @Test
    public void testPushAndRetrieveStatus() {
        String commitId = randomCommitId();
        String key = random("key");
        pushStatus(commitId, key, "SUCCESSFUL", "name", "desc", "http://example.com");

        RestAssured.
                given().
                auth().preemptive().basic(DefaultFuncTestData.getAdminUser(), DefaultFuncTestData.getAdminPassword()).
                header("Accept", ContentType.JSON.getAcceptHeader()).
                expect().
                body("values[0].key", equalTo(key)).
                body("values[0].state", equalTo("SUCCESSFUL")).
                body("values[0].name", equalTo("name")).
                body("values[0].description", equalTo("desc")).
                body("values[0].url", equalTo("http://example.com")).
                statusCode(200).
                when().
                get(restUrl(commitId));
    }

    @Test
    public void testPushAndRetrieveMultipleStatus() {
        String commitId = randomCommitId();
        String key1 = random("key1");
        String key2 = random("key2");

        pushStatus(commitId, key1, "FAILED", "name1", "desc", "http://example.com/1");
        pushStatus(commitId, key2, "SUCCESSFUL", "name2", "desc2", "http://example.com/2");
        pushStatus(commitId, key1, "SUCCESSFUL", "name3", "desc3", "http://example.com/3"); // push on the initial key

        RestAssured.
                given().
                auth().preemptive().basic(DefaultFuncTestData.getAdminUser(), DefaultFuncTestData.getAdminPassword()).
                header("Accept", ContentType.JSON.getAcceptHeader()).
                expect().
                body("values[0].key", equalTo(key1)).
                body("values[0].state", equalTo("SUCCESSFUL")).
                body("values[0].name", equalTo("name3")).
                body("values[0].description", equalTo("desc3")).
                body("values[0].url", equalTo("http://example.com/3")).
                body("values[1].key", equalTo(key2)).
                body("values[1].state", equalTo("SUCCESSFUL")).
                body("values[1].name", equalTo("name2")).
                body("values[1].description", equalTo("desc2")).
                body("values[1].url", equalTo("http://example.com/2")).
                statusCode(200).
                when().
                get(restUrl(commitId));
    }

    /* check the name can be empty */
    @Test
    public void testPushStatusWithEmptyName() {
            String commitId = randomCommitId(), key = random("key");
            pushStatus(commitId, key, "SUCCESSFUL", "", "description", "http://example.com");
            RestAssured.
                    given().
                    auth().preemptive().basic(DefaultFuncTestData.getAdminUser(), DefaultFuncTestData.getAdminPassword()).
                    header("Accept", ContentType.JSON.getAcceptHeader()).
                    expect().
                    body("values[0].key", equalTo(key)).
                    body("values[0].name", nullValue()).
                    statusCode(200).
                    when().
                    get(restUrl(commitId));
    }

    /* check the name can be null */
    @Test
    public void testPushStatusWithNoName() {
            String commitId = randomCommitId(), key = random("key");
            pushStatus(commitId, key, "SUCCESSFUL", null, "description", "http://example.com");
            RestAssured.
                    given().
                    auth().preemptive().basic(DefaultFuncTestData.getAdminUser(), DefaultFuncTestData.getAdminPassword()).
                    header("Accept", ContentType.JSON.getAcceptHeader()).
                    expect().
                    body("values[0].key", equalTo(key)).
                    body("values[0].name", nullValue()).
                    statusCode(200).
                    when().
                    get(restUrl(commitId));
    }

    /* check the description can be empty */
    @Test
    public void testPushStatusWithEmptyDescription() {
            String commitId = randomCommitId(), key = random("key");
            pushStatus(commitId, key, "SUCCESSFUL", "name", "", "http://example.com");
            RestAssured.
                    given().
                    auth().preemptive().basic(DefaultFuncTestData.getAdminUser(), DefaultFuncTestData.getAdminPassword()).
                    header("Accept", ContentType.JSON.getAcceptHeader()).
                    expect().
                    body("values[0].key", equalTo(key)).
                    body("values[0].description", nullValue()).
                    statusCode(200).
                    when().
                    get(restUrl(commitId));
    }

    /* check the description can be null */
    @Test
    public void testPushStatusWithNoDescription() {
            String commitId = randomCommitId(), key = random("key");
            pushStatus(commitId, key, "SUCCESSFUL", "name", null, "http://example.com");
            RestAssured.
                    given().
                    auth().preemptive().basic(DefaultFuncTestData.getAdminUser(), DefaultFuncTestData.getAdminPassword()).
                    header("Accept", ContentType.JSON.getAcceptHeader()).
                    expect().
                    body("values[0].key", equalTo(key)).
                    body("values[0].description", nullValue()).
                    statusCode(200).
                    when().
                    get(restUrl(commitId));
    }

    /* check various casings are accepted for the state */
    @Test
    public void testPushStatusWithMixedCaseState() {
        for (String state : Arrays.asList("successful", "Successful", "SUCCESSFUL")) {
            String commitId = randomCommitId(), key = random("key");
            pushStatus(commitId, key, state, "42", "description", "http://example.com");
            RestAssured.
                    given().
                    auth().preemptive().basic(DefaultFuncTestData.getAdminUser(), DefaultFuncTestData.getAdminPassword()).
                    header("Accept", ContentType.JSON.getAcceptHeader()).
                    expect().
                    body("values[0].key", equalTo(key)).
                    body("values[0].state", equalTo("SUCCESSFUL")).
                    statusCode(200).
                    when().
                    get(restUrl(commitId));
        }
    }

    /* check long description are accepted but truncated */
    @Test
    public void testPushStatusWithVeryLargeDescription() {
        String commitId = randomCommitId(), key = random("key");
        String description = StringUtils.repeat("abc", 100);
        pushStatus(commitId, key, "SUCCESSFUL", "42", description, "http://example.com");
        RestAssured.
                given().
                auth().preemptive().basic(DefaultFuncTestData.getAdminUser(), DefaultFuncTestData.getAdminPassword()).
                header("Accept", ContentType.JSON.getAcceptHeader()).
                expect().
                body("values[0].key", equalTo(key)).
                body("values[0].description", equalTo(StringUtils.abbreviate(description, 255))).
                statusCode(200).
                when().
                get(restUrl(commitId));
    }

    @Test
    public void testDisallowPushByAnonymousUser() {
        RestAssured.
                given().
                body(createStatus(random("key"), "SUCCESSFUL", "name", "description", "http://example.com")).
                contentType(ContentType.JSON).
                header("Accept", ContentType.JSON.getAcceptHeader()).
                expect().
                statusCode(401).
                when().
                post(restUrl(randomCommitId()));
    }

    @Test
    public void testDisallowPushByUnlicensedUser() throws Exception {
        final String user = "unlicensed-BSRT";
        testContext.user(user, false);
        RestAssured.
                given().
                auth().preemptive().basic(user, user).
                body(createStatus(random("key"), "SUCCESSFUL", "name", "description", "http://example.com")).
                contentType(ContentType.JSON).
                header("Accept", ContentType.JSON.getAcceptHeader()).
                expect().
                statusCode(401).
                when().
                post(restUrl(randomCommitId()));
    }

    @Test
    public void testPushStatusWithInvalidCommitId() {
        RestAssured.
                given().
                auth().preemptive().basic(DefaultFuncTestData.getAdminUser(), DefaultFuncTestData.getAdminPassword()).
                body(createStatus(random("key"), "SUCCESSFUL", "name", "description", "http://example.com")).
                contentType(ContentType.JSON).
                header("Accept", ContentType.JSON.getAcceptHeader()).
                expect().
                statusCode(400).
                body(containsString("commit ID")).
                when().
                post(restUrl("12345INVALID"));
    }

    @Test
    public void testPushStatusWithInvalidStateAndUrl() {
        RestAssured.
                given().
                auth().preemptive().basic(DefaultFuncTestData.getAdminUser(), DefaultFuncTestData.getAdminPassword()).
                body(createStatus(random("key"), "WRONG STATUS", "name", "description", "not an URL")).
                contentType(ContentType.JSON).
                header("Accept", ContentType.JSON.getAcceptHeader()).
                expect().
                statusCode(400).
                body(containsString("Please specify a valid state")).
                body(containsString("Please specify a valid url")).
                when().
                post(restUrl(randomCommitId()));
    }

    @Test
    public void testPushWithTooLargeName() throws Exception {
        String name = StringUtils.repeat("name", 70);
        RestAssured.
                given().
                auth().preemptive().basic(DefaultFuncTestData.getAdminUser(), DefaultFuncTestData.getAdminPassword()).
                body(createStatus(random("key"), "SUCCESSFUL", name, "description", "http://example.com")).
                contentType(ContentType.JSON).
                header("Accept", ContentType.JSON.getAcceptHeader()).
                expect().
                statusCode(400).
                body(containsString("Please specify a name smaller than 255 characters")).
                when().
                post(restUrl(randomCommitId()));
    }

    @Test
    public void testGetStatsForIndividualCommit() throws Exception {
        String commitId = randomCommitId();
        pushStatus(commitId, random("key"), "FAILED", "name1", "desc", "http://example.com/1");
        pushStatus(commitId, random("key"), "SUCCESSFUL", "name2", "desc2", "http://example.com/2");
        pushStatus(commitId, random("key"), "SUCCESSFUL", "name3", "desc3", "http://example.com/3");
        pushStatus(commitId, random("key"), "INPROGRESS", "name4", "desc4", "http://example.com/4");

        RestAssured.
                given().
                auth().preemptive().basic(DefaultFuncTestData.getAdminUser(), DefaultFuncTestData.getAdminPassword()).
                header("Accept", ContentType.JSON.getAcceptHeader()).
                expect().
                body("successful", equalTo(2)).
                body("failed", equalTo(1)).
                body("inProgress", equalTo(1)).
                statusCode(200).
                when().
                get(restStatsUrl(commitId));
    }

    @Test
    public void testGetStatsForMultipleCommits() throws Exception {
        String commitId1 = randomCommitId();
        pushStatus(commitId1, random("key1"), "SUCCESSFUL", "name1", "desc", "http://example.com/1");
        pushStatus(commitId1, random("key5"), "SUCCESSFUL", "name5", "desc5", "http://example.com/5");

        String commitId2 = randomCommitId();
        pushStatus(commitId2, random("key2"), "FAILED", "name2", "desc2", "http://example.com/2");
        pushStatus(commitId2, random("key3"), "FAILED", "name3", "desc3", "http://example.com/3");
        pushStatus(commitId2, random("key6"), "INPROGRESS", "name6", "desc6", "http://example.com/6");

        String commitId3 = randomCommitId();
        pushStatus(commitId3, random("key4"), "SUCCESSFUL", "name4", "desc4", "http://example.com/4");

        RestAssured.
                given().
                body(new String[]{commitId1, commitId2, commitId3}).
                auth().preemptive().basic(DefaultFuncTestData.getAdminUser(), DefaultFuncTestData.getAdminPassword()).
                contentType(ContentType.JSON).
                header("Accept", ContentType.JSON.getAcceptHeader()).
                expect().
                body("'%s'.'successful'", withArgs(commitId1), equalTo(2)).
                body("'%s'.'failed'", withArgs(commitId1), equalTo(0)).
                body("'%s'.'inProgress'", withArgs(commitId1), equalTo(0)).
                body("'%s'.'successful'", withArgs(commitId2), equalTo(0)).
                body("'%s'.'failed'", withArgs(commitId2), equalTo(2)).
                body("'%s'.'inProgress'", withArgs(commitId2), equalTo(1)).
                body("'%s'.'successful'", withArgs(commitId3), equalTo(1)).
                body("'%s'.'failed'", withArgs(commitId3), equalTo(0)).
                body("'%s'.'inProgress'", withArgs(commitId3), equalTo(0)).
                statusCode(200).
                when().
                post(restStatsUrl());
    }

    private void pushStatus(String commitId, String key, String state, String name, String description, String url) {
        Object status = createStatus(key, state, name, description, url);
        RestAssured.
                given().
                auth().preemptive().basic(DefaultFuncTestData.getAdminUser(), DefaultFuncTestData.getAdminPassword()).
                body(status).
                contentType(ContentType.JSON).
                header("Accept", ContentType.JSON.getAcceptHeader()).
                expect().
                statusCode(204).
                when().
                post(restUrl(commitId));
    }

    private Object createStatus(String key, String state, String name, String description, String url) {
        JSONObject status = new JSONObject();
        status.put("key", key);
        status.put("state", state);
        status.put("url", url);

        if (name != null) {
            status.put("name", name);
        }
        if (description != null) {
            status.put("description", description);
        }

        return status;
    }

    private String restUrl(String commitId) {
        return DefaultFuncTestData.getRestURL("build-status", "latest") + "/commits/" + commitId;
    }

    private String restStatsUrl() {
        return DefaultFuncTestData.getRestURL("build-status", "latest") + "/commits/stats";
    }

    private String restStatsUrl(String commitId) {
        return DefaultFuncTestData.getRestURL("build-status", "latest") + "/commits/stats/" + commitId;
    }

    private String random(String prefix) {
        return prefix + '-' + Math.random();
    }

    private String randomCommitId() {
        try {
            byte[] randomBytes = new byte[16];
            rand.nextBytes(randomBytes);
            return Hex.encodeHexString(sha1(randomBytes));
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException(e);
        }
    }

    private byte[] sha1(byte[] bytes) throws NoSuchAlgorithmException {
        return MessageDigest.getInstance("SHA1").digest(bytes);
    }

}
