package com.atlassian.bitbucket.internal.ssh.servlet;

import com.atlassian.bitbucket.AuthorisationException;
import com.atlassian.bitbucket.auth.AuthenticationContext;
import com.atlassian.bitbucket.i18n.SimpleI18nService;
import com.atlassian.bitbucket.internal.ssh.InternalSshKeyService;
import com.atlassian.bitbucket.nav.NavBuilder;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.permission.PermissionValidationService;
import com.atlassian.bitbucket.project.Project;
import com.atlassian.bitbucket.project.ProjectService;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.repository.RepositoryService;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.user.TestApplicationUser;
import com.atlassian.bitbucket.user.UserService;
import com.atlassian.sal.api.auth.AuthenticationController;
import com.atlassian.sal.api.auth.LoginUriProvider;
import com.atlassian.sal.api.xsrf.XsrfTokenValidator;
import com.atlassian.stash.internal.project.InternalNormalProject;
import com.atlassian.stash.internal.project.InternalProject;
import com.atlassian.stash.internal.repository.InternalRepository;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.net.URI;

import static com.atlassian.bitbucket.internal.ssh.servlet.SshKeyForm.KEY_READ_ONLY;
import static org.junit.Assert.*;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willThrow;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.class)
public class SshKeysControllerTest {
    private static final ApplicationUser USER = new TestApplicationUser("alice");
    private static final ApplicationUser OTHER_USER = new TestApplicationUser("bob");
    private static final Project PROJECT = new InternalNormalProject.Builder().id(888).name("project").key("projectKey").build();
    private static final Repository REPOSITORY = new InternalRepository.Builder().id(666).name("repo").project((InternalProject) PROJECT).build();

    private static final String TEXT = "AAAAB3NzaC1yc2EAAAABIwAAAQEAwQ6EygyBdZwLgHxt2CDXypmlZyOtfWwPTMMTfX29PmLqcGrsLJmXY/A5RawENKRy9"
            + "saaPPgsyml8dvnjSDqY1H5s7P3K62twM9d0cbME1nJc8xTt8krCQqHUW4UZ4rRfjYTH7XoF89LmBxHPDWIz+k9JAgMutHPr6S4k"
            + "cN6bCnuX/tA3wArAJ7+EXLRZATKUAm2eMPJxRWA9N7WQ5KUHfY+KjzXwv0xdVBNnO6pLpb9V4Ht9vn3LKrEFnb/1SWnh5hdjTnn"
            + "S7VKj+qUYNh4mEt+Yvi3UchtR3cLbRhC1BpTtbmRu+AccUTnWgRIXelRXXwnb7GPs8DvtvxHvXxv0YQ==";

    private static final String LIST_KEYS_IN_USER_ADMIN_URL = "/admin/list/keys/for/specific/user";
    private static final String LIST_KEYS_IN_USER_PROFILE_URL = "/ssh/list/keys/for/current/user";
    private static final String LIST_KEYS_IN_PROJECT_SETTINGS = "/ssh/list/keys/for/project";
    private static final String LIST_KEYS_IN_REPO_SETTINGS = "/ssh/list/keys/for/repo";

    @Mock
    private HttpServletRequest request;
    @Mock
    private HttpServletResponse response;
    
    @Mock
    private AuthenticationContext authenticationContext;
    @Mock
    private AuthenticationController authenticationController;
    @Mock
    private InternalSshKeyService keyService;
    @Mock
    private LoginUriProvider loginUriProvider;
    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
    private NavBuilder navBuilder;
    @Mock
    private PermissionValidationService permissionValidationService;
    @Mock
    private ProjectService projectService;
    @Mock
    private RepositoryService repositoryService;
    @Mock
    private UserService userService;
    @Mock
    private XsrfTokenValidator xsrfTokenValidator;
    @Mock
    private AddKeyRequestHandler addKeyRequestHandler;
    @Mock
    private ViewKeysRequestHandler viewKeysRequestHandler;
    @Mock
    private NotFoundHandler notFoundHandler;
    
