package it.com.atlassian.stash.rest.scm.git.lfs;

import com.atlassian.bitbucket.test.DefaultFuncTestData;
import com.jayway.restassured.RestAssured;
import com.jayway.restassured.response.Response;
import net.sf.json.JSONObject;
import org.apache.commons.codec.digest.DigestUtils;
import org.hamcrest.Matcher;
import org.junit.Test;

import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response.Status;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.UUID;

import static com.atlassian.bitbucket.test.DefaultFuncTestData.getAdminPassword;
import static com.atlassian.bitbucket.test.DefaultFuncTestData.getAdminUser;
import static javax.ws.rs.core.Response.Status.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertTrue;

/**
 * Tests the Git LFS Batch API. A more detailed authentication matrix test can be found
 * in {@link GitLfsBatchAuthRestTest}.
 */
public class GitLfsBatchRestTest extends BaseGitLfsRestTest {

    private static final String EXPECTED_BASE_STORE_URL = DefaultFuncTestData.getBaseURL() + "/rest/git-lfs/storage/";
    private static final String TEST_OID = "b0ea9b7f5267c26ee7f7606d26db9e548aad9a890ec4cd906b3e06b31e6d4888";
    private static final int TEST_SIZE = 8192;

    @Test
    public void testBatchPostUploadSingleObject() {
        RestAssured.given()
                    .auth().preemptive().basic(getAdminUser(), getAdminPassword())
                    .contentType(GIT_LFS_CONTENT_TYPE)
                    .body(buildRequestBody("upload", TEST_OID, TEST_SIZE))
                .when()
                    .post(getBaseLfsUrl(TEST_PROJECT, TEST_REPO) + "/objects/batch")
                .then()
                    .statusCode(OK.getStatusCode())
                    .contentType(GIT_LFS_CONTENT_TYPE)
                    .body("objects[0].oid", equalTo(TEST_OID))
                    .body("objects[0].size", equalTo(TEST_SIZE))
                    .body("objects[0].actions.upload.href", startsWith(EXPECTED_BASE_STORE_URL))
                    .body("objects[0].actions.upload.href", containsString(TEST_OID))
                    .body("objects[0].actions.upload.header.Authorization", startsWith("JWT"))
                    .log().ifValidationFails();
    }

    @Test
    public void testBatchPostDownloadSingleObjectNotFound() {
        RestAssured.given()
                    .auth().preemptive().basic(getAdminUser(), getAdminPassword())
                    .contentType(GIT_LFS_CONTENT_TYPE)
                    .body(buildRequestBody("download", TEST_OID, TEST_SIZE))
                .when()
                    .post(getBaseLfsUrl(TEST_PROJECT, TEST_REPO) + "/objects/batch")
                .then()
                    .statusCode(OK.getStatusCode())
                    .contentType(GIT_LFS_CONTENT_TYPE)
                    .body("objects[0].oid", equalTo(TEST_OID))
                    .body("objects[0].size", equalTo(TEST_SIZE))
                    .body("objects[0].error.code", equalTo(NOT_FOUND.getStatusCode()))
                    .body("objects[0].error.message", equalTo("Object does not exist on the server"))
                    .log().ifValidationFails();
    }

    @Test
    public void testBatchPostUploadMultipleObject() {
        String testOid2 = "829bb5b9fd0c78bbf351ae8a25fae8b0a33379f1758045ef60efa63c7af20491";
        int testSize2 = 456;

        RestAssured.given()
                    .auth().preemptive().basic(getAdminUser(), getAdminPassword())
                    .contentType(GIT_LFS_CONTENT_TYPE)
                    .body(buildRequestBody("upload", TEST_OID, TEST_SIZE, testOid2, testSize2))
                .when()
                    .post(getBaseLfsUrl(TEST_PROJECT, TEST_REPO) + "/objects/batch")
                .then()
                    .statusCode(OK.getStatusCode())
                    .contentType(GIT_LFS_CONTENT_TYPE)
                    .body("objects[0].oid", equalTo(TEST_OID))
                    .body("objects[0].size", equalTo(TEST_SIZE))
                    .body("objects[0].actions.upload.href", startsWith(EXPECTED_BASE_STORE_URL))
                    .body("objects[0].actions.upload.href", containsString(TEST_OID))
                    .body("objects[0].actions.upload.header.Authorization", startsWith("JWT"))
                    .body("objects[1].oid", equalTo(testOid2))
                    .body("objects[1].size", equalTo(testSize2))
                    .body("objects[1].actions.upload.href", startsWith(EXPECTED_BASE_STORE_URL))
                    .body("objects[1].actions.upload.href", containsString(testOid2))
                    .body("objects[1].actions.upload.header.Authorization", startsWith("JWT"))
                    .log().ifValidationFails();
    }

