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

import com.atlassian.bitbucket.rest.util.RestUtils;
import com.atlassian.bitbucket.junit.HashedTestNameRule;
import com.atlassian.bitbucket.test.DefaultFuncTestData;
import com.atlassian.bitbucket.permission.Permission;
import com.jayway.restassured.RestAssured;
import com.jayway.restassured.path.json.JsonPath;
import com.jayway.restassured.specification.ResponseSpecification;
import it.com.atlassian.stash.BaseRestTest;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;

import javax.ws.rs.core.MediaType;
import java.io.IOException;
import java.util.List;
import java.util.Map;

import static com.atlassian.bitbucket.test.PermissionsTestHelper.revokeProjectPermissionForUser;
import static com.atlassian.bitbucket.test.PermissionsTestHelper.revokeRepositoryPermissionForUser;
import static com.atlassian.bitbucket.test.rest.JsonMatchers.*;
import static com.atlassian.bitbucket.test.rest.RepositoryJsonMatchers.isRepositoryWithNameAndProjectKey;
import static javax.ws.rs.core.Response.Status;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.junit.Assert.*;

public class RecentRepositoriesResourceTest extends BaseRestTest {

    public static final String PASSWORD1 = "password";

    public static final String PROJECT1_KEY = "PROJ1";
    public static final String PROJECT2_KEY = "PROJ2";
    public static final String REPOSITORY_SLUG_PREFIX = "repository";
    public static final int NUM_REPOS_PER_PROJECT = 10;

    @Rule
    public final HashedTestNameRule hashedTestName = new HashedTestNameRule();

    private HttpClient client;

    @BeforeClass
    public static void createTestData() {
        getClassTestContext().project(PROJECT1_KEY, "Recent Repos Project 1", "Recent Repos Project 1 description");
        getClassTestContext().project(PROJECT2_KEY, "Recent Repos Project 2", "Recent Repos Project 2 description");
        for (int i = 0; i < NUM_REPOS_PER_PROJECT; i++) {
            getClassTestContext().repository(PROJECT1_KEY, REPOSITORY_SLUG_PREFIX + i);
            getClassTestContext().repository(PROJECT2_KEY, REPOSITORY_SLUG_PREFIX + i);
        }
    }

    @Before
    public void setUpHttpClient() {
        client = HttpClientBuilder.create().build();
    }

    @Before
    public void createUserAndPerms() {
        testContext.user(getUsername(), "Recent. Repos", PASSWORD1, getUsername() + "@stash.atlassian.com");
        // read on all repos, ADMIN on repo 0 in each project, WRITE on repo 1 in each project
        testContext.projectPermission(PROJECT1_KEY, getUsername(), Permission.PROJECT_READ)
                .projectPermission(PROJECT2_KEY, getUsername(), Permission.PROJECT_READ)
                .repositoryPermission(PROJECT1_KEY, REPOSITORY_SLUG_PREFIX + 0, getUsername(), Permission.REPO_ADMIN)
                .repositoryPermission(PROJECT2_KEY, REPOSITORY_SLUG_PREFIX + 0, getUsername(), Permission.REPO_ADMIN)
                .repositoryPermission(PROJECT1_KEY, REPOSITORY_SLUG_PREFIX + 1, getUsername(), Permission.REPO_WRITE)
                .repositoryPermission(PROJECT2_KEY, REPOSITORY_SLUG_PREFIX + 1, getUsername(), Permission.REPO_WRITE);
    }

    @Test
    public void testRequiresAuthentication() {
        RestAssured.given()
                .contentType(MediaType.APPLICATION_JSON)
                .expect()
                .statusCode(401)
                .when()
                .get(getRecentRepositoriesUrl());
    }

    @Test
    public void testRecentReposEmptyToStartWith() {
        RestAssured.given()
                .auth()
                .preemptive()
                .basic(getUsername(), PASSWORD1)
                .contentType(MediaType.APPLICATION_JSON)
                .expect()
                .statusCode(200)
                .body("size", equalTo(0))
                .body("limit", equalTo(RestUtils.DEFAULT_RECENT_REPOS_LIMIT))
                .body("start", equalTo(Integer.valueOf(RestUtils.DEFAULT_START)))
                .when()
                .get(getRecentRepositoriesUrl());
    }

