package com.atlassian.bitbucket.internal.mirroring.mirror.auth;

import com.atlassian.bitbucket.auth.AuthenticationContext;
import com.atlassian.bitbucket.auth.HttpAuthenticationSuccessContext;
import com.atlassian.bitbucket.internal.mirroring.mirror.MirrorConstants;
import com.atlassian.bitbucket.internal.mirroring.mirror.MirroringUserUtils;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.request.RequestContext;
import com.atlassian.bitbucket.request.RequestManager;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.user.ServiceUser;
import com.atlassian.bitbucket.user.UserType;
import com.atlassian.jwt.JwtConstants;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Optional;

import static com.atlassian.bitbucket.internal.mirroring.mirror.MirrorConstants.ATTR_JWT_HIGHEST_PERM;
import static com.atlassian.bitbucket.internal.mirroring.mirror.MirrorConstants.ATTR_JWT_PAYLOAD;
import static com.atlassian.bitbucket.permission.Permission.LICENSED_USER;
import static com.atlassian.bitbucket.permission.Permission.SYS_ADMIN;
import static java.util.Optional.empty;
import static org.junit.Assert.*;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class DefaultMirrorAuthenticationContextTest {

    private static final String UPSTREAM_ID = "aabbccdd";

    @Mock
    private AuthenticationContext authenticationContext;
    @Mock
    private HttpServletRequest request;
    @Mock
    private RequestContext requestContext;
    @Mock
    private RequestManager requestManager;
    @Mock
    private HttpSession session;
    @InjectMocks
    private DefaultMirrorAuthenticationContext context;

    @Before
    public void setUp() {
        when(requestManager.getRequestContext()).thenReturn(requestContext);
        when(requestContext.getRawRequest()).thenReturn(request);
    }

    @Test
    public void testGetCurrentUpstreamUserUnauthenticated() {
        assertEquals(empty(), context.getCurrentUpstreamUser());
    }

    @Test
    public void testGetCurrentUpstreamUserAuthenticatedAsNormalUser() {
        mockAuthenticated(normalUser(1, "user"));

        assertEquals(empty(), context.getCurrentUpstreamUser());
    }

    @Test
    public void testGetCurrentUpstreamUserAuthenticatedAsNonMirrorServiceUser() {
        mockAuthenticated(serviceUser(1, "user", "bot"));

        assertEquals(empty(), context.getCurrentUpstreamUser());
    }

    @Test
    public void testGetCurrentUpstreamUserAuthenticatedAsUpstreamServer() {
        ServiceUser upstreamUser = serviceUser(1, MirroringUserUtils.getUpstreamUsernameFor(UPSTREAM_ID), "addon");
        mockAuthenticated(upstreamUser);

        Optional<ServiceUser> user = context.getCurrentUpstreamUser();
        assertNotEquals(empty(), user);
        assertSame(upstreamUser, user.get());
    }

    @Test
    public void testImpersonatedUserNotAuthenticatedButTokenSet() {
        mockJwtWithContextUserOnRequest("userKey", "username", "slug", "displayName", Permission.ADMIN);
        mockUsernameParameter("ignored");

        assertAnonymous();
    }

    @Test
    public void testImpersonatedUserNotAuthenticatedButParameter() {
        mockJwtWithContextUserOnRequest("userKey", "username", "slug", "displayName", Permission.ADMIN);
        mockUsernameParameter("ignored");

        assertAnonymous();
    }

    @Test
    public void testImpersonatedUserAuthenticatedAsNonUpstreamServerAndTokenSet() {
        mockAuthenticated(serviceUser(8, "other", "addon"));
        mockJwtWithContextUserOnRequest("userKey", "username", "slug", "displayName", Permission.ADMIN);
        mockUsernameParameter("ignored");

        assertAnonymous();
    }

    @Test
    public void testImpersonatedUserAuthenticatedAsUpstreamServerAndTokenSetOnSession() {
        mockJwtWithContextUserOnSession("99", "username", "slug", "displayName", SYS_ADMIN);
        mockUsernameParameter("ignored");

        doTestImpersonatedUserAuthenticatedAsUpstreamServerAndTokenSet(
                99, "username", "slug", "displayName", SYS_ADMIN);
    }

    @Test
    public void testImpersonatedUserAuthenticatedAsUpstreamServerAndTokenSetOnSessionAndRequest() {
        mockJwtWithContextUserOnSession("101", "username-1", "slug-1", "displayName-1", SYS_ADMIN);
        mockJwtWithContextUserOnRequest("99", "username", "slug", "displayName", SYS_ADMIN);
        mockUsernameParameter("ignored");

        // token on request should win
        doTestImpersonatedUserAuthenticatedAsUpstreamServerAndTokenSet(
                99, "username", "slug", "displayName", SYS_ADMIN);
    }

    @Test
    public void testImpersonatedUserAuthenticatedAsUpstreamServerAndTokenSetOnRequest() {
        mockJwtWithContextUserOnRequest("99", "username", "slug", "displayName", SYS_ADMIN);
        mockUsernameParameter("ignored");

        doTestImpersonatedUserAuthenticatedAsUpstreamServerAndTokenSet(
                99, "username", "slug", "displayName", SYS_ADMIN);
    }

    @Test
    public void testImpersonatedUserAuthenticatedAsUpstreamWithUrlParameter() {
        String user = "user";

        mockAuthenticated(serviceUser(8, MirroringUserUtils.getUpstreamUsernameFor(UPSTREAM_ID), "addon"));
        mockUsernameParameter(user);

        Optional<ApplicationUser> impersonatedUser = context.getImpersonatedUser();
        assertSimpleUser(user, impersonatedUser.get());
    }

    @Test
    public void testImpersonatedUserAuthenticatedAsUpstreamWithUrlWithNoSubstitution() {
        String user = MirrorConstants.PARAM_UPSTREAM_USERNAME_TEMPLATE;

        mockAuthenticated(serviceUser(8, MirroringUserUtils.getUpstreamUsernameFor(UPSTREAM_ID), "addon"));
        mockUsernameParameter(user);

        assertAnonymous();
    }

    @Test
    public void testImpersonatedUserAuthenticatedAsUpstreamWithEmptyUrlParameter() {
        String user = "";

        mockAuthenticated(serviceUser(8, MirroringUserUtils.getUpstreamUsernameFor(UPSTREAM_ID), "addon"));
        mockUsernameParameter(user);

        assertAnonymous();
    }

    @Test
    public void testImpersonatedUserAuthenticatedAsUpstreamWithEmptyBlankUrlParameters() {
        String user = "          ";

        mockAuthenticated(serviceUser(8, MirroringUserUtils.getUpstreamUsernameFor(UPSTREAM_ID), "addon"));
        mockUsernameParameter(user);

        assertAnonymous();
    }

    @Test
    public void testImpersonatedUserAuthenticatedAsUpstreamWithNoUserData() {
        mockAuthenticated(serviceUser(8, MirroringUserUtils.getUpstreamUsernameFor(UPSTREAM_ID), "addon"));

        assertAnonymous();
    }

    @Test
    public void testImpersonatedUserAuthenticatedAsUpstreamServerAndTokenWithNoUserSet() {
        mockAuthenticated(serviceUser(8, MirroringUserUtils.getUpstreamUsernameFor(UPSTREAM_ID), "addon"));
        mockJwtNoContextUser();

        assertAnonymous();
    }

    @Test
    public void testOnAuthenticationSuccessPersistsJwtPayload() throws ServletException, IOException {
        HttpAuthenticationSuccessContext successContext = mock(HttpAuthenticationSuccessContext.class);
        when(successContext.getRequest()).thenReturn(request);
        when(request.getSession(false)).thenReturn(session);
        mockJwtWithContextUserOnRequest("key", "name", "slug", "displayName", LICENSED_USER);

        context.onAuthenticationSuccess(successContext);

        verify(request).getSession(false);
        verify(session).setAttribute(eq(ATTR_JWT_PAYLOAD), anyString());
    }

    @Test
    public void testOnAuthenticationSuccessPersistsHighestPermission() throws ServletException, IOException {
        HttpAuthenticationSuccessContext successContext = mock(HttpAuthenticationSuccessContext.class);
        when(successContext.getRequest()).thenReturn(request);
        when(request.getSession(false)).thenReturn(session);
        when(request.getAttribute(ATTR_JWT_HIGHEST_PERM)).thenReturn(SYS_ADMIN);

        context.onAuthenticationSuccess(successContext);

        verify(request).getSession(false);
        verify(session).setAttribute(eq(ATTR_JWT_HIGHEST_PERM), same(SYS_ADMIN));
    }

    @Test
    public void testOnAuthenticationSuccessNoSession() throws ServletException, IOException {
        HttpAuthenticationSuccessContext successContext = mock(HttpAuthenticationSuccessContext.class);
        when(successContext.getRequest()).thenReturn(request);
        mockJwtWithContextUserOnRequest("key", "name", "slug", "displayName", Permission.ADMIN);

        context.onAuthenticationSuccess(successContext);

        verify(request).getSession(false);
        verify(session, never()).setAttribute(eq(ATTR_JWT_PAYLOAD), anyString());
        verify(session, never()).setAttribute(eq(MirrorConstants.ATTR_JWT_HIGHEST_PERM), anyObject());
    }

    @Test
    public void testOnAuthenticationSuccessNoJwt() throws ServletException, IOException {
        HttpAuthenticationSuccessContext successContext = mock(HttpAuthenticationSuccessContext.class);
        when(successContext.getRequest()).thenReturn(request);
        when(request.getSession(false)).thenReturn(session);

        context.onAuthenticationSuccess(successContext);

        verify(request, never()).getSession(false);
        verify(session, never()).setAttribute(eq(ATTR_JWT_PAYLOAD), anyString());
        verify(session, never()).setAttribute(eq(MirrorConstants.ATTR_JWT_HIGHEST_PERM), anyObject());
    }

    private void assertAnonymous() {
        assertEquals(empty(), context.getImpersonatedUser());
        assertEquals(empty(), context.getImpersonatedUserHighestPermission());
    }

    private void assertSimpleUser(String name, ApplicationUser user) {

        assertEquals(-1, user.getId());
        assertEquals(name, user.getName());
        assertEquals(name, user.getDisplayName());
        assertEquals(name, user.getSlug());
        assertNull(user.getEmailAddress());

        assertFalse(context.getImpersonatedUserHighestPermission().isPresent());
    }

    private void doTestImpersonatedUserAuthenticatedAsUpstreamServerAndTokenSet(
            int id, String username, String slug, String displayName, Permission permission) {
        mockAuthenticated(serviceUser(8, MirroringUserUtils.getUpstreamUsernameFor(UPSTREAM_ID), "addon"));

        Optional<ApplicationUser> optUser = context.getImpersonatedUser();
        assertTrue(optUser.isPresent());
        ApplicationUser user = optUser.get();
        assertEquals(id, user.getId());
        assertEquals(username, user.getName());
        assertEquals(slug, user.getSlug());
        assertEquals(displayName, user.getDisplayName());
        assertEquals(UserType.NORMAL, user.getType());

        Optional<Permission> optPermission = context.getImpersonatedUserHighestPermission();
        assertTrue(optPermission.isPresent());
        assertEquals(permission, optPermission.get());
    }

    private static String getJwtJson(String userKey, String username, String slug, String displayName, Permission permission) {
        return "{\n" +
                "    \"iss\": \"issuer\",\n" +
                "    \"iat\": 1300819370,\n" +
                "    \"exp\": 1300819380,\n" +
                "    \"qsh\": \"8063ff4ca1e41df7bc90c8ab6d0f6207d491cf6dad7c66ea797b4614b71922e9\",\n" +
                "    \"sub\": \"batman\",\n" +
                "    \"context\": {\n" +
                "        \"user\": {\n" +
                "            \"userKey\": \"" + userKey + "\",\n" +
                "            \"username\": \"" + username + "\",\n" +
                "            \"displayName\": \"" + displayName + "\",\n" +
                "            \"slug\": \"" + slug + "\",\n" +
                "            \"highestPermission\": \"" + permission.name() + "\"\n" +
                "        }\n" +
                "    }\n" +
                "}";
    }

    private void mockAuthenticated(ApplicationUser user) {
        when(authenticationContext.isAuthenticated()).thenReturn(true);
        when(authenticationContext.getCurrentUser()).thenReturn(user);
    }

    private void mockUsernameParameter(String user) {
        when(request.getParameter("upstream-username")).thenReturn(user);
    }

    private void mockJwtWithContextUserOnRequest(String userKey, String username, String slug, String displayName,
                                                 Permission permission) {
        String jwtJson = getJwtJson(userKey, username, slug, displayName, permission);
        when(request.getAttribute(JwtConstants.HttpRequests.JWT_JSON_PAYLOAD_ATTRIBUTE_NAME)).thenReturn(jwtJson);
    }

    private void mockJwtWithContextUserOnSession(String userKey, String username, String slug, String displayName,
                                                 Permission permission) {
        String jwtJson = getJwtJson(userKey, username, slug, displayName, permission);
        when(request.getAttribute(JwtConstants.HttpRequests.JWT_JSON_PAYLOAD_ATTRIBUTE_NAME)).thenReturn(jwtJson);
    }

    private void mockJwtNoContextUser() {
        String jwtJson = "{\n" +
                "    \"iss\": \"issuer\",\n" +
                "    \"iat\": 1300819370,\n" +
                "    \"exp\": 1300819380,\n" +
                "    \"qsh\": \"8063ff4ca1e41df7bc90c8ab6d0f6207d491cf6dad7c66ea797b4614b71922e9\"\n" +
                "}";
        when(request.getAttribute(JwtConstants.HttpRequests.JWT_JSON_PAYLOAD_ATTRIBUTE_NAME)).thenReturn(jwtJson);
    }

    private ApplicationUser normalUser(int id, String name) {
        ApplicationUser user = mock(ApplicationUser.class);
        when(user.getType()).thenReturn(UserType.NORMAL);
        when(user.getId()).thenReturn(id);
        when(user.getName()).thenReturn(name);
        return user;
    }

    private ServiceUser serviceUser(int id, String name, String label) {
        ServiceUser user = mock(ServiceUser.class);
        when(user.getType()).thenReturn(UserType.SERVICE);
        when(user.getId()).thenReturn(id);
        when(user.getName()).thenReturn(name);
        when(user.getLabel()).thenReturn(label);
        return user;
    }
}