package com.atlassian.stash.internal.auth;

import com.atlassian.bitbucket.auth.HttpAuthenticationContext;
import com.atlassian.bitbucket.user.NoSuchUserException;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.i18n.KeyedMessage;
import com.atlassian.bitbucket.i18n.SimpleI18nService;
import com.atlassian.bitbucket.auth.IncorrectPasswordAuthenticationException;
import com.atlassian.crowd.embedded.api.User;
import com.atlassian.stash.internal.crowd.CrowdControl;
import com.atlassian.stash.internal.user.InternalNormalUser;
import com.atlassian.stash.internal.user.UserHelper;
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.assertNull;
import static org.junit.Assert.assertSame;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class EmbeddedCrowdHttpAuthenticationHandlerTest {
    private static final String PASSWORD = "credentials";
    private static final String USERNAME = "user";

    @Mock
    private HttpAuthenticationContext context;
    @Mock
    private CrowdControl crowdControl;
    @InjectMocks
    private EmbeddedCrowdHttpAuthenticationHandler handler;
    @Spy
    private I18nService i18nService = new SimpleI18nService();
    @Mock
    private HttpServletRequest request;
    @Mock
    private HttpServletResponse response;
    @Mock
    private HttpSession session;
    @Mock
    private InternalNormalUser user;
    @Mock
    private UserHelper userHelper;

    @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(user.getName()).thenReturn(USERNAME);
    }

    /**
     * Credentials of any type other than {@code String} should skip authentication entirely
     */
    @Test
    public void testAuthenticateCredentialsNotString() throws Exception {
        when(context.getUsername()).thenReturn(USERNAME);
        when(context.getCredentials()).thenReturn(99);

        assertNull(handler.authenticate(context));

        verify(crowdControl, never()).authenticate(anyString(), anyString());
    }

    /**
     * Empty password should throw an exception without calling the user service.
     */
    @Test(expected = IncorrectPasswordAuthenticationException.class)
    public void testAuthenticateEmptyPassword() throws Exception {
        when(context.getUsername()).thenReturn(USERNAME);
        when(context.getCredentials()).thenReturn("");

        try {
            handler.authenticate(context);
        } finally {
            verify(crowdControl, never()).authenticate(anyString(), anyString());
        }
    }

    /**
     * Empty username should throw an exception without calling the user service.
     */
    @Test(expected = IncorrectPasswordAuthenticationException.class)
    public void testAuthenticateEmptyUsername() throws Exception {
        when(context.getUsername()).thenReturn("");
        when(context.getCredentials()).thenReturn(PASSWORD);

        try {
            handler.authenticate(context);
        } finally {
            verify(crowdControl, never()).authenticate(anyString(), anyString());
        }
    }

    @Test(expected = IncorrectPasswordAuthenticationException.class)
    public void testAuthenticateIncorrectPassword() throws Exception {
        when(context.getUsername()).thenReturn(USERNAME);
        when(context.getCredentials()).thenReturn(PASSWORD);
        IncorrectPasswordAuthenticationException e = new IncorrectPasswordAuthenticationException(mock(KeyedMessage.class));
        when(crowdControl.authenticate(eq(USERNAME), eq(PASSWORD))).thenThrow(e);

        handler.authenticate(context);
    }

    /**
     * Null passwords should skip authentication entirely, even if a username is supplied.
     */
    @Test
    public void testAuthenticateNoPassword() throws Exception {
        when(context.getUsername()).thenReturn(USERNAME);

        assertNull(handler.authenticate(context));

        verify(crowdControl, never()).authenticate(anyString(), anyString());
    }

    /**
     * Null usernames should skip authentication entirely, even if a password is supplied.
     */
    @Test
    public void testAuthenticateNoUsername() throws Exception {
        when(context.getCredentials()).thenReturn(PASSWORD);

        assertNull(handler.authenticate(context));

        verify(crowdControl, never()).authenticate(anyString(), anyString());
    }

    @Test
    public void testAuthenticateNoUsernameOrPassword() throws Exception {
        // without username or password, the authenticator should opt out of authentication
        assertNull(handler.authenticate(context));
    }

    @Test(expected = IncorrectPasswordAuthenticationException.class)
    public void testAuthenticateNoSuchUser() throws Exception {
        when(context.getUsername()).thenReturn(USERNAME);
        when(context.getCredentials()).thenReturn(PASSWORD);
        NoSuchUserException e = new NoSuchUserException(mock(KeyedMessage.class), USERNAME);
        when(crowdControl.authenticate(eq(USERNAME), eq(PASSWORD))).thenThrow(e);

        handler.authenticate(context);
    }

    @Test
    public void testAuthenticateSuccess() throws Exception {
        when(context.getUsername()).thenReturn(USERNAME);
        when(context.getCredentials()).thenReturn(PASSWORD);
        mockCrowdAuthenticate(USERNAME, PASSWORD, user);

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

    @Test
    public void testValidateAuthentication() throws Exception {
        handler.validateAuthentication(context);

        // should be a no-op
        verifyZeroInteractions(user, context, i18nService);
    }

    private void mockCrowdAuthenticate(String username, String password, InternalNormalUser user) {
        User crowdUser = mock(User.class);
        when(crowdControl.authenticate(username, password)).thenReturn(crowdUser);
        when(userHelper.transformOrCreate(crowdUser)).thenReturn(user);
    }
}
