package com.atlassian.stash.internal.web.users;

import com.atlassian.bitbucket.auth.AuthenticationContext;
import com.atlassian.bitbucket.auth.CaptchaRequiredAuthenticationException;
import com.atlassian.bitbucket.auth.IncorrectPasswordAuthenticationException;
import com.atlassian.bitbucket.i18n.KeyedMessage;
import com.atlassian.bitbucket.i18n.SimpleI18nService;
import com.atlassian.bitbucket.nav.NavBuilder;
import com.atlassian.bitbucket.ui.PluginFormFragments;
import com.atlassian.bitbucket.ui.PluginFormFragmentsFactory;
import com.atlassian.bitbucket.ui.ValidationErrors;
import com.atlassian.bitbucket.user.*;
import com.atlassian.stash.internal.auth.AuthenticationHelper;
import com.atlassian.stash.internal.crowd.CrowdControl;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.*;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.RedirectView;

import java.util.Collection;

import static com.atlassian.stash.internal.spring.ModelAndViewMatcher.modelAndView;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.same;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class UserAccountControllerTest {

    @Mock
    private CrowdControl crowdControl;
    @Mock
    private AuthenticationContext authenticationContext;
    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
    private PluginFormFragmentsFactory formFragmentsFactory;
    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
    private NavBuilder navBuilder;
    @Spy
    private SimpleI18nService i18nService;
    @Mock
    private UserAdminService userAdminService;
    @Mock
    private UserService userService;
    @Mock
    private AuthenticationHelper authenticationHelper;
    @Spy
    private LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
    @InjectMocks
    private UserAccountController controller;

    @Before
    public void setUp() {
        validator.afterPropertiesSet();
    }

    @Test(expected = AccessDeniedException.class)
    public void testAccountViewWhenAnonymous() {
        when(authenticationContext.getCurrentUser()).thenReturn(null);
        controller.viewSettings();
    }

    @Test(expected = AccessDeniedException.class)
    public void testPasswordViewWhenAnonymous() {
        when(authenticationContext.getCurrentUser()).thenReturn(null);
        controller.viewPassword();
    }

    @Test
    public void testSendsToAccountPage() {
        ApplicationUser user = mock(ApplicationUser.class);
        when(authenticationContext.getCurrentUser()).thenReturn(user);
        DetailedUser detailedUser = mock(DetailedUser.class);
        when(userAdminService.getUserDetails(same(user))).thenReturn(detailedUser);

        ModelAndView modelAndView = controller.viewSettings();

        assertThat(modelAndView, modelAndView(UserAccountController.SETTINGS_VIEW));
    }

    @Test
    public void testPostWithoutErrorsSavesAndSendsToAccountPage() {
        ApplicationUser user = mock(ApplicationUser.class);
        when(authenticationContext.getCurrentUser()).thenReturn(user);
        DetailedUser detailedUser = mock(DetailedUser.class);
        when(userAdminService.getUserDetails(same(user))).thenReturn(detailedUser);

        Errors errors = mock(Errors.class);
        when(errors.hasErrors()).thenReturn(false);

        when(userService.updateUser(anyString(), anyString())).thenReturn(user);

        ModelAndView modelAndView = controller.updateSettings(mock(AccountSettingsForm.class), errors, new MockHttpServletRequest());
        PluginFormFragments formFragments = formFragmentsFactory.forKey(anyString(), anyMapOf(String.class, Object.class));
        verify(formFragments).validate(anyMapOf(String.class, String[].class), any(ValidationErrors.class));
        verify(formFragments).execute(anyMapOf(String.class, String[].class));

        assertThat(modelAndView, modelAndView(UserAccountController.SETTINGS_VIEW)
                .withEntry("saved", true));
    }

    @Test
    public void testPostWithErrorsDoesntSaveAndSendsToAccountPage() {
        ApplicationUser user = mock(ApplicationUser.class);
        when(authenticationContext.getCurrentUser()).thenReturn(user);
        DetailedUser detailedUser = mock(DetailedUser.class);
        when(userAdminService.getUserDetails(same(user))).thenReturn(detailedUser);
        PluginFormFragments formFragments = formFragmentsFactory.forKey(anyString(), anyMapOf(String.class, Object.class));

        Errors errors = mock(Errors.class);
        when(errors.hasErrors()).thenReturn(true);

        when(userService.updateUser(anyString(), anyString())).thenReturn(user);
        when(formFragments.getErrorHtml(anyMapOf(String.class, String[].class), Matchers.<String, Collection<String>>anyMapOf(String.class, null))).thenReturn("");

        ModelAndView modelAndView = controller.updateSettings(mock(AccountSettingsForm.class), errors, new MockHttpServletRequest());
        verify(formFragments).validate(anyMapOf(String.class, String[].class), any(ValidationErrors.class));
        verify(formFragments).getErrorHtml(anyMapOf(String.class, String[].class), Matchers.<String, Collection<String>>anyMapOf(String.class, null));
        verifyNoMoreInteractions(formFragments);
        assertThat(modelAndView, modelAndView(UserAccountController.SETTINGS_VIEW)
                .withEntry("saved", false));
    }

    @Test
    public void testViewPasswordPage() {
        ApplicationUser user = mock(ApplicationUser.class);
        when(authenticationContext.getCurrentUser()).thenReturn(user);
        DetailedUser detailedUser = mock(DetailedUser.class);
        when(userAdminService.getUserDetails(same(user))).thenReturn(detailedUser);
        when(crowdControl.canResetPassword(eq(user.getName()))).thenReturn(true);

        ModelAndView modelAndView = controller.viewPassword();
        assertThat(modelAndView, modelAndView(UserAccountController.PASSWORD_VIEW)
                .withEntry("editablePassword", true)
                .withEntry("user", detailedUser));
    }

    @Test
    public void testViewPasswordPageWhenPasswordNotEditable() {
        ApplicationUser user = mock(ApplicationUser.class);
        when(authenticationContext.getCurrentUser()).thenReturn(user);
        DetailedUser detailedUser = mock(DetailedUser.class);
        when(userAdminService.getUserDetails(same(user))).thenReturn(detailedUser);

        ModelAndView modelAndView = controller.viewPassword();
        assertThat(modelAndView, modelAndView(UserAccountController.PASSWORD_VIEW)
                .withEntry("editablePassword", false)
                .withEntry("user", detailedUser));
    }

    @Test
    public void testPostWithoutErrorsSavesAndSendsToPasswordPage() {
        ApplicationUser user = mock(ApplicationUser.class);
        when(authenticationContext.getCurrentUser()).thenReturn(user);
        DetailedUser detailedUser = mock(DetailedUser.class);
        when(userAdminService.getUserDetails(same(user))).thenReturn(detailedUser);
        when(userService.authenticateWithCaptcha(anyString(), anyString())).thenReturn(detailedUser);

        Errors errors = mock(Errors.class);
        when(errors.hasErrors()).thenReturn(false);

        ModelAndView modelAndView = controller.updatePassword(mock(AccountPasswordForm.class), mock(Errors.class), new MockHttpServletRequest());
        assertThat(modelAndView, modelAndView(UserAccountController.PASSWORD_VIEW)
                .withEntry("saved", true));
    }

    @Test
    public void testPostWithErrorsDoesntSaveAndSendsToPasswordPage() {
        ApplicationUser user = mock(ApplicationUser.class);
        when(authenticationContext.getCurrentUser()).thenReturn(user);
        DetailedUser detailedUser = mock(DetailedUser.class);
        when(userAdminService.getUserDetails(same(user))).thenReturn(detailedUser);

        Errors errors = mock(Errors.class);
        when(errors.hasErrors()).thenReturn(true);

        doThrow(new IncorrectPasswordAuthenticationException(new KeyedMessage("", "", ""))).when(userService).updatePassword(anyString(), anyString());

        ModelAndView modelAndView = controller.updatePassword(mock(AccountPasswordForm.class), errors, new MockHttpServletRequest());
        assertThat(modelAndView, modelAndView(UserAccountController.PASSWORD_VIEW)
                .withEntry("saved", false));
    }

    @Test
    public void testPostWithBadPasswordSendsToCaptchaAfterMaxAttempts() {
        ApplicationUser user = mock(ApplicationUser.class);
        when(authenticationContext.getCurrentUser()).thenReturn(user);
        when(userAdminService.getUserDetails(same(user))).thenReturn(mock(DetailedUser.class));
        when(navBuilder.account().password().buildAbsolute()).thenReturn("redirect url");
        when(navBuilder.login().buildConfigured()).thenReturn("login url");
        when(userService.authenticateWithCaptcha(anyString(), anyString()))
                .thenThrow(new CaptchaRequiredAuthenticationException(new KeyedMessage("", "", "")));
        when(user.getName()).thenReturn("admin");

        Errors errors = mock(Errors.class);
        when(errors.hasErrors()).thenReturn(false);

        ModelAndView modelAndView = controller.updatePassword(mock(AccountPasswordForm.class), errors, new MockHttpServletRequest());
        assertTrue(modelAndView.getView() instanceof RedirectView);
        assertEquals(((RedirectView) modelAndView.getView()).getUrl(), "login url");
        assertTrue(modelAndView.getModel().get("next") == "redirect url");
    }

    @Test
    public void testPostEmptyNoMutableDetailsSaved() {
        initUserDetails(false);
        checkUpdateSettings(new AccountSettingsForm(), true);
    }

    @Test
    public void testPostEmptyMutableDetailsWithErrors() {
        initUserDetails(true);
        checkUpdateSettings(new AccountSettingsForm(), false);
    }

    @Test(expected = AccessDeniedException.class)
    public void testPostSettingsFormWithAnonymous() {
        when(authenticationContext.getCurrentUser()).thenReturn(null);
        controller.updateSettings(mock(AccountSettingsForm.class), mock(Errors.class), new MockHttpServletRequest());
    }

    @Test(expected = AccessDeniedException.class)
    public void testPostPasswordFormWithAnonymous() {
        when(authenticationContext.getCurrentUser()).thenReturn(null);
        controller.updatePassword(mock(AccountPasswordForm.class), mock(Errors.class), new MockHttpServletRequest());
    }

    private void checkUpdateSettings(AccountSettingsForm form, boolean expected) {
        ModelAndView modelAndView = controller.updateSettings(form, new BeanPropertyBindingResult(form, "form"), new MockHttpServletRequest());
        assertThat(modelAndView, modelAndView(UserAccountController.SETTINGS_VIEW).withEntry("saved", expected));
    }

    private void initUserDetails(boolean mutableDetails) {
        ApplicationUser user = new TestApplicationUser("foo");
        when(authenticationContext.getCurrentUser()).thenReturn(user);
        DetailedUser detailedUser = mock(DetailedUser.class);
        when(detailedUser.isMutableDetails()).thenReturn(mutableDetails);
        when(userAdminService.getUserDetails(same(user))).thenReturn(detailedUser);
    }
}