    @Test
    public void testBatchPostUploadSingleObjectWithInvalidOid() {
        String invalidOid = "1234567890";

        RestAssured.given()
                    .auth().preemptive().basic(getAdminUser(), getAdminPassword())
                    .contentType(GIT_LFS_CONTENT_TYPE)
                    .body(buildRequestBody("upload", invalidOid, TEST_SIZE))
                .when()
                    .post(getBaseLfsUrl(TEST_PROJECT, TEST_REPO) + "/objects/batch")
                .then()
                    .statusCode(OK.getStatusCode())
                    .contentType(GIT_LFS_CONTENT_TYPE)
                    .body("objects[0].oid", equalTo(invalidOid))
                    .body("objects[0].size", equalTo(TEST_SIZE))
                    .body("objects[0].error.code", equalTo(422))
                    .body("objects[0].error.message", equalTo("Invalid object ID"))
                    .log().ifValidationFails();
    }

    @Test
    public void testBatchPostUploadSingleObjectWithInvalidSize() {
        int invalidSize = -1;

        RestAssured.given()
                    .auth().preemptive().basic(getAdminUser(), getAdminPassword())
                    .contentType(GIT_LFS_CONTENT_TYPE)
                    .body(buildRequestBody("upload", TEST_OID, invalidSize))
                .when()
                    .post(getBaseLfsUrl(TEST_PROJECT, TEST_REPO) + "/objects/batch")
                .then()
                    .statusCode(OK.getStatusCode())
                    .contentType(GIT_LFS_CONTENT_TYPE)
                    .body("objects[0].oid", equalTo(TEST_OID))
                    .body("objects[0].size", equalTo(invalidSize))
                    .body("objects[0].error.code", equalTo(422))
                    .body("objects[0].error.message", equalTo("Invalid object size"))
                    .log().ifValidationFails();
    }

    @Test
    public void testBatchPostUploadSingleObjectWithNonNumericSize() {
        JSONObject requestBody = buildRequestBody("upload", TEST_OID, TEST_SIZE);

        // Patch the size attribute to be invalid
        requestBody.getJSONArray("objects")
                .optJSONObject(0)
                .put("size", "&@#$ab$@#$");

        batchPostWithError(TEST_PROJECT, TEST_REPO, requestBody, Status.BAD_REQUEST,
                startsWith("Object size must be an integer value"));
    }

    @Test
    public void testBatchPostUploadSingleObjectWithInsufficientFreeSpace() {
        long size = Long.MAX_VALUE;

        RestAssured.given()
                    .auth().preemptive().basic(getAdminUser(), getAdminPassword())
                    .contentType(GIT_LFS_CONTENT_TYPE)
                    .body(buildRequestBody("upload", TEST_OID, size))
                .when()
                    .post(getBaseLfsUrl(TEST_PROJECT, TEST_REPO) + "/objects/batch")
                .then()
                    .statusCode(OK.getStatusCode())
                    .contentType(GIT_LFS_CONTENT_TYPE)
                    .body("objects[0].oid", equalTo(TEST_OID))
                    .body("objects[0].size", equalTo(size))
                    .body("objects[0].error.code", equalTo(503))
                    .body("objects[0].error.message", equalTo("Insufficient free space in store"))
                    .log().ifValidationFails();
    }

    @Test
    public void testBatchPostProjectNotFound() {
        JSONObject requestBody = buildRequestBody("upload", TEST_OID, TEST_SIZE);

        batchPostWithError("InvalidProject", TEST_REPO, requestBody, Status.NOT_FOUND,
                startsWith("Repository not found"));
    }

    @Test
    public void testBatchPostRepoNotFound() {
        JSONObject requestBody = buildRequestBody("upload", TEST_OID, TEST_SIZE);

        batchPostWithError(TEST_PROJECT, "InvalidRepo", requestBody, Status.NOT_FOUND,
                startsWith("Repository not found"));
    }

    @Test
    public void testBatchPostUnsupportedOperation() {
        JSONObject requestBody = buildRequestBody("foo", TEST_OID, TEST_SIZE);

        batchPostWithError(TEST_PROJECT, TEST_REPO, requestBody, Status.BAD_REQUEST,
                equalTo("Unsupported operation: foo"));
    }

    @Test
    public void testBatchPostMissingOperation() {
        JSONObject requestBody = buildRequestBody("upload", TEST_OID, TEST_SIZE).discard("operation");

        batchPostWithError(TEST_PROJECT, TEST_REPO, requestBody, Status.BAD_REQUEST,
                equalTo("Request is missing required 'operation' attribute"));
    }

    @Test
    public void testBatchPostMissingOid() {
        JSONObject requestBody = buildRequestBody("upload", TEST_OID, TEST_SIZE);
        requestBody.getJSONArray("objects")
                .optJSONObject(0)
                .discard("oid");

        batchPostWithError(TEST_PROJECT, TEST_REPO, requestBody, Status.BAD_REQUEST,
                equalTo("Request is missing required 'oid' attribute"));
    }

