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

import com.atlassian.bitbucket.test.ClasspathResourceRule;
import com.atlassian.bitbucket.test.DefaultFuncTestData;
import com.atlassian.bitbucket.test.TestContext;
import com.atlassian.bitbucket.test.rest.project.RestProjectHelper;
import com.atlassian.bitbucket.test.rest.repository.RestRepositoryHelper;
import com.atlassian.bitbucket.test.rest.user.RestUsersHelper;
import com.atlassian.bitbucket.test.rest.user.RestUsersRequest;
import com.atlassian.bitbucket.permission.Permission;
import com.jayway.restassured.RestAssured;
import com.jayway.restassured.specification.ResponseSpecification;
import it.com.atlassian.stash.BaseRestTest;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;

import javax.ws.rs.core.Response;

import static com.atlassian.bitbucket.test.rest.specification.RestErrorSpecifications.expectError;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;

/**
 * Test suite for user search functionality of the user resource.
 */
public class UserResourceSearchTest extends BaseRestTest {

    private static final String GROUP_1 = "group";
    private static final String PROJECT_1 = DefaultFuncTestData.getProject1();
    private static final String PROJECT_2 = "PROJECT_2";
    private static final String REPOSITORY_1 = DefaultFuncTestData.getProject1Repository1();
    private static final String REPOSITORY_2 = "rep_2";

    // all usernames are lower-case to avoid having to consider case-sensitivity in all assertions
    private static final String PROJECT_1_ADMIN = usernameFor(PROJECT_1, "admin");
    private static final String PROJECT_1_WRITE = usernameFor(PROJECT_1, "write");
    private static final String PROJECT_1_READ = usernameFor(PROJECT_1, "read");

    private static final String REPOSITORY_1_ADMIN = usernameFor(REPOSITORY_1, "admin");
    private static final String REPOSITORY_1_WRITE = usernameFor(REPOSITORY_1, "write");
    private static final String REPOSITORY_1_READ = usernameFor(REPOSITORY_1, "read");

    // those users should _never_ be returned for queries against rep_1 permissions
    private static final String PROJECT_2_ADMIN = usernameFor(PROJECT_2, "admin");
    private static final String PROJECT_2_WRITE = usernameFor(PROJECT_2, "write");
    private static final String PROJECT_2_READ = usernameFor(PROJECT_2, "read");

    // has REPO_READ to both test repositories
    private static final String MULTI_REPOSITORY_READ = "multi_repo_read";

    private static final int TOTAL_USER_COUNT = 12;

    @ClassRule
    public static ClasspathResourceRule resources = new ClasspathResourceRule();

    private final RestProjectHelper restProject1Helper = new RestProjectHelper(PROJECT_1);
    private final RestRepositoryHelper restRepository1Helper = new RestRepositoryHelper(PROJECT_1, REPOSITORY_1);
    private final RestUsersHelper restUsersHelper = new RestUsersHelper();

