package com.atlassian.bitbucket.internal.crowd.sso;

import com.atlassian.bitbucket.auth.*;
import com.atlassian.bitbucket.crowd.sso.CrowdSsoService;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.user.UserService;
import com.atlassian.crowd.exception.*;
import com.atlassian.crowd.integration.http.CrowdHttpAuthenticator;
import com.atlassian.crowd.model.user.User;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.i18n.SimpleI18nService;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import static org.junit.Assert.*;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.same;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class CrowdSsoAuthenticationHandlerTest {

    private static final String SSO_COOKIE = "cookie";
    private static final String PASSWORD = "credentials";
    private static final String USERNAME = "user";

    @Mock
    private CrowdHttpAuthenticator authenticator;
    @Mock
    private HttpAuthenticationContext context;
    @Mock
    private CrowdSsoService crowdSsoService;
    @Mock
    private User crowdUser;
    @InjectMocks
    private CrowdSsoAuthenticationHandler handler;
    @Spy
    private I18nService i18nService = new SimpleI18nService();
    @Mock
    private HttpServletRequest request;
    @Mock
    private HttpServletResponse response;
    @Mock
    private HttpSession session;
    @Mock
    private ApplicationUser user;
    @Mock
    private UserService userService;

    @Before
    public void setup() {
        when(context.getRequest()).thenReturn(request);
        when(context.getResponse()).thenReturn(response);
        when(request.getSession()).thenReturn(session);
        when(request.getSession(anyBoolean())).thenReturn(session);
        when(crowdUser.getName()).thenReturn(USERNAME);
        when(user.getName()).thenReturn(USERNAME);
    }

    @Test
    public void testAuthenticateBasicAuth() throws Exception {
        mockSsoState(true, true, true, true);
        when(context.getMethod()).thenReturn(HttpAuthenticationContext.METHOD_BASIC);

        assertNull(handler.authenticate(context));
    }

    @Test
    public void testAuthenticateNonSsoUser() throws Exception {
        mockSsoState(true, false, true, true);
        mockFormAuth();

        assertNull(handler.authenticate(context));
    }

    @Test
    public void testAuthenticateFormSsoDisabled() throws Exception {
        mockSsoState(false, true, true, true);
        mockFormAuth();

        assertNull(handler.authenticate(context));
    }

    @Test(expected = InactiveUserAuthenticationException.class)
    public void testAuthenticateFormFailedAccessDenied() throws Exception {
        mockSsoState(true, true, true, false);
        mockFormAuth();
        when(authenticator.authenticate(same(request), same(response), eq(USERNAME), eq(PASSWORD)))
                .thenThrow(new ApplicationAccessDeniedException("ERROR"));

        // handler should opt out of authentication if it cannot authenticate remotely
        assertNull(handler.authenticate(context));
    }

    @Test
    public void testAuthenticateFormFailedCrowdMisconfigured() throws Exception {
        mockSsoState(true, true, true, false);
        mockFormAuth();
        when(authenticator.authenticate(same(request), same(response), eq(USERNAME), eq(PASSWORD)))
                .thenThrow(new ApplicationPermissionException("ERROR"));

        // handler should opt out of authentication if it cannot authenticate remotely
        assertNull(handler.authenticate(context));
    }

    @Test
    public void testAuthenticateFormFailedCrowdUnavailable() throws Exception {
        mockSsoState(true, true, true, false);
        mockFormAuth();
        when(authenticator.authenticate(same(request), same(response), eq(USERNAME), eq(PASSWORD)))
                .thenThrow(new OperationFailedException("ERROR"));

        // handler should opt out of authentication if it cannot authenticate remotely
        assertNull(handler.authenticate(context));
    }

    @Test(expected = ExpiredPasswordAuthenticationException.class)
    public void testAuthenticateFormFailedPasswordExpired() throws Exception {
        mockSsoState(true, true, true, false);
        mockFormAuth();
        when(authenticator.authenticate(same(request), same(response), eq(USERNAME), eq(PASSWORD)))
                .thenThrow(new ExpiredCredentialException("ERROR"));

        handler.authenticate(context);
    }


    @Test(expected = InactiveUserAuthenticationException.class)
    public void testAuthenticateFormFailedUserDisabled() throws Exception {
        mockSsoState(true, true, true, false);
        mockFormAuth();
        when(authenticator.authenticate(same(request), same(response), eq(USERNAME), eq(PASSWORD)))
                .thenThrow(new InactiveAccountException("ERROR"));

        handler.authenticate(context);
    }

    @Test(expected = IncorrectPasswordAuthenticationException.class)
    public void testAuthenticateFormFailedWrongCredentials() throws Exception {
        mockSsoState(true, true, true, false);
        mockFormAuth();
        when(authenticator.authenticate(same(request), same(response), eq(USERNAME), eq(PASSWORD)))
                .thenThrow(new InvalidAuthenticationException("ERROR"));

        handler.authenticate(context);
    }

    @Test
    public void testAuthenticateFormSuccess() throws Exception {
        mockSsoState(true, true, true, false);
        mockFormAuth();
        when(authenticator.authenticate(same(request), same(response), eq(USERNAME), eq(PASSWORD)))
                .thenReturn(crowdUser);
        when(userService.getUserByName(eq(USERNAME))).thenReturn(user);

        assertSame(user, handler.authenticate(context));
    }

    @Test
    public void testAuthenticateTokenForEmptyPassword() throws Exception {
        mockSsoState(true, true, true, true);
        mockTokenAuth();

        when(context.getCredentials()).thenReturn("");
        when(context.getUsername()).thenReturn("jdoe");

        assertNull(handler.authenticate(context));

        verify(authenticator).getToken(same(request));
    }

    @Test
    public void testAuthenticateTokenForBlankUsername() throws Exception {
        mockSsoState(true, true, true, true);
        mockTokenAuth();

        when(context.getCredentials()).thenReturn("credentials");
        when(context.getUsername()).thenReturn("\t\n    ");

        assertNull(handler.authenticate(context));

        verify(authenticator).getToken(same(request));
    }

    @Test
    public void testAuthenticateTokenForEmptyUsername() throws Exception {
        mockSsoState(true, true, true, true);
        mockTokenAuth();

        when(context.getCredentials()).thenReturn("credentials");
        when(context.getUsername()).thenReturn("");

        assertNull(handler.authenticate(context));

        verify(authenticator).getToken(same(request));
    }

    @Test
    public void testAuthenticateTokenFailedInvalidToken() throws Exception {
        mockSsoState(true, true, true, true);
        mockTokenAuth();

        when(authenticator.getUser(same(request)))
                .thenThrow(new InvalidTokenException("ERROR"));

        assertNull(handler.authenticate(context));
    }

    @Test
    public void testAuthenticateTokenFailedRemoteUserUnknownLocally() throws Exception {
        mockSsoState(true, true, true, true);
        mockTokenAuth();

        when(authenticator.getUser(same(request))).thenReturn(crowdUser);

        assertNull(handler.authenticate(context));
    }

    @Test
    public void testAuthenticateTokenSuccess() throws Exception {
        mockSsoState(true, true, true, true);
        mockTokenAuth();

        when(authenticator.getUser(same(request))).thenReturn(crowdUser);
        when(authenticator.isAuthenticated(request, response)).thenReturn(true);
        when(userService.getUserByName(eq(USERNAME))).thenReturn(user);

        assertSame(user, handler.authenticate(context));
    }

    @Test
    public void testAuthenticateTokenFailureIfCrowdFailsAuthentication() throws Exception {
        mockSsoState(true, true, true, true);
        mockTokenAuth();

        when(authenticator.getUser(same(request))).thenReturn(crowdUser);
        when(authenticator.isAuthenticated(request, response)).thenReturn(false);
        when(userService.getUserByName(eq(USERNAME))).thenReturn(user);

        assertNull(handler.authenticate(context));
    }

    @Test
    public void testLogoutNonSsoUserSsoEnabled() throws Exception {
        mockSsoState(true, true, true, true);

        handler.logout(user, request, response);

        verify(session, never()).removeAttribute(eq(CrowdSsoAuthenticationHandler.KEY_CROWD_SSO_AUTHENTICATED));
        verify(authenticator, never()).logout(same(request), same(response));
    }

    @Test
    public void testLogoutSsoUser() throws Exception {
        mockSsoState(true, true, true, true);
        mockAuthenticatedByHandler();

        handler.logout(user, request, response);

        verify(session).removeAttribute(eq(CrowdSsoAuthenticationHandler.KEY_CROWD_SSO_AUTHENTICATED));
        verify(authenticator).logout(same(request), same(response));
    }

    @Test
    public void testLogoutSsoUserPermissionException() throws Exception {
        mockSsoState(true, true, true, true);
        mockAuthenticatedByHandler();
        doThrow(new ApplicationPermissionException("")).when(authenticator).logout(same(request), same(response));

        handler.logout(user, request, response);

        verify(session).removeAttribute(eq(CrowdSsoAuthenticationHandler.KEY_CROWD_SSO_AUTHENTICATED));
        verify(authenticator).logout(same(request), same(response));
    }

    @Test
    public void testLogoutSsoUserCrowdException() throws Exception {
        mockSsoState(true, true, true, true);
        mockAuthenticatedByHandler();
        doThrow(new OperationFailedException("")).when(authenticator).logout(same(request), same(response));

        handler.logout(user, request, response);

        verify(session).removeAttribute(eq(CrowdSsoAuthenticationHandler.KEY_CROWD_SSO_AUTHENTICATED));
        verify(authenticator).logout(same(request), same(response));
    }

    @Test
    public void testValidateNoSession() throws Exception {
        mockSsoState(true, true, true, true);
        reset(request);

        handler.validateAuthentication(context);

        verify(authenticator, never()).isAuthenticated(same(request), same(response));
    }

    @Test(expected = ExpiredAuthenticationException.class)
    public void testValidateNoToken() throws Exception {
        mockSsoState(true, true, false, true);
        mockAuthenticatedByHandler();

        handler.validateAuthentication(context);
    }

    @Test
    public void testValidateNonSsoUser() throws Exception {
        mockSsoState(true, true, true, true);
        handler.validateAuthentication(context);

        verify(authenticator, never()).isAuthenticated(same(request), same(response));
    }

    @Test(expected = ExpiredAuthenticationException.class)
    public void testValidateSsoUserSsoUnavailable() throws Exception {
        // tests the scenario where a user was logged in through SSO and the SSO directory is removed
        mockSsoState(true, true, true, true);
        mockAuthenticatedByHandler();

        when(authenticator.isAuthenticated(same(request), same(response))).thenThrow(new OperationFailedException(""));

        handler.validateAuthentication(context);
    }

    @Test(expected = ExpiredAuthenticationException.class)
    public void testValidateSsoUserSsoDisabled() throws Exception {
        // tests the scenario where a user was logged in through SSO and the SSO directory is removed
        mockSsoState(false, true, true, true);
        mockAuthenticatedByHandler();

        handler.validateAuthentication(context);
    }

    @Test(expected = ExpiredAuthenticationException.class)
    public void testValidateSsoUserRemoteSessionExpired() throws Exception {
        mockSsoState(true, true, true, true);
        mockAuthenticatedByHandler();

        when(authenticator.isAuthenticated(same(request), same(response))).thenReturn(false);

        handler.validateAuthentication(context);
    }

    @Test
    public void testValidateSsoUserRemoteSessionExpiredShouldLogoutUserFromCrowd() throws Exception {
        mockSsoState(true, true, true, true);
        mockAuthenticatedByHandler();
        when(authenticator.isAuthenticated(same(request), same(response))).thenReturn(false);

        try {
            handler.validateAuthentication(context);
            fail();
        } catch (ExpiredAuthenticationException e) {
            assertTrue(true);
        }

        verify(authenticator).logout(same(request), same(response));
    }

    @Test
    public void testValidateSsoUser() throws Exception {
        mockSsoState(true, true, true, true);
        mockAuthenticatedByHandler();

        when(authenticator.isAuthenticated(same(request), same(response))).thenReturn(true);

        handler.validateAuthentication(context);

        verify(authenticator).isAuthenticated(same(request), same(response));
    }

    private void mockAuthenticatedByHandler() {
        when(session.getAttribute(eq(CrowdSsoAuthenticationHandler.KEY_CROWD_SSO_AUTHENTICATED))).thenReturn(SSO_COOKIE);
    }

    private void mockFormAuth() {
        when(context.getMethod()).thenReturn(HttpAuthenticationContext.METHOD_FORM);
        when(context.getUsername()).thenReturn(USERNAME);
        when(context.getCredentials()).thenReturn(PASSWORD);
    }

    private void mockTokenAuth() {
        when(context.getMethod()).thenReturn(HttpAuthenticationContext.METHOD_TOKEN);
    }

    private void mockSsoState(boolean enabled, boolean userManagedBySso, boolean cookiePresent,
                              boolean remoteSessionValid) throws Exception {
        if (enabled) {
            when(crowdSsoService.getAuthenticator()).thenReturn(authenticator);
            if (cookiePresent) {
                when(authenticator.getToken(same(request))).thenReturn(SSO_COOKIE);
            }
            if (remoteSessionValid) {
                when(authenticator.getUser(same(request))).thenReturn(crowdUser);
            }
        }

        if (userManagedBySso) {
            when(crowdSsoService.isManagedBySso(eq(USERNAME))).thenReturn(true);
            when(crowdSsoService.isSsoCandidate(same(request))).thenReturn(true);
        }
    }
}