    @Test
    public void testRepeatedAccessOfRepoYieldsOneEntry() throws IOException, InterruptedException {
        fireRepositoryAccessedEvent(getUsername(), PROJECT1_KEY, REPOSITORY_SLUG_PREFIX + "0");
        //At least one millisecond between last event
        Thread.sleep(1);
        fireRepositoryAccessedEvent(getUsername(), PROJECT1_KEY, REPOSITORY_SLUG_PREFIX + "0");

        RestAssured.given()
                .auth()
                .preemptive()
                .basic(getUsername(), PASSWORD1)
                .contentType(MediaType.APPLICATION_JSON)
                .expect()
                .statusCode(200)
                .body("size", equalTo(1))
                .body("values[0].slug", equalTo("repository" + "0"))
                .body("values[0].project.key", equalTo(PROJECT1_KEY))
                .when()
                .get(getRecentRepositoriesUrl());
    }

    @Test
    public void testRepoAccessIsRecorded() throws IOException, InterruptedException {
        for (int i = 0; i < NUM_REPOS_PER_PROJECT; i++) {
            fireRepositoryAccessedEvent(getUsername(), PROJECT1_KEY, REPOSITORY_SLUG_PREFIX + i);
            //At least one millisecond between last event
            Thread.sleep(1);
        }

        ResponseSpecification expect = RestAssured.given()
                        .auth()
                        .preemptive()
                        .basic(getUsername(), PASSWORD1)
                        .contentType(MediaType.APPLICATION_JSON)
                        .expect()
                        .statusCode(200)
                        .body("size", equalTo(NUM_REPOS_PER_PROJECT));

        for (int i = 0; i < NUM_REPOS_PER_PROJECT; i++) {
            expect = expect.body("values[" + i + "].slug", equalTo("repository" + (NUM_REPOS_PER_PROJECT - i - 1)));
            expect = expect.body("values[" + i + "].project.key", equalTo(PROJECT1_KEY));
        }

        expect
                .with().param("limit", NUM_REPOS_PER_PROJECT)
                .when()
                .get(getRecentRepositoriesUrl());
    }

    @Test
    public void testRepoAccessNotShownWhenPermsRevoked() throws IOException, InterruptedException {
        int HALF_NUM_REPOS_PER_PROJECT = NUM_REPOS_PER_PROJECT / 2;
        for (int i = 0; i < HALF_NUM_REPOS_PER_PROJECT; i++) {
            fireRepositoryAccessedAndWait(getUsername(), PROJECT1_KEY, REPOSITORY_SLUG_PREFIX + i);
        }
        for (int i = 0; i < HALF_NUM_REPOS_PER_PROJECT; i++) {
            fireRepositoryAccessedAndWait(getUsername(), PROJECT2_KEY, REPOSITORY_SLUG_PREFIX + i);
        }

        revokeAllPermissionsForProject(PROJECT1_KEY);

        ResponseSpecification expect = RestAssured.given()
                        .auth()
                        .preemptive()
                        .basic(getUsername(), PASSWORD1)
                        .contentType(MediaType.APPLICATION_JSON)
                        .expect()
                        .statusCode(200)
                        .body("size", equalTo(NUM_REPOS_PER_PROJECT / 2));

        for (int i = 0; i < HALF_NUM_REPOS_PER_PROJECT; i++) {
            expect = expect.body("values[" + i + "].slug", equalTo("repository" + (HALF_NUM_REPOS_PER_PROJECT - i - 1)));
            expect = expect.body("values[" + i + "].project.key", equalTo(PROJECT2_KEY));
        }

        expect.with().param("limit", NUM_REPOS_PER_PROJECT)
                .when()
                .get(getRecentRepositoriesUrl());
    }

    @SuppressWarnings("unchecked")
    @Test
    public void testRecentlyAccessedByWritePermission() throws IOException, InterruptedException {
        for (int i = 0; i < NUM_REPOS_PER_PROJECT; i++) {
            fireRepositoryAccessedAndWait(getUsername(), PROJECT1_KEY, REPOSITORY_SLUG_PREFIX + i);
        }

        JsonPath response = getRecentlyAccessedRepositories(Permission.REPO_WRITE.name(), 0, NUM_REPOS_PER_PROJECT);

        assertThat(response, isPageMatching(2, true));
        assertThat(getValues(response), contains(
                isRepositoryWithNameAndProjectKey(REPOSITORY_SLUG_PREFIX + 1, PROJECT1_KEY),
                isRepositoryWithNameAndProjectKey(REPOSITORY_SLUG_PREFIX + 0, PROJECT1_KEY)
        ));
    }