    @BeforeClass
    public static void createUserSearchData() {
        getClassTestContext().globalPermission(DefaultFuncTestData.getRegularUser(), Permission.PROJECT_CREATE);

        getClassTestContext().project(PROJECT_2);
        getClassTestContext().repository(PROJECT_2, REPOSITORY_2);

        getClassTestContext().user(PROJECT_1_ADMIN)
                .projectPermission(PROJECT_1, PROJECT_1_ADMIN, Permission.PROJECT_ADMIN);
        getClassTestContext().user(PROJECT_1_WRITE)
                .projectPermission(PROJECT_1, PROJECT_1_WRITE, Permission.PROJECT_WRITE);
        getClassTestContext().user(PROJECT_1_READ)
                .projectPermission(PROJECT_1, PROJECT_1_READ, Permission.PROJECT_READ);

        getClassTestContext().user(PROJECT_2_ADMIN)
                .projectPermission(PROJECT_2, PROJECT_2_ADMIN, Permission.PROJECT_ADMIN);
        getClassTestContext().user(PROJECT_2_WRITE)
                .projectPermission(PROJECT_2, PROJECT_2_WRITE, Permission.PROJECT_WRITE);
        getClassTestContext().user(PROJECT_2_READ)
                .projectPermission(PROJECT_2, PROJECT_2_READ, Permission.PROJECT_READ);

        getClassTestContext().user(REPOSITORY_1_ADMIN)
                .repositoryPermission(PROJECT_1, REPOSITORY_1, REPOSITORY_1_ADMIN, Permission.REPO_ADMIN);
        getClassTestContext().user(REPOSITORY_1_WRITE)
                .repositoryPermission(PROJECT_1, REPOSITORY_1, REPOSITORY_1_WRITE, Permission.REPO_WRITE);
        getClassTestContext().user(REPOSITORY_1_READ)
                .repositoryPermission(PROJECT_1, REPOSITORY_1, REPOSITORY_1_READ, Permission.REPO_READ);

        getClassTestContext().user(MULTI_REPOSITORY_READ)
                .repositoryPermission(PROJECT_1, REPOSITORY_1, MULTI_REPOSITORY_READ, Permission.REPO_READ)
                .repositoryPermission(PROJECT_2, REPOSITORY_2, MULTI_REPOSITORY_READ, Permission.REPO_READ);

        getClassTestContext().group(GROUP_1, REPOSITORY_1_ADMIN, REPOSITORY_1_WRITE, REPOSITORY_1_READ);
    }

    @Test
    public void testFindAllUsers() throws Exception {
        // user expectations based on alphabetical order (admin comes first, regular user last)
        RestUsersRequest userSearchRequest = new RestUsersRequest.Builder()
                .status(Response.Status.OK)
                .specification(expectDefaultPageOf(TOTAL_USER_COUNT))
                .specification(expectDefaultAdminAt(0))
                .specification(expectDefaultUserAt(TOTAL_USER_COUNT - 1))
                .build();

        restUsersHelper.get(userSearchRequest);
    }

    @Test
    public void testFindAllUsersPage() throws Exception {
        // user expectations based on alphabetical order
        ResponseSpecification pageExpectations = RestAssured.expect()
                .body("size", equalTo(4))
                .body("limit", equalTo(TOTAL_USER_COUNT))
                .body("isLastPage", equalTo(true))
                .body("start", equalTo(8));

        RestUsersRequest userSearchRequest = new RestUsersRequest.Builder()
                .page(8, TOTAL_USER_COUNT)
                .status(Response.Status.OK)
                .specification(pageExpectations)
                .specification(expectUserAt(0, REPOSITORY_1_ADMIN))
                .specification(expectUserAt(1, REPOSITORY_1_READ))
                .specification(expectUserAt(2, REPOSITORY_1_WRITE))
                .specification(expectDefaultUserAt(3))
                .build();

        restUsersHelper.get(userSearchRequest);
    }

    @Test
    public void testFindUsersByFilter() throws Exception {
        RestUsersRequest userSearchRequest = new RestUsersRequest.Builder()
                .filter("us")
                .status(Response.Status.OK)
                .specification(expectDefaultPageOf(1))
                .specification(expectDefaultUserAt(0))
                .build();

        restUsersHelper.get(userSearchRequest);
    }

    @Test
    public void testFindUsersByGroup() throws Exception {
        RestUsersRequest userSearchRequest = new RestUsersRequest.Builder()
                .group(GROUP_1)
                .status(Response.Status.OK)
                .specification(expectDefaultPageOf(3))
                .specification(expectUserAt(0, REPOSITORY_1_ADMIN))
                .specification(expectUserAt(1, REPOSITORY_1_READ))
                .specification(expectUserAt(2, REPOSITORY_1_WRITE))
                .build();

        restUsersHelper.get(userSearchRequest);
    }

    @Test
    public void testFindUsersByGroupAndFilter() throws Exception {
        RestUsersRequest userSearchRequest = new RestUsersRequest.Builder()
                .group(GROUP_1)
                .filter("read")
                .status(Response.Status.OK)
                .specification(expectDefaultPageOf(1))
                .specification(expectUserAt(0, REPOSITORY_1_READ))
                .build();

        restUsersHelper.get(userSearchRequest);
    }

