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

import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.test.DefaultFuncTestData;
import com.atlassian.bitbucket.test.GitLfsTestHelper;
import com.google.common.io.ByteStreams;
import com.jayway.restassured.RestAssured;
import com.jayway.restassured.response.ValidatableResponse;
import com.jayway.restassured.specification.RequestSpecification;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.MediaType;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.UUID;

import static javax.ws.rs.core.Response.Status.OK;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.fail;

/**
 * Tests relating to ensuring authentication and authorisation is working correctly on the Git LFS Batch API
 */
@RunWith(Parameterized.class)
public class GitLfsBatchAuthRestTest extends BaseGitLfsRestTest {

    private enum OpType {
        DOWNLOAD,
        UPLOAD
    }

    // Repositories
    private static final String REPO_ADMIN = DefaultFuncTestData.getProject1Repository1();
    private static final String REPO_USER = "regular-user-repo";
    private static final String REPO_PUBLIC = "public-repo";

    // Users
    private static final String USER_ADMIN = DefaultFuncTestData.getAdminUser();
    private static final String USER_REGULAR = DefaultFuncTestData.getRegularUser();
    private static final String USER_NONEXISTENT = "nonexistentuser";

    // Passwords
    private static final String PASSWORD_ADMIN = DefaultFuncTestData.getAdminUser();
    private static final String PASSWORD_REGULAR = DefaultFuncTestData.getRegularUserPassword();
    private static final String PASSWORD_NONEXISTENT = "nonexistentuser-password";
    private static final String PASSWORD_WRONG = "wrong password";

    // Other test constants
    private static final String TEST_PROJECT = DefaultFuncTestData.getProject1();

    private final String project;
    private final String repository;
    private final String user;
    private final String password;
    private final OpType opType;
    private final int expectedStatus;

    private String downloadOid;
    private int downloadSize;

    public GitLfsBatchAuthRestTest(String repository, String user, String password, OpType opType, int expectedStatus) {
        this.project = TEST_PROJECT;
        this.repository = repository;
        this.user = user;
        this.password = password;
        this.opType = opType;
        this.expectedStatus = expectedStatus;
    }

    @BeforeClass
    public static void setupClass() {
        // Repo accessible by regular user
        getClassTestContext().repository(TEST_PROJECT, REPO_USER)
                .repositoryPermission(TEST_PROJECT, REPO_USER, USER_REGULAR, Permission.REPO_READ);
        GitLfsTestHelper.enable(TEST_PROJECT, REPO_USER);

        // Publicly accessible repo
        getClassTestContext().repository(TEST_PROJECT, REPO_PUBLIC, true);
        GitLfsTestHelper.enable(TEST_PROJECT, REPO_PUBLIC);
    }

    @Before
    public void setup() throws IOException {
        // A test object needs to exist for a download request to succeed
        String content = UUID.randomUUID().toString();
        downloadOid = uploadTestObject(TEST_PROJECT, REPO_ADMIN, content);
        downloadOid = uploadTestObject(TEST_PROJECT, REPO_USER, content);
        downloadOid = uploadTestObject(TEST_PROJECT, REPO_PUBLIC, content);
        downloadSize = content.length();
    }

    @Parameterized.Parameters
    public static Collection<Object[]> data() throws Exception {
        return Arrays.asList(new Object[][]{
                // Admin user
                {REPO_ADMIN, USER_ADMIN, PASSWORD_ADMIN, OpType.DOWNLOAD, HttpServletResponse.SC_OK},
                {REPO_ADMIN, USER_ADMIN, PASSWORD_WRONG, OpType.DOWNLOAD, HttpServletResponse.SC_UNAUTHORIZED},
                {REPO_USER, USER_ADMIN, PASSWORD_ADMIN, OpType.DOWNLOAD, HttpServletResponse.SC_OK},
                {REPO_PUBLIC, USER_ADMIN, PASSWORD_ADMIN, OpType.DOWNLOAD, HttpServletResponse.SC_OK},

                {REPO_ADMIN, USER_ADMIN, PASSWORD_ADMIN, OpType.UPLOAD, HttpServletResponse.SC_OK},
                {REPO_ADMIN, USER_ADMIN, PASSWORD_WRONG, OpType.UPLOAD, HttpServletResponse.SC_UNAUTHORIZED},
                {REPO_USER, USER_ADMIN, PASSWORD_ADMIN, OpType.UPLOAD, HttpServletResponse.SC_OK},
                {REPO_PUBLIC, USER_ADMIN, PASSWORD_ADMIN, OpType.UPLOAD, HttpServletResponse.SC_OK},

                // Regular user
                {REPO_ADMIN, USER_REGULAR, PASSWORD_REGULAR, OpType.DOWNLOAD, HttpServletResponse.SC_NOT_FOUND},
                {REPO_USER, USER_REGULAR, PASSWORD_WRONG, OpType.DOWNLOAD, HttpServletResponse.SC_UNAUTHORIZED},
                {REPO_USER, USER_REGULAR, PASSWORD_REGULAR, OpType.DOWNLOAD, HttpServletResponse.SC_OK},
                {REPO_PUBLIC, USER_REGULAR, PASSWORD_REGULAR, OpType.DOWNLOAD, HttpServletResponse.SC_OK},

                {REPO_ADMIN, USER_REGULAR, PASSWORD_REGULAR, OpType.UPLOAD, HttpServletResponse.SC_NOT_FOUND},
                {REPO_USER, USER_REGULAR, PASSWORD_WRONG, OpType.UPLOAD, HttpServletResponse.SC_UNAUTHORIZED},
                {REPO_USER, USER_REGULAR, PASSWORD_REGULAR, OpType.UPLOAD, HttpServletResponse.SC_FORBIDDEN},
                {REPO_PUBLIC, USER_REGULAR, PASSWORD_REGULAR, OpType.UPLOAD, HttpServletResponse.SC_FORBIDDEN},

                // Anonymous
                {REPO_ADMIN, null, null, OpType.DOWNLOAD, HttpServletResponse.SC_UNAUTHORIZED},
                {REPO_USER, null, null, OpType.DOWNLOAD, HttpServletResponse.SC_UNAUTHORIZED},
                {REPO_PUBLIC, null, null, OpType.DOWNLOAD, HttpServletResponse.SC_OK},

                {REPO_ADMIN, null, null, OpType.UPLOAD, HttpServletResponse.SC_UNAUTHORIZED},
                {REPO_USER, null, null, OpType.UPLOAD, HttpServletResponse.SC_UNAUTHORIZED},
                {REPO_PUBLIC, null, null, OpType.UPLOAD, HttpServletResponse.SC_FORBIDDEN},

                // Non-existent user
                {REPO_ADMIN, USER_NONEXISTENT, PASSWORD_NONEXISTENT, OpType.DOWNLOAD, HttpServletResponse.SC_UNAUTHORIZED},
                {REPO_USER, USER_NONEXISTENT, PASSWORD_NONEXISTENT, OpType.DOWNLOAD, HttpServletResponse.SC_UNAUTHORIZED},
                {REPO_PUBLIC, USER_NONEXISTENT, PASSWORD_NONEXISTENT, OpType.DOWNLOAD, HttpServletResponse.SC_UNAUTHORIZED},

                {REPO_ADMIN, USER_NONEXISTENT, PASSWORD_NONEXISTENT, OpType.DOWNLOAD, HttpServletResponse.SC_UNAUTHORIZED},
                {REPO_USER, USER_NONEXISTENT, PASSWORD_NONEXISTENT, OpType.DOWNLOAD, HttpServletResponse.SC_UNAUTHORIZED},
                {REPO_PUBLIC, USER_NONEXISTENT, PASSWORD_NONEXISTENT, OpType.DOWNLOAD, HttpServletResponse.SC_UNAUTHORIZED}
        });
    }