    private SshKeysController controller;

    @Before
    public void setup() {
        controller = new SshKeysController(authenticationContext, authenticationController,
                new SimpleI18nService(SimpleI18nService.Mode.RETURN_KEYS), keyService,
                loginUriProvider, navBuilder, permissionValidationService, projectService, repositoryService,
                userService, xsrfTokenValidator, addKeyRequestHandler, notFoundHandler, viewKeysRequestHandler);

        given(authenticationContext.isAuthenticated()).willReturn(true);
        given(authenticationContext.getCurrentUser()).willReturn(USER);
        given(navBuilder.admin().users().view(USER.getSlug()).listSshKeys().buildRelative()).willReturn(LIST_KEYS_IN_USER_ADMIN_URL);
        given(navBuilder.pluginServlets().path("ssh", "account", "keys").buildRelative()).willReturn(LIST_KEYS_IN_USER_PROFILE_URL);
        given(navBuilder.pluginServlets().path("ssh", "keys", "projects", PROJECT.getKey()).buildRelative()).willReturn(LIST_KEYS_IN_PROJECT_SETTINGS);
        given(navBuilder.pluginServlets().path("ssh", "keys", "projects", PROJECT.getKey(), "repos", REPOSITORY.getSlug()).buildRelative()).willReturn(LIST_KEYS_IN_REPO_SETTINGS);
        given(userService.getUserBySlug(USER.getSlug())).willReturn(USER);
        given(userService.getUserBySlug(OTHER_USER.getSlug())).willReturn(OTHER_USER);
        given(projectService.getByKey(PROJECT.getKey())).willReturn(PROJECT);
        given(repositoryService.getBySlug(PROJECT.getKey(), REPOSITORY.getSlug())).willReturn(REPOSITORY);
        given(xsrfTokenValidator.validateFormEncodedToken(request)).willReturn(true);
    }

    @Test
    public void should401OnGetIfNoAccessToProject() throws Exception {
        prepareRequest("GET", "/projects/" + PROJECT.getKey() + "/keys");
        willThrow(AuthorisationException.class).given(permissionValidationService).validateForProject(PROJECT, Permission.PROJECT_ADMIN);

        controller.service(request, response);
        verify(response).sendError(eq(HttpServletResponse.SC_UNAUTHORIZED), anyString());
    }

    @Test
    public void should401OnGetIfNoAccessToRepository() throws Exception {
        prepareRequest("GET", "/projects/" + PROJECT.getKey() + "/repos/" + REPOSITORY.getSlug() + "/keys");
        willThrow(AuthorisationException.class).given(permissionValidationService).validateForRepository(REPOSITORY, Permission.REPO_ADMIN);

        controller.service(request, response);
        verify(response).sendError(eq(HttpServletResponse.SC_UNAUTHORIZED), anyString());
    }

    @Test
    public void should404OnGetIfProjectNotFound() throws Exception {
        prepareRequest("GET", "/projects/unknown/keys");

        controller.service(request, response);

        verify(response).sendError(eq(HttpServletResponse.SC_NOT_FOUND), anyString());
    }

    @Test
    public void should401OnGetIfAnonymousUserAndProjectNotFound() throws Exception {
        // Don't leak which projects do/don't exist with 401 vs 404 errors
        willThrow(AuthorisationException.class).given(permissionValidationService).validateAuthenticated();
        prepareRequest("GET", "/projects/unknown/keys");

        controller.service(request, response);

        verify(response).sendError(eq(HttpServletResponse.SC_UNAUTHORIZED), anyString());
    }

    @Test
    public void should401OnGetIfUserInsufficientPermissions() throws Exception {
        prepareRequest("GET", "/admin/users/" + USER.getSlug() + "/keys");
        given(keyService.canEditSshKeyForUser(USER)).willReturn(false);

        controller.service(request, response);

        verify(response).sendError(eq(HttpServletResponse.SC_UNAUTHORIZED), anyString());
    }