    @Test
    public void testFindUsersByGroupAndPermission() throws Exception {
        RestUsersRequest userSearchRequest = new RestUsersRequest.Builder()
                .group(GROUP_1)
                .permission(Permission.REPO_ADMIN.name())
                .permissionResource("projectKey", PROJECT_1)
                .permissionResource("repositorySlug", REPOSITORY_1)
                .status(Response.Status.OK)
                .specification(expectDefaultPageOf(1))
                .specification(expectUserAt(0, REPOSITORY_1_ADMIN))
                .build();

        restUsersHelper.get(userSearchRequest);
    }

    @Test
    public void testFindUsersByGlobalPermission() throws Exception {
        RestUsersRequest userSearchRequest = new RestUsersRequest.Builder()
                .permission(Permission.ADMIN.name())
                .status(Response.Status.OK)
                .specification(expectDefaultPageOf(1))
                .specification(expectDefaultAdminAt(0))
                .build();

        restUsersHelper.get(userSearchRequest);
    }

    @Test
    public void testFindUsersByInvalidPermissionShouldFail() throws Exception {
        RestUsersRequest userSearchRequest = new RestUsersRequest.Builder()
                .permission("SOME_ADMIN")
                .status(Response.Status.BAD_REQUEST)
                .specification(expectError("permission", "SOME_ADMIN is not a valid permission."))
                .build();

        restUsersHelper.get(userSearchRequest);
    }

    @Test
    public void testFindUsersByProjectAdminPermissionUsingProjectKey() throws Exception {
        RestUsersRequest userSearchRequest = new RestUsersRequest.Builder()
                .permission(Permission.PROJECT_ADMIN.name())
                .permissionResource("projectKey", PROJECT_1)
                .status(Response.Status.OK)
                .specification(expectDefaultPageOf(2))
                .specification(expectDefaultAdminAt(0))
                .specification(expectUserAt(1, PROJECT_1_ADMIN))
                .build();

        restUsersHelper.get(userSearchRequest);
    }

    @Test
    public void testFindUsersByProjectReadPermissionUsingProjectKey() throws Exception {
        RestUsersRequest userSearchRequest = new RestUsersRequest.Builder()
                .permission(Permission.PROJECT_READ.name())
                .permissionResource("projectKey", PROJECT_1)
                .status(Response.Status.OK)
                .specification(expectDefaultPageOf(4))
                .specification(expectDefaultAdminAt(0))
                .specification(expectUserAt(1, PROJECT_1_ADMIN))
                .specification(expectUserAt(2, PROJECT_1_READ))
                .specification(expectUserAt(3, PROJECT_1_WRITE))
                .build();

        restUsersHelper.get(userSearchRequest);
    }

    @Test
    public void testFindUsersByProjectWritePermissionUsingProjectId() throws Exception {
        int project1Id = restProject1Helper.get().get("id");

        RestUsersRequest userSearchRequest = new RestUsersRequest.Builder()
                .permission(Permission.PROJECT_WRITE.name())
                .permissionResource("projectId", Integer.toString(project1Id))
                .status(Response.Status.OK)
                .specification(expectDefaultPageOf(3))
                .specification(expectDefaultAdminAt(0))
                .specification(expectUserAt(1, PROJECT_1_ADMIN))
                .specification(expectUserAt(2, PROJECT_1_WRITE))
                .build();

        restUsersHelper.get(userSearchRequest);
    }

    @Test
    public void testFindUsersByProjectPermissionWithInvalidProject() throws Exception {
        RestUsersRequest userSearchRequest = new RestUsersRequest.Builder()
                .permission(Permission.PROJECT_ADMIN.name())
                .permissionResource("projectKey", "NO_SUCH_PROJECT")
                .status(Response.Status.BAD_REQUEST)
                .specification(expectError("permission.projectKey", "Project NO_SUCH_PROJECT does not exist."))
                .build();

        restUsersHelper.get(userSearchRequest);
    }

    @Test
    public void testFindUsersByProjectPermissionWithNoProjectResource() throws Exception {
        RestUsersRequest userSearchRequest = new RestUsersRequest.Builder()
                .permission(Permission.PROJECT_ADMIN.name()) // no permission resource specified
                .status(Response.Status.BAD_REQUEST)
                .specification(expectError("permission", "Invalid project permission request: 'permission'. " +
                        "Either 'permission.projectId', or 'permission.projectKey' parameters should be provided."))
                .build();

        restUsersHelper.get(userSearchRequest);
    }

