package com.atlassian.stash.internal.user;

import com.atlassian.crowd.embedded.api.Attributes;
import com.atlassian.crowd.embedded.api.User;
import com.atlassian.crowd.embedded.api.UserWithAttributes;
import com.atlassian.crowd.embedded.impl.DelegatingUserWithAttributes;
import com.atlassian.crowd.embedded.impl.ImmutableUser;
import com.atlassian.crowd.search.query.entity.restriction.PropertyImpl;
import com.atlassian.security.random.SecureTokenGenerator;
import com.atlassian.stash.internal.crowd.CrowdControl;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatcher;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import java.util.concurrent.TimeUnit;

import static com.atlassian.stash.internal.crowd.CrowdTestUtils.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.Matchers.argThat;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.class)
public class PasswordResetHelperTest {

    private static final int DEFAULT_TOKEN_VALIDITY_PERIOD = 1;

    @Mock
    private CrowdControl crowdControl;
    @Mock
    private SecureTokenGenerator tokenGenerator;

    private PasswordResetHelper helper;

    @Before
    public void setUp() throws Exception {
        helper = new CrowdPasswordResetHelper(crowdControl, tokenGenerator, DEFAULT_TOKEN_VALIDITY_PERIOD);
    }

    @Test
    public void testResetPassword() throws Exception {
        User user = ImmutableUser.newUser().name("name").displayName("fullname")
                .emailAddress("test@example.com").toUser();
        String newPassword = "newpassword";

        helper.resetPassword(user, newPassword);
        verify(crowdControl).removeUserAttribute(matchUser(user), eq(CrowdPasswordResetHelper.PASSWORD_RESET_TOKEN_ATTRIBUTE));
        verify(crowdControl).removeUserAttribute(matchUser(user), eq(CrowdPasswordResetHelper.PASSWORD_RESET_TOKEN_EXPIRATION_ATTRIBUTE));
        verify(crowdControl).setPassword(matchUser(user), eq(newPassword));

    }

    @Test
    public void testAddResetPasswordToken() throws Exception {
        User user = ImmutableUser.newUser().name("name").displayName("fullname")
                .emailAddress("test@example.com").toUser();

        when(tokenGenerator.generateToken()).thenReturn("token");

        helper.addResetPasswordToken(user);
        verify(crowdControl).setUserAttribute(matchUser(user), eq(CrowdPasswordResetHelper.PASSWORD_RESET_TOKEN_EXPIRATION_ATTRIBUTE), tokenExpiration());
        verify(crowdControl).setUserAttribute(matchUser(user), eq(CrowdPasswordResetHelper.PASSWORD_RESET_TOKEN_ATTRIBUTE), eq("token"));
        verify(tokenGenerator).generateToken();
    }

    @Test
    public void testFindPasswordResetRequest() throws Exception {
        User user = ImmutableUser.newUser().name("user").toUser();
        mockSearchByToken(user, "token-in-db", System.currentTimeMillis() + 3600000);
        assertEquals(user, helper.findUserByResetToken("token-in-db"));
    }

    @Test
    public void testFindPasswordResetRequestWithExpiredToken() throws Exception {
        User user = ImmutableUser.newUser().name("user").toUser();
        mockSearchByToken(user, "token-in-db", 123);
        assertNull(helper.findUserByResetToken("token-in-db"));
    }

    @Test
    public void testFindPasswordResetRequestForUserWithoutToken() throws Exception {
        User user = ImmutableUser.newUser().name("user").toUser();
        mockRequest(user, emptyAttributes());
        assertNull(helper.findUserByResetToken("token-value"));
    }

    @Test
    public void testFindPasswordResetRequestWithNoToken() throws Exception {
        when(crowdControl.findUserByProperty(eq(new PropertyImpl<String>(CrowdPasswordResetHelper.PASSWORD_RESET_TOKEN_ATTRIBUTE, String.class)), eq("token-value"))).thenReturn(null);
        assertNull(helper.findUserByResetToken("token-value"));
    }

    /* -- utils --- */

    private void mockSearchByToken(User user, String tokenValue, long tokenExpiration) {
        mockRequest(user, tokenAttributes(tokenValue, tokenExpiration));
    }

    private void mockRequest(User user, Attributes attributes) {
        UserWithAttributes userWithAttributes = new DelegatingUserWithAttributes(user, attributes);
        when(crowdControl.findUserWithAttributes(user.getName())).thenReturn(userWithAttributes);
        when(crowdControl.findUserByProperty(
                eq(new PropertyImpl<String>(CrowdPasswordResetHelper.PASSWORD_RESET_TOKEN_ATTRIBUTE, String.class)),
                eq(attributes.getValue(CrowdPasswordResetHelper.PASSWORD_RESET_TOKEN_ATTRIBUTE))))
                .thenReturn(user);
    }

    private static Attributes tokenAttributes(String tokenValue, long tokenExpiration) {
        return toAttributes(
                CrowdPasswordResetHelper.PASSWORD_RESET_TOKEN_ATTRIBUTE, tokenValue,
                CrowdPasswordResetHelper.PASSWORD_RESET_TOKEN_EXPIRATION_ATTRIBUTE, Long.toString(tokenExpiration));
    }

    private static String tokenExpiration() {
        return argThat(new ArgumentMatcher<String>() {
            @Override
            public boolean matches(Object argument) {
                long expiration = (Long) argument;
                long now = System.currentTimeMillis();
                return expiration > now &&
                        (expiration - now) < (1.5 * TimeUnit.MINUTES.toMillis(DEFAULT_TOKEN_VALIDITY_PERIOD));
            }
        });
    }
}