    @Test
    public void should404OnGetIfRepositoryNotFound() throws Exception {
        prepareRequest("GET", "/projects/" + PROJECT.getKey() + "/repos/unknown/keys");

        controller.service(request, response);

        verify(response).sendError(eq(HttpServletResponse.SC_NOT_FOUND), anyString());
    }

    @Test
    public void should404OnGetIfUserNotFound() throws Exception {
        prepareRequest("GET", "/admin/users/unknown/keys");

        controller.service(request, response);

        verify(response).sendError(eq(HttpServletResponse.SC_NOT_FOUND), anyString());
    }

    @Test
    public void should401OnGetIfAnonymousUserAndRepositoryNotFound() throws Exception {
        // Don't leak which repositories do/don't exist with 401 vs 404 errors
        willThrow(AuthorisationException.class).given(permissionValidationService).validateAuthenticated();
        prepareRequest("GET", "/projects/" + PROJECT.getKey() + "/repos/unknown/keys");

        controller.service(request, response);

        verify(response).sendError(eq(HttpServletResponse.SC_UNAUTHORIZED), anyString());
    }

    @Test
    public void shouldDelegateToNotFoundHandlerOnPostIfRequiredParamsMissing() throws Exception {
        prepareRequest("POST");
        controller.service(request, response);

        verify(notFoundHandler).post(null, request, response);
    }

    @Test
    public void should401OnPostIfNoAccessToProject() throws Exception {
        prepareRequest("POST", "/projects/" + PROJECT.getKey() + "/keys");
        willThrow(AuthorisationException.class).given(permissionValidationService).validateForProject(PROJECT, Permission.PROJECT_ADMIN);

        controller.service(request, response);
        verify(response).sendError(eq(HttpServletResponse.SC_UNAUTHORIZED), anyString());
    }

    @Test
    public void should401OnPostIfNoAccessToRepository() throws Exception {
        prepareRequest("POST", "/projects/" + PROJECT.getKey() + "/repos/" + REPOSITORY.getSlug() + "/keys");
        willThrow(AuthorisationException.class).given(permissionValidationService).validateForRepository(REPOSITORY, Permission.REPO_ADMIN);

        controller.service(request, response);
        verify(response).sendError(eq(HttpServletResponse.SC_UNAUTHORIZED), anyString());
    }

    @Test
    public void should401OnPostIfUserInsufficientPermissions() throws Exception {
        prepareRequest("POST", "/admin/users/" + USER.getSlug() + "/keys");
        given(keyService.canEditSshKeyForUser(USER)).willReturn(false);

        controller.service(request, response);

        verify(response).sendError(eq(HttpServletResponse.SC_UNAUTHORIZED), anyString());
    }

    @Test
    public void should404OnPostIfProjectNotFound() throws Exception {
        prepareRequest("POST", "/projects/" + PROJECT.getKey() + "/keys");
        given(projectService.getByKey(PROJECT.getKey())).willReturn(null);

        controller.service(request, response);

        verify(response).sendError(eq(HttpServletResponse.SC_NOT_FOUND), anyString());
    }

    @Test
    public void should404OnPostIfRepositoryNotFound() throws Exception {
        prepareRequest("POST", "/projects/" + PROJECT.getKey() + "/repos/" + REPOSITORY.getSlug() + "/keys");
        given(repositoryService.getBySlug(PROJECT.getKey(), REPOSITORY.getSlug())).willReturn(null);

        controller.service(request, response);

        verify(response).sendError(eq(HttpServletResponse.SC_NOT_FOUND), anyString());
    }

    @Test
    public void shouldRedirectToLoginWhenNotAuthenticated() throws Exception {
        String requestUri = "/projects/" + PROJECT.getKey() + "/keys";
        URI loginURI = new URI("/login");

        prepareRequest("POST");
        when(request.getRequestURI()).thenReturn(requestUri);

        given(authenticationController.shouldAttemptAuthentication(request)).willReturn(true);
        given(loginUriProvider.getLoginUri(new URI(requestUri))).willReturn(loginURI);

        controller.service(request, response);

        verify(response).sendRedirect("/login");
    }