    @Test
    public void testFindUsersByProjectPermissionWithNotExistingProjectId() throws Exception {
        RestUsersRequest userSearchRequest = new RestUsersRequest.Builder()
                .permission(Permission.PROJECT_WRITE.name())
                .permissionResource("projectId", "1234")
                .status(Response.Status.BAD_REQUEST)
                .specification(expectError("permission.projectId", "Project with ID 1234 does not exist."))
                .build();

        restUsersHelper.get(userSearchRequest);
    }

    @Test
    public void testFindUsersByRepositoryAdminPermissionUsingSlug() throws Exception {
        RestUsersRequest userSearchRequest = new RestUsersRequest.Builder()
                .permission(Permission.REPO_ADMIN.name())
                .permissionResource("projectKey", PROJECT_1)
                .permissionResource("repositorySlug", REPOSITORY_1)
                .status(Response.Status.OK)
                .specification(expectDefaultPageOf(3))
                .specification(expectDefaultAdminAt(0))
                .specification(expectUserAt(1, PROJECT_1_ADMIN))
                .specification(expectUserAt(2, REPOSITORY_1_ADMIN))
                .build();

        restUsersHelper.get(userSearchRequest);
    }

    @Test
    public void testFindUsersByRepositoryReadPermissionUsingSlug() throws Exception {
        RestUsersRequest userSearchRequest = new RestUsersRequest.Builder()
                .permission(Permission.REPO_READ.name())
                .permissionResource("projectKey", PROJECT_1)
                .permissionResource("repositorySlug", REPOSITORY_1)
                .status(Response.Status.OK)
                .specification(expectDefaultPageOf(8))
                .specification(expectDefaultAdminAt(0))
                .specification(expectUserAt(1, MULTI_REPOSITORY_READ))
                .specification(expectUserAt(2, PROJECT_1_ADMIN))
                .specification(expectUserAt(3, PROJECT_1_READ))
                .specification(expectUserAt(4, PROJECT_1_WRITE))
                .specification(expectUserAt(5, REPOSITORY_1_ADMIN))
                .specification(expectUserAt(6, REPOSITORY_1_READ))
                .specification(expectUserAt(7, REPOSITORY_1_WRITE))
                .build();

        restUsersHelper.get(userSearchRequest);
    }

    @Test
    public void testFindUsersByRepositoryWritePermissionUsingRepositoryId() throws Exception {
        int repo1Id = restRepository1Helper.get().get("id");

        RestUsersRequest userSearchRequest = new RestUsersRequest.Builder()
                .permission(Permission.REPO_WRITE.name())
                .permissionResource("repositoryId", Integer.toString(repo1Id))
                .status(Response.Status.OK)
                .specification(expectDefaultPageOf(5))
                .specification(expectDefaultAdminAt(0))
                .specification(expectUserAt(1, PROJECT_1_ADMIN))
                .specification(expectUserAt(2, PROJECT_1_WRITE))
                .specification(expectUserAt(3, REPOSITORY_1_ADMIN))
                .specification(expectUserAt(4, REPOSITORY_1_WRITE))
                .build();

        restUsersHelper.get(userSearchRequest);
    }

    @Test
    public void testFindUsersByRepositoryPermissionWithInvalidRepository() throws Exception {
        RestUsersRequest userSearchRequest = new RestUsersRequest.Builder()
                .permission(Permission.REPO_ADMIN.name())
                .permissionResource("projectKey", PROJECT_1)
                .permissionResource("repositorySlug", "no_such_repo")
                .status(Response.Status.BAD_REQUEST)
                .specification(expectError("permission.repositorySlug",
                        "Repository " + PROJECT_1 + "/no_such_repo does not exist."))
                .build();

        restUsersHelper.get(userSearchRequest);
    }