    @Test
    public void testRecentlyAccessedByAdminPermission() throws IOException, InterruptedException {
        for (int i = 0; i < NUM_REPOS_PER_PROJECT; i++) {
            fireRepositoryAccessedAndWait(getUsername(), PROJECT1_KEY, REPOSITORY_SLUG_PREFIX + i);
        }

        JsonPath response = getRecentlyAccessedRepositories(Permission.REPO_ADMIN.name(), 0, NUM_REPOS_PER_PROJECT);

        assertThat(response, isPageMatching(1, true));
        assertThat(getValues(response), contains(
                isRepositoryWithNameAndProjectKey(REPOSITORY_SLUG_PREFIX + 0, PROJECT1_KEY)
        ));
    }

    @Test
    public void testInvalidPermissionResultsInBadRequest() throws IOException, InterruptedException {
        JsonPath response = getRecentlyAccessedRepositories("nosuchpermission", 0, 5, Status.BAD_REQUEST);

        assertThat(response, isErrorResponseThat(containsError(
                isRestErrorWithMessage("nosuchpermission is not a valid permission.")
        )));
    }

    @Test
    public void testPermissionNotRelatedToRepositoryResultsInBadRequest() throws IOException, InterruptedException {
        JsonPath response = getRecentlyAccessedRepositories(Permission.PROJECT_VIEW.name(), 0, 5, Status.BAD_REQUEST);

        assertThat(response, isErrorResponseThat(containsError(
                isRestErrorWithMessage(Permission.PROJECT_VIEW + " is not a Repository permission.")
        )));
    }

    private void fireRepositoryAccessedAndWait(String username, String projectKey, String repositorySlug)
            throws IOException, InterruptedException {
        assertEquals(Status.OK.getStatusCode(), fireRepositoryAccessedEvent(username, projectKey, repositorySlug));
        // wait 1ms to make sure we have distinct timestamps between events
        Thread.sleep(1);
    }

    private int fireRepositoryAccessedEvent(String username, String projectKey, String repoSlug) throws IOException {
        String uri = String.format(
                DefaultFuncTestData.getBaseURL() + "/plugins/servlet/recentReposDriver?su=%s&user=%s&proj=%s&repo=%s",
                "admin", username, projectKey, repoSlug);
        HttpGet get = new HttpGet(uri);
        HttpResponse response = client.execute(get);
        int statusCode = response.getStatusLine().getStatusCode();
        assertEquals("Bad status code. Code = \"" + statusCode + "\", uri = \"" + uri +
                "\", content = \"" + responseToString(response.getEntity()), 200, statusCode);
        EntityUtils.consume(response.getEntity());
        return statusCode;
    }

    private JsonPath getRecentlyAccessedRepositories(String permission, int start, int limit) {
        return getRecentlyAccessedRepositories(permission, start, limit, Status.OK);
    }

    private JsonPath getRecentlyAccessedRepositories(String permission, int start, int limit, Status expectedStatus) {
        return RestAssured.given()
                .auth().preemptive().basic(getUsername(), PASSWORD1)
                .contentType(MediaType.APPLICATION_JSON)
                .expect().statusCode(expectedStatus.getStatusCode())
                .log().ifStatusCodeMatches(not(expectedStatus.getStatusCode()))
                .with().param("permission", permission).param("start", start).param("limit", limit)
                .get(getRecentRepositoriesUrl()).jsonPath();
    }

    private static String getRecentRepositoriesUrl() {
        return DefaultFuncTestData.getRestURL() + "/profile/recent/repos";
    }

    private static List<Map<String, Object>> getValues(JsonPath listResponse) {
        return listResponse.getList("values");
    }

    private static String responseToString(HttpEntity entity) throws IOException {
        return EntityUtils.toString(entity, "UTF-8");
    }

    private void revokeAllPermissionsForProject(String projectKey) {
        revokeProjectPermissionForUser(projectKey, getUsername());
        revokeRepositoryPermissionForUser(projectKey, REPOSITORY_SLUG_PREFIX + 0, getUsername());
        revokeRepositoryPermissionForUser(projectKey, REPOSITORY_SLUG_PREFIX + 1, getUsername());
    }

    private String getUsername() {
        return "recentRepos1-" + hashedTestName;
    }
}