    @Test
    public void shouldDelegateToAddHandlerOnRepositoryPost() throws Exception {
        prepareRequest("POST", "/projects/" + PROJECT.getKey() + "/repos/" + REPOSITORY.getSlug() + "/keys/add");
        when(request.getParameter(eq("text"))).thenReturn("ssh-rsa " + TEXT + " The label");

        controller.service(request, response);

        ArgumentCaptor<SshKeysRequest> reqCaptor = ArgumentCaptor.forClass(SshKeysRequest.class);
        verify(addKeyRequestHandler).post(reqCaptor.capture(), eq(request), eq(response));
        SshKeysRequest req = reqCaptor.getValue();

        assertEquals(REPOSITORY, req.getRepository());
        assertNull(req.getProject());
        assertNull(req.getUser());
        assertFalse(req.isAdmin());
        SshKeyForm form = req.getForm();
        assertNotNull(form);
        assertEquals("ssh-rsa " + TEXT + " The label", form.getText());
        assertEquals("The label", form.getLabel());
    }

    @Test
    public void shouldDelegateToAddHandlerOnProjectGet() throws Exception {
        prepareRequest("GET", "/projects/" + PROJECT.getKey() + "/keys/add");

        controller.service(request, response);

        ArgumentCaptor<SshKeysRequest> reqCaptor = ArgumentCaptor.forClass(SshKeysRequest.class);
        verify(addKeyRequestHandler).get(reqCaptor.capture(), eq(request), eq(response));
        SshKeysRequest req = reqCaptor.getValue();

        assertEquals(PROJECT, req.getProject());
        assertNull(req.getRepository());
        assertNull(req.getUser());
        assertFalse(req.isAdmin());
        SshKeyForm form = req.getForm();
        assertTrue(form.size() == 1);
        assertSame(form.get(KEY_READ_ONLY), Boolean.TRUE);
    }

    @Test
    public void shouldDelegateToViewHandlerOnUserProfileGet() throws Exception {
        prepareRequest("GET", "/account/keys/add");

        controller.service(request, response);

        ArgumentCaptor<SshKeysRequest> reqCaptor = ArgumentCaptor.forClass(SshKeysRequest.class);
        verify(addKeyRequestHandler).get(reqCaptor.capture(), eq(request), eq(response));
        SshKeysRequest req = reqCaptor.getValue();

        assertNull(req.getProject());
        assertNull(req.getRepository());
        assertEquals(USER, req.getUser());
        assertFalse(req.isAdmin());
        SshKeyForm form = req.getForm();
        assertTrue(form.size() == 1);
        assertSame(form.get(KEY_READ_ONLY), Boolean.TRUE);
    }

    @Test
    public void shouldDelegateToViewHandlerOnUserAdminGet() throws Exception {
        prepareRequest("GET", "/admin/users/" + OTHER_USER.getSlug() + "/keys/add");
        given(keyService.canEditSshKeyForUser(OTHER_USER)).willReturn(true);

        controller.service(request, response);

        ArgumentCaptor<SshKeysRequest> reqCaptor = ArgumentCaptor.forClass(SshKeysRequest.class);
        verify(addKeyRequestHandler).get(reqCaptor.capture(), eq(request), eq(response));
        SshKeysRequest req = reqCaptor.getValue();

        assertNull(req.getProject());
        assertNull(req.getRepository());
        assertEquals(OTHER_USER, req.getUser());
        assertTrue(req.isAdmin());
        SshKeyForm form = req.getForm();
        assertTrue(form.size() == 1);
        assertSame(form.get(KEY_READ_ONLY), Boolean.TRUE);
    }

    private void prepareRequest(String method) {
        prepareRequest(method, null);
    }

    private void prepareRequest(String method, String pathInfo) {
        when(request.getMethod()).thenReturn(method);
        when(request.getPathInfo()).thenReturn(pathInfo);
    }
}