    @Test
    public void testFindUsersByRepositoryPermissionWithNoRepositoryResource() throws Exception {
        RestUsersRequest userSearchRequest = new RestUsersRequest.Builder()
                .permission(Permission.REPO_ADMIN.name()) // no permission resource specified
                .status(Response.Status.BAD_REQUEST)
                .specification(expectError("permission", "Invalid repository permission request: 'permission'. " +
                        "Either 'permission.repositoryId', or 'permission.projectKey' and 'permission.repositorySlug' " +
                        "parameters should be provided."))
                .build();

        restUsersHelper.get(userSearchRequest);
    }

    @Test
    public void testFindUsersByRepositoryPermissionWithNotExistingRepositoryId() throws Exception {
        RestUsersRequest userSearchRequest = new RestUsersRequest.Builder()
                .permission(Permission.REPO_WRITE.name())
                .permissionResource("repositoryId", "1234")
                .status(Response.Status.BAD_REQUEST)
                .specification(expectError("permission.repositoryId", "Repository with ID 1234 does not exist."))
                .build();

        restUsersHelper.get(userSearchRequest);
    }

    @Test
    public void testFindUsersWithNameAndPermissionFilter() throws Exception {
        // should return users with write permission to rep_1
        RestUsersRequest userSearchRequest = new RestUsersRequest.Builder()
                .filter("write")
                .permission(Permission.REPO_READ.name())
                .permissionResource("projectKey", PROJECT_1)
                .permissionResource("repositorySlug", REPOSITORY_1)
                .status(Response.Status.OK)
                .specification(expectDefaultPageOf(2))
                .specification(expectUserAt(0, PROJECT_1_WRITE))
                .specification(expectUserAt(1, REPOSITORY_1_WRITE))
                .build();

        restUsersHelper.get(userSearchRequest);
    }

    @Test
    public void testFindUsersByMultiplePermissions() throws Exception {
        // 2 different repo_read permissions, only admin and multi_repo_user have them
        RestUsersRequest userSearchRequest = new RestUsersRequest.Builder()
                .permission(1, Permission.REPO_READ.name())
                .permissionResource(1, "projectKey", PROJECT_1)
                .permissionResource(1, "repositorySlug", REPOSITORY_1)
                .permission(2, Permission.REPO_READ.name())
                .permissionResource(2, "projectKey", PROJECT_2)
                .permissionResource(2, "repositorySlug", REPOSITORY_2)
                .status(Response.Status.OK)
                .specification(expectDefaultPageOf(2))
                .specification(expectDefaultAdminAt(0))
                .specification(expectUserAt(1, MULTI_REPOSITORY_READ))
                .build();

        restUsersHelper.get(userSearchRequest);
    }

    private static ResponseSpecification expectDefaultAdminAt(int position) {
        return expectUserAt(position, DefaultFuncTestData.getAdminUser(),
                "Administrator", "admin@example.com", true);
    }

    private static ResponseSpecification expectDefaultPageOf(int userCount) {
        return RestAssured.expect()
                .body("size", equalTo(userCount))
                .body("limit", equalTo(25))
                .body("isLastPage", equalTo(true))
                .body("start", equalTo(0));
    }

    private static ResponseSpecification expectDefaultUserAt(int position) {
        return expectUserAt(position, DefaultFuncTestData.getRegularUser(), "User", "user@example.com", true);
    }

    private static ResponseSpecification expectUserAt(int position, String username) {
        return expectUserAt(position, username, username, username + TestContext.DEFAULT_EMAIL_HOST, true);
    }

    private static ResponseSpecification expectUserAt(int position, String name, String displayName,
                                                      String emailAddress, boolean active) {
        String value = "values[" + position + "]";

        return RestAssured.expect()
                .body(value + ".name", equalTo(name))
                .body(value + ".displayName", equalTo(displayName))
                .body(value + ".emailAddress", equalTo(emailAddress))
                .body(value + ".active", equalTo(active))
                .body(value + ".links", notNullValue())
                .body(value + ".links.self.size()", equalTo(1))
                .body(value + ".links.self[0].href", endsWith("/users/" + name));
    }

    private static String usernameFor(String resource, String permission) {
        return resource.toLowerCase() + "_" + permission.toLowerCase();
    }
}