    @Test
    public void testBatch() throws IOException {
        switch (opType) {
            case DOWNLOAD:
                batchDownload();
                break;
            case UPLOAD:
                batchUpload();
                break;
            default:
                fail("Unhandled operation type");
        }
    }

    public void batchDownload() throws IOException {
        ValidatableResponse response = requestForUser(user, password)
                    .body(buildRequestBody("download", downloadOid, downloadSize))
                .when()
                    .post(getBaseLfsUrl(project, repository) + "/objects/batch")
                .then();

        validateResponse(response, "download", downloadOid, downloadSize);

        if (expectedStatus == HttpServletResponse.SC_OK) {
            String url = response.extract().path("objects[0].actions.download.href");
            String token = response.extract().path("objects[0].actions.download.header.Authorization");
            validateObjectAccessible(url, token);
        }
    }

    public void batchUpload() throws IOException {
        String oid = "1920fd2db0ff3f9f047c05ce0b06bdb211270fdb0ad12dd74e2e1f0c9c97e370";
        int size = 123;

        ValidatableResponse response = requestForUser(user, password)
                    .body(buildRequestBody("upload", oid, size))
                .when()
                    .post(getBaseLfsUrl(project, repository) + "/objects/batch")
                .then();

        validateResponse(response, "upload", oid, size);
    }

    /**
     * Create a {@link RequestSpecification} for a given user, with a given password. If the user
     * parameter is non-null then basic auth will be used, otherwise the request will be anonymous.
     */
    private RequestSpecification requestForUser(String user, String password) {
        RequestSpecification requestSpecification = RestAssured.given().contentType(GIT_LFS_CONTENT_TYPE);

        if (user != null) {
            requestSpecification = requestSpecification.auth().preemptive().basic(user, password);
        }

        return requestSpecification;
    }

    /**
     * Ensure the Embedded Store permits retrieving of the object
     */
    private void validateObjectAccessible(String url, String token) throws IOException {
        try (InputStream is = RestAssured.given()
                    .header("Authorization", token)
                .when()
                    .get(url)
                .then()
                    .statusCode(OK.getStatusCode())
                    .contentType(MediaType.APPLICATION_OCTET_STREAM)
                    .log().ifValidationFails()
                    .extract().asInputStream()) {
            ByteStreams.skipFully(is, downloadSize);
        }
    }

    /**
     * Ensure the response object is correct for the given set of test parameters
     */
    private void validateResponse(ValidatableResponse response, String op, String oid, int size) {
        response.log().ifValidationFails();
        response.statusCode(expectedStatus);
        response.contentType(GIT_LFS_CONTENT_TYPE);

        switch (expectedStatus) {
            case HttpServletResponse.SC_OK:
                response.body("objects[0].oid", equalTo(oid));
                response.body("objects[0].size", equalTo(size));
                response.body("objects[0].actions." + op + ".href", startsWith("http://"));
                response.body("objects[0].actions." + op + ".href", containsString(oid));
                response.body("objects[0].actions." + op + ".header.Authorization", startsWith("JWT"));
                break;
            case HttpServletResponse.SC_UNAUTHORIZED:
                response.body("message", startsWith("Not authenticated"));
                break;
            case HttpServletResponse.SC_FORBIDDEN:
                response.body("message", startsWith("You are not permitted to access this resource: Access is denied"));
                break;
            default:
                response.body("message", startsWith("Repository not found"));
        }
    }
}
