package com.atlassian.stash.internal.spring.security;

import com.atlassian.bitbucket.auth.*;
import com.atlassian.bitbucket.i18n.KeyedMessage;
import com.atlassian.bitbucket.i18n.SimpleI18nService;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.stash.internal.ApplicationConstants;
import com.atlassian.stash.internal.auth.AuthenticationHelper;
import com.atlassian.stash.internal.user.StashUserAuthenticationToken;
import com.atlassian.bitbucket.scm.AuthenticationState;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextImpl;

import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

import static com.atlassian.stash.internal.spring.security.StashAuthenticationFilter.KEY_PASSWORD;
import static com.atlassian.stash.internal.spring.security.StashAuthenticationFilter.KEY_USERNAME;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class StashAuthenticationFilterTest {

    private static String LOGIN_URL = "/mvc/login";
    private static String LOGIN_URI = "http://localhost:7990/bitbucket/mvc/login";
    private static String PROJECTS_URI = "http://localhost:7990/bitbucket/mvc/projects";

    private static final String URL_SECURITY_CHECK = "/stash/" + ApplicationConstants.URL_SECURITY_CHECK;

    @Mock
    private AuthenticationContext authenticationContext;
    @Mock
    private AuthenticationHelper authenticationHelper;
    @Mock
    private AuthenticationManager authenticationManager;
    @Mock
    private StashUserAuthenticationToken authenticationToken;
    @Mock
    private HttpAuthenticationHandler authHandler;
    @Mock
    private FilterChain chain;
    @Mock
    private HttpAuthenticationFailureHandler failureHandler;
    private StashAuthenticationFilter filter;
    @Spy
    private SimpleI18nService i18nService;
    @Mock
    private HttpServletRequest request;
    @Mock
    private HttpServletResponse response;
    @Mock
    private HttpAuthenticationSuccessHandler successHandler;
    @Mock
    private ApplicationUser user;

    @Before
    public void setup() {
        filter = new StashAuthenticationFilter(authenticationContext, authHandler, authenticationHelper,
                authenticationManager, failureHandler, successHandler, i18nService, LOGIN_URL);
        when(authenticationToken.getPrincipal()).thenReturn(user);
    }

    @Test
    public void testNoProviders() throws Exception {
        when(request.getRequestURI()).thenReturn(PROJECTS_URI);
        when(authenticationManager.authenticate(any(Authentication.class)))
                .thenThrow(new ProviderNotFoundException(null));

        filter.doFilter(request, response, chain);
    }

    @Test
    public void testBasicAuthentication() throws Exception {
        when(request.getRequestURI()).thenReturn(PROJECTS_URI);
        when(request.getHeader(eq("Authorization"))).thenReturn("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==");
        when(authenticationManager.authenticate(any(Authentication.class))).thenReturn(authenticationToken);

        filter.doFilter(request, response, chain);

        ArgumentCaptor<Authentication> contextCaptor = ArgumentCaptor.forClass(Authentication.class);
        verify(authenticationManager).authenticate(contextCaptor.capture());

        HttpAuthenticationContextToken token = (HttpAuthenticationContextToken) contextCaptor.getValue();
        HttpAuthenticationContext context = token.getContext();
        assertEquals("Aladdin", context.getUsername());
        assertEquals("open sesame", contextCaptor.getValue().getCredentials());
    }

    @Test
    public void testFormBasedAuthentication() throws Exception {
        when(request.getRequestURI()).thenReturn(PROJECTS_URI);
        when(request.getParameter(KEY_USERNAME)).thenReturn("user");
        when(request.getParameter(KEY_PASSWORD)).thenReturn("password");
        when(authenticationManager.authenticate(any(Authentication.class))).thenReturn(authenticationToken);

        filter.doFilter(request, response, chain);

        ArgumentCaptor<Authentication> contextCaptor = ArgumentCaptor.forClass(Authentication.class);
        verify(authenticationManager).authenticate(contextCaptor.capture());

        HttpAuthenticationContextToken token = (HttpAuthenticationContextToken) contextCaptor.getValue();
        HttpAuthenticationContext context = token.getContext();
        assertEquals("user", context.getUsername());
        assertEquals("password", contextCaptor.getValue().getCredentials());
    }

    @Test
    public void testAuthenticationFailureBadCredentials() throws Exception {
        IncorrectPasswordAuthenticationException e = new IncorrectPasswordAuthenticationException(mock(KeyedMessage.class));
        when(request.getRequestURI()).thenReturn(PROJECTS_URI);
        when(authenticationManager.authenticate(any(Authentication.class))).thenThrow(new BadCredentialsException("", e));
        when(failureHandler.onAuthenticationFailure(any(HttpAuthenticationFailureContext.class))).thenReturn(true);

        filter.doFilter(request, response, chain);

        ArgumentCaptor<HttpAuthenticationFailureContext> contextCaptor = ArgumentCaptor.forClass(HttpAuthenticationFailureContext.class);
        verify(failureHandler).onAuthenticationFailure(contextCaptor.capture());
        assertSame(e, contextCaptor.getValue().getAuthenticationException());
        assertEquals(AuthenticationState.NOT_AUTHENTICATED, contextCaptor.getValue().getAuthenticationState());
    }

    @Test
    public void testAuthenticationFailureBadCaptcha() throws Exception {
        CaptchaRequiredAuthenticationException e = new CaptchaRequiredAuthenticationException(mock(KeyedMessage.class));
        when(request.getRequestURI()).thenReturn(PROJECTS_URI);
        when(authenticationManager.authenticate(any(Authentication.class))).thenThrow(new LockedException("", e));
        when(failureHandler.onAuthenticationFailure(any(HttpAuthenticationFailureContext.class))).thenReturn(true);

        filter.doFilter(request, response, chain);

        ArgumentCaptor<HttpAuthenticationFailureContext> contextCaptor = ArgumentCaptor.forClass(HttpAuthenticationFailureContext.class);
        verify(failureHandler).onAuthenticationFailure(contextCaptor.capture());
        assertSame(e, contextCaptor.getValue().getAuthenticationException());
        assertEquals(AuthenticationState.CAPTCHA_REQUIRED, contextCaptor.getValue().getAuthenticationState());
    }

    @Test
    public void testAuthenticationFailureUnexpectedException() throws Exception {
        IOException e = new IOException();
        when(request.getRequestURI()).thenReturn(PROJECTS_URI);
        when(authenticationManager.authenticate(any(Authentication.class))).thenThrow(
                new AuthenticationServiceException("", e));
        when(failureHandler.onAuthenticationFailure(any(HttpAuthenticationFailureContext.class))).thenReturn(true);

        filter.doFilter(request, response, chain);

        ArgumentCaptor<HttpAuthenticationFailureContext> contextCaptor = ArgumentCaptor.forClass(HttpAuthenticationFailureContext.class);
        verify(failureHandler).onAuthenticationFailure(contextCaptor.capture());
        assertTrue(contextCaptor.getValue().getAuthenticationException() instanceof AuthenticationSystemException);
    }

    @Test
    public void testAuthenticationIgnoresDigestHeader() throws Exception {
        when(request.getRequestURI()).thenReturn(PROJECTS_URI);
        when(request.getHeader(eq("Authorization"))).thenReturn("Digest username=\"Mufasa\"");
        when(authenticationManager.authenticate(any(Authentication.class))).thenReturn(authenticationToken);

        filter.doFilter(request, response, chain);

        ArgumentCaptor<Authentication> contextCaptor = ArgumentCaptor.forClass(Authentication.class);
        verify(authenticationManager).authenticate(contextCaptor.capture());

        HttpAuthenticationContextToken token = (HttpAuthenticationContextToken) contextCaptor.getValue();
        HttpAuthenticationContext context = token.getContext();
        assertNull(context.getUsername());
        assertNull(context.getCredentials());
    }

    @Test
    public void testAuthenticationIgnoresEmptyHeader() throws Exception {
        when(request.getRequestURI()).thenReturn(PROJECTS_URI);
        when(request.getHeader(eq("Authorization"))).thenReturn("");
        when(authenticationManager.authenticate(any(Authentication.class))).thenReturn(authenticationToken);

        filter.doFilter(request, response, chain);

        ArgumentCaptor<Authentication> contextCaptor = ArgumentCaptor.forClass(Authentication.class);
        verify(authenticationManager).authenticate(contextCaptor.capture());

        HttpAuthenticationContextToken token = (HttpAuthenticationContextToken) contextCaptor.getValue();
        HttpAuthenticationContext context = token.getContext();
        assertNull(context.getUsername());
        assertNull(context.getCredentials());
    }

    @Test
    public void testAuthenticationIgnoresHeaderMissingColon() throws Exception {
        when(request.getRequestURI()).thenReturn(PROJECTS_URI);
        when(request.getHeader(eq("Authorization"))).thenReturn("Basic TWFsZm9ybWVk");
        when(authenticationManager.authenticate(any(Authentication.class))).thenReturn(authenticationToken);

        filter.doFilter(request, response, chain);

        ArgumentCaptor<Authentication> contextCaptor = ArgumentCaptor.forClass(Authentication.class);
        verify(authenticationManager).authenticate(contextCaptor.capture());

        HttpAuthenticationContextToken token = (HttpAuthenticationContextToken) contextCaptor.getValue();
        HttpAuthenticationContext context = token.getContext();
        assertNull(context.getUsername());
        assertNull(context.getCredentials());
    }

    @Test
    public void testAuthenticationIgnoresHeaderWithJustPrefix() throws Exception {
        when(request.getRequestURI()).thenReturn(PROJECTS_URI);
        when(request.getHeader(eq("Authorization"))).thenReturn("Basic ");
        when(authenticationManager.authenticate(any(Authentication.class))).thenReturn(authenticationToken);

        filter.doFilter(request, response, chain);

        ArgumentCaptor<Authentication> contextCaptor = ArgumentCaptor.forClass(Authentication.class);
        verify(authenticationManager).authenticate(contextCaptor.capture());

        HttpAuthenticationContextToken token = (HttpAuthenticationContextToken) contextCaptor.getValue();
        HttpAuthenticationContext context = token.getContext();
        assertNull(context.getUsername());
        assertNull(context.getCredentials());
    }

    @Test
    public void testAuthenticationIgnoresUndecodableHeader() throws Exception {
        when(request.getRequestURI()).thenReturn(PROJECTS_URI);
        when(request.getHeader(eq("Authorization"))).thenReturn("Basic [");
        when(authenticationManager.authenticate(any(Authentication.class))).thenReturn(authenticationToken);

        filter.doFilter(request, response, chain);

        ArgumentCaptor<Authentication> contextCaptor = ArgumentCaptor.forClass(Authentication.class);
        verify(authenticationManager).authenticate(contextCaptor.capture());

        HttpAuthenticationContextToken token = (HttpAuthenticationContextToken) contextCaptor.getValue();
        HttpAuthenticationContext context = token.getContext();
        assertNull(context.getUsername());
        assertNull(context.getCredentials());
    }

    @Test
    public void testAuthenticationSuccessHandled() throws Exception {
        when(request.getRequestURI()).thenReturn(PROJECTS_URI);
        when(authenticationManager.authenticate(any(Authentication.class))).thenReturn(authenticationToken);
        when(successHandler.onAuthenticationSuccess(any(HttpAuthenticationSuccessContext.class))).thenReturn(true);

        filter.doFilter(request, response, chain);

        ArgumentCaptor<HttpAuthenticationSuccessContext> contextCaptor = ArgumentCaptor.forClass(HttpAuthenticationSuccessContext.class);
        verify(successHandler).onAuthenticationSuccess(contextCaptor.capture());
        assertSame(user, contextCaptor.getValue().getUser());
    }

    @Test
    public void testAuthenticationSuccessNotHandled() throws Exception {
        when(request.getRequestURI()).thenReturn(PROJECTS_URI);
        when(authenticationManager.authenticate(any(Authentication.class))).thenReturn(authenticationToken);

        filter.doFilter(request, response, chain);

        // if the successHandler returned true, the filter chain should be bypassed
        verify(chain).doFilter(same(request), same(response));
    }

    @Test
    public void testNoAuthenticationOnLoginPageAfterFailure() throws Exception {
        when(request.getRequestURI()).thenReturn(LOGIN_URI);
        when(authenticationHelper.isAuthenticationFailed(same(request))).thenReturn(true);

        filter.doFilter(request, response, chain);

        verify(chain).doFilter(same(request), same(response));
        verifyZeroInteractions(authenticationManager);
    }

    @Test
    public void testValidateAuthentication() throws Exception {
        when(request.getRequestURI()).thenReturn(PROJECTS_URI);
        when(authenticationContext.getCurrentUser()).thenReturn(user);
        when(authenticationContext.isAuthenticated()).thenReturn(true);

        filter.doFilter(request, response, chain);

        ArgumentCaptor<HttpAuthenticationContext> captor = ArgumentCaptor.forClass(HttpAuthenticationContext.class);
        verify(authHandler).validateAuthentication(captor.capture());
    }

    @Test
    public void testValidateAuthenticationFailedNoReAuth() throws Exception {
        ExpiredAuthenticationException e = new ExpiredAuthenticationException(mock(KeyedMessage.class));

        when(request.getRequestURI()).thenReturn(PROJECTS_URI);
        when(authenticationContext.getCurrentUser()).thenReturn(user);
        when(authenticationContext.isAuthenticated()).thenReturn(true);
        // mock failing validation
        doThrow(e).when(authHandler).validateAuthentication(any(HttpAuthenticationContext.class));
        // and re-authentication where all authenticators opt-out
        when(authenticationManager.authenticate(any(Authentication.class))).thenThrow(new ProviderNotFoundException(""));

        filter.doFilter(request, response, chain);

        ArgumentCaptor<HttpAuthenticationFailureContext> captor = ArgumentCaptor.forClass(HttpAuthenticationFailureContext.class);
        verify(failureHandler).onAuthenticationFailure(captor.capture());

        assertSame(e, captor.getValue().getAuthenticationException());
    }

    @Test
    public void testValidateAuthenticationFailedReAuthFails() throws Exception {
        BadCredentialsException authenticationEx = new BadCredentialsException("",
                new ExpiredAuthenticationException(mock(KeyedMessage.class)));
        ExpiredAuthenticationException validationEx = new ExpiredAuthenticationException(mock(KeyedMessage.class));

        when(request.getRequestURI()).thenReturn(PROJECTS_URI);
        when(authenticationContext.getCurrentUser()).thenReturn(user);
        when(authenticationContext.isAuthenticated()).thenReturn(true);
        // mock failing validation
        doThrow(validationEx).when(authHandler).validateAuthentication(any(HttpAuthenticationContext.class));
        // and re-authentication where all authentication fails
        when(authenticationManager.authenticate(any(Authentication.class))).thenThrow(authenticationEx);

        filter.doFilter(request, response, chain);

        ArgumentCaptor<HttpAuthenticationFailureContext> captor = ArgumentCaptor.forClass(HttpAuthenticationFailureContext.class);
        verify(failureHandler).onAuthenticationFailure(captor.capture());

        assertSame(validationEx, captor.getValue().getAuthenticationException());
    }

    @Test
    public void testValidateAuthenticationFailedReAuthSucceeds() throws Exception {
        ExpiredAuthenticationException e = new ExpiredAuthenticationException(mock(KeyedMessage.class));

        when(request.getRequestURI()).thenReturn(PROJECTS_URI);
        when(authenticationContext.getCurrentUser()).thenReturn(user);
        when(authenticationContext.isAuthenticated()).thenReturn(true);
        // mock failing validation
        doThrow(e).when(authHandler).validateAuthentication(any(HttpAuthenticationContext.class));
        // but successful re-authentication
        when(authenticationManager.authenticate(any(Authentication.class))).thenReturn(authenticationToken);

        filter.doFilter(request, response, chain);

        verify(successHandler).onAuthenticationSuccess(any(HttpAuthenticationSuccessContext.class));
    }

    @Test
    public void testAuthenticatedRequestToSecurityCheckUrlShouldClearSecurityContext() throws Exception {
        SecurityContext context = new SecurityContextImpl();
        SecurityContextHolder.setContext(context);
        when(request.getRequestURI()).thenReturn(URL_SECURITY_CHECK);
        when(authenticationContext.getCurrentUser()).thenReturn(user);
        when(authenticationContext.isAuthenticated()).thenReturn(false);
        filter.doFilter(request, response, chain);
        assertNotSame(context, SecurityContextHolder.getContext());
    }
}