    @Test
    public void testBatchPostMissingObjectsArray() {
        JSONObject requestBody = buildRequestBody("upload", TEST_OID, TEST_SIZE).discard("objects");

        batchPostWithError(TEST_PROJECT, TEST_REPO, requestBody, Status.BAD_REQUEST,
                equalTo("Request is missing required 'objects' attribute"));
    }

    @Test
    public void testBatchPostMalformedJson() {
        batchPostWithError(TEST_PROJECT, TEST_REPO, "{ , }", Status.BAD_REQUEST, any(String.class));
    }

    @Test
    public void testUploadObjectAlreadyExists() throws Exception {
        // Usually introducing randomness into a test is bad, but since LFS doesn't have an API to delete
        // objects, executing a test multiple times without cleaning the workspace is problematic. Creating
        // a unique object permits subsequent test runs to always result in a new file being PUT.
        String content = UUID.randomUUID().toString();
        String oid = DigestUtils.sha256Hex(content);

        // Get upload URL
        Response response = RestAssured.given()
                    .auth().preemptive().basic(getAdminUser(), getAdminPassword())
                    .contentType(GIT_LFS_CONTENT_TYPE)
                    .body(buildRequestBody("upload", oid, content.length()))
                .when()
                    .post(getBaseLfsUrl(TEST_PROJECT, TEST_REPO) + "/objects/batch")
                .then()
                    .statusCode(OK.getStatusCode())
                    .contentType(GIT_LFS_CONTENT_TYPE)
                    .body("objects[0].oid", equalTo(oid))
                    .body("objects[0].size", equalTo(content.length()))
                    .log().ifValidationFails()
                    .extract()
                    .response();

        String url = response.path("objects[0].actions.upload.href");
        String token = response.path("objects[0].actions.upload.header.Authorization");

        // Put file
        InputStream contentStream = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
        RestAssured.given()
                    .header("Authorization", token)
                    .contentType(MediaType.APPLICATION_OCTET_STREAM)
                    .body(contentStream)
                .when()
                    .put(url)
                .then()
                    .statusCode(OK.getStatusCode())
                    .log().ifValidationFails();

        // Request a download, just to ensure the server recognises the object now exists
        RestAssured.given()
                    .auth().preemptive().basic(getAdminUser(), getAdminPassword())
                    .contentType(GIT_LFS_CONTENT_TYPE)
                    .body(buildRequestBody("download", oid, content.length()))
                .when()
                    .post(getBaseLfsUrl(TEST_PROJECT, TEST_REPO) + "/objects/batch")
                .then()
                    .statusCode(OK.getStatusCode())
                    .contentType(GIT_LFS_CONTENT_TYPE)
                    .body("objects[0].oid", equalTo(oid))
                    .body("objects[0].size", equalTo(content.length()))
                    .log().ifValidationFails();

        // Request an upload URL for the same file, response should not provide an upload action
        HashMap<String, String> actions = RestAssured.given()
                    .auth().preemptive().basic(getAdminUser(), getAdminPassword())
                    .contentType(GIT_LFS_CONTENT_TYPE)
                    .body(buildRequestBody("upload", oid, content.length()))
                .when()
                    .post(getBaseLfsUrl(TEST_PROJECT, TEST_REPO) + "/objects/batch")
                .then()
                    .statusCode(OK.getStatusCode())
                    .contentType(GIT_LFS_CONTENT_TYPE)
                    .body("objects[0].oid", equalTo(oid))
                    .body("objects[0].size", equalTo(content.length()))
                    .log().ifValidationFails()
                    .extract()
                    .path("objects[0].actions");
        assertTrue(actions.isEmpty());
    }

    /**
     * This tests a token that is "not well formed", and as a result status 400 is returned.
     * NOTE: Unfortunately the wrong content type is returned from Atlassian Connect's JWT filter, and
     * the message format is also incorrect. However the client handles this, so this test ignores
     * that fact for now.
     */
    @Test
    public void testInvalidAuthToken() {
        RestAssured.given()
                    .header("Authorization", "JWT NOTAVALIDTOKEN")
                    .contentType(GIT_LFS_CONTENT_TYPE)
                    .body(buildRequestBody("upload", TEST_OID, TEST_SIZE))
                .when()
                    .post(getBaseLfsUrl(TEST_PROJECT, TEST_REPO) + "/objects/batch")
                .then()
                    .statusCode(BAD_REQUEST.getStatusCode())
                    .log().ifValidationFails();
    }

    private static <T> void batchPostWithError(String project, String repository, Object body, Status expectedStatus,
                                               Matcher<T> messageMatcher) {
        RestAssured.given()
                    .auth().preemptive().basic(getAdminUser(), getAdminPassword())
                    .contentType(GIT_LFS_CONTENT_TYPE)
                    .body(body)
                .when()
                    .post(getBaseLfsUrl(project, repository) + "/objects/batch")
                .then()
                    .statusCode(expectedStatus.getStatusCode())
                    .contentType(GIT_LFS_CONTENT_TYPE)
                    .body("message", messageMatcher)
                    .log().ifValidationFails();
    }
}
