package com.atlassian.stash.internal.user;

import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.user.ApplicationUserVisitor;
import com.atlassian.bitbucket.user.DetailedUser;
import com.atlassian.bitbucket.user.ServiceUser;
import com.atlassian.security.random.SecureTokenGenerator;
import com.google.common.collect.ImmutableSet;
import org.apache.commons.lang3.StringUtils;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.junit.Test;

import javax.persistence.Column;
import java.lang.reflect.Field;
import java.text.Normalizer;
import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import static com.atlassian.stash.internal.user.InternalApplicationUser.generateSlug;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class InternalNormalUserTest {
    @Test
    public void testBuilderSlug() {
        String base = StringUtils.repeat("z", InternalApplicationUser.MAX_GENERATED_SLUG_LENGTH);

        InternalNormalUser.Builder builder = builder()
                .slug(base); //One less than MAX_SLUG_LENGTH, all slug-safe
        for (int i = 0; i < InternalNormalUser.MAX_SLUG_RETRY_COUNT; ++i) {
            builder.slug(base + i); //All retry iterations should be acceptable
        }
    }

    @Test(expected = IllegalArgumentException.class)
    public void testBuilderSlugWithExactLengthButNonDigitFinalCharacter() {
        builder().slug(StringUtils.repeat("d", ApplicationUser.MAX_SLUG_LENGTH));
    }

    @Test(expected = IllegalArgumentException.class)
    public void testBuilderSlugWithExcessiveLength() {
        builder().slug(StringUtils.repeat("c", 255));
    }

    @Test(expected = IllegalArgumentException.class)
    public void testBuilderSlugWithIllegalCharacters() {
        builder().slug("na/jdoe");
    }

    @Test(expected = NullPointerException.class)
    public void testBuilderSlugWithNull() {
        builder().slug(null);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testBuilderSlugWithUppercaseLetters() {
        builder().slug("TEST"); //Short enough, no nasty characters, but not lowercase
    }

    @Test
    public void testBuilderWithTimeZone() {
        InternalNormalUser admin = builder().slug("admin").timeZone(ZoneOffset.MAX).build();
        assertThat(admin.getTimeZone(), equalTo(ZoneOffset.MAX));
    }

    @Test
    public void testBuilderWithoutTimeZone() {
        InternalNormalUser admin = builder().slug("admin").timeZone(null).build();
        assertThat(admin.getTimeZone(), Matchers.nullValue());
    }

    @Test
    public void testGenerateToken() {
        SecureTokenGenerator tokenGenerator = mock(SecureTokenGenerator.class);
        when(tokenGenerator.generateToken()).thenReturn("280104906893474d0dd8c747e24c892b58c5ea55",
                StringUtils.repeat("A", InternalApplicationUser.MAX_SLUG_LENGTH),
                StringUtils.repeat("B", 255));

        assertEquals("280104906893474d0dd8c747e24c892b58c5ea55", generateSlug(tokenGenerator));
        assertEquals(StringUtils.repeat("A", InternalApplicationUser.MAX_SLUG_LENGTH),
                generateSlug(tokenGenerator));
        assertEquals(StringUtils.repeat("B", InternalApplicationUser.MAX_SLUG_LENGTH),
                generateSlug(tokenGenerator));
    }

    @Test
    public void testSlugify() {
        assertEquals("abc 123", InternalApplicationUser.slugify("ABC 123"));
        assertEquals(normalise("\u00e4b\u00e7 123"), InternalApplicationUser.slugify("\u00e4b\u00e7 123"));
        assertEquals("a b c-1_2_3", InternalApplicationUser.slugify("a b c-1_2_3"));
        assertEquals(normalise("jos\u00e9"), InternalApplicationUser.slugify("jos\u00e9"));
        assertEquals(normalise("jos\u00e9"), InternalApplicationUser.slugify("JOS\u00c9"));
        assertEquals("\u9c81\u8fc5", InternalApplicationUser.slugify("\u9c81\u8fc5")); //鲁迅
        assertEquals("a_b_c", InternalApplicationUser.slugify("a\\b/c")); //Replace \ and / with _
        assertEquals("a_b_c", InternalApplicationUser.slugify("a$b@c"));  //Replace $ and @ with _
        assertEquals("a_b_c", InternalApplicationUser.slugify("a!b:c"));  //Replace ! and : with _
        assertEquals("a_b_c", InternalApplicationUser.slugify("a;b,c"));  //Replace ; and , with _
        assertEquals("a_b_c", InternalApplicationUser.slugify("a+b*c"));  //Replace + and * with _
        assertEquals("a_b_c", InternalApplicationUser.slugify("a?b%c"));  //Replace ? and % with _
        assertEquals("a_b_c", InternalApplicationUser.slugify("a[b]c"));  //Replace [ and ] with _
        assertEquals("a_b_c", InternalApplicationUser.slugify("a(b)c"));  //Replace ( and ) with _
        assertEquals("a_b_c", InternalApplicationUser.slugify("a'b=c"));  //Replace ' and = with _
        assertEquals("a_b_c", InternalApplicationUser.slugify("a&b#c"));  //Replace & and # with _
        assertEquals("a|b_c", InternalApplicationUser.slugify("a|b#c"));  //Replace & and # with _
        assertEquals(StringUtils.repeat("a", InternalApplicationUser.MAX_GENERATED_SLUG_LENGTH),
                InternalApplicationUser.slugify(StringUtils.repeat("A", 255))); //255 is the username length
        assertEquals(StringUtils.repeat("a", InternalApplicationUser.MAX_GENERATED_SLUG_LENGTH),
                InternalApplicationUser.slugify(StringUtils.repeat("A", InternalApplicationUser.MAX_SLUG_LENGTH)));
    }

    @Test
    public void testFromUserReturnsEmptyWhenForAnonymous() {
        assertThat(InternalNormalUser.fromUser(null), isEmpty());
    }

    @Test
    public void testFromUserReturnsEmptyForInternalServiceUser() {
        assertThat(InternalNormalUser.fromUser(internalServiceUser()), isEmpty());
    }

    @Test
    public void testFromUserReturnsEmptyForServiceUser() {
        assertThat(InternalNormalUser.fromUser(serviceUser()), isEmpty());
    }

    @Test
    public void testFromUserReturnsEmptyForApplicationUser() {
        assertThat(InternalNormalUser.fromUser(applicationUser()), isEmpty());
    }

    @Test
    public void testFromUserReturnsEmptyForDetailedServiceUser() {
        assertThat(InternalNormalUser.fromUser(detailedUser(internalServiceUser())), isEmpty());
    }

    @Test
    public void testFromUserReturnsUserForDetailedNormalUser() {
        InternalNormalUser normalUser = internalNormalUser();

        assertThat(InternalNormalUser.fromUser(detailedUser(normalUser)), hasValue(normalUser));
    }

    @Test
    public void testFromUserReturnsUserForNormalUser() {
        InternalNormalUser normalUser = internalNormalUser();

        assertThat(InternalNormalUser.fromUser(normalUser), hasValue(normalUser));
    }

    @Test
    public void testIsUserInSyncWithDao() {
        Set<String> existingFields = ImmutableSet.of("username", "slug", "locale", "deletedDate", "timeZone");

        List<String> newFields = Arrays.stream(InternalNormalUser.class.getDeclaredFields())
                .filter(m -> m.getAnnotation(Column.class) != null)
                .map(Field::getName)
                .filter(name -> !existingFields.contains(name))
                .sorted()
                .collect(Collectors.toList());

        if (!newFields.isEmpty()) {
            fail("New field(s) " + newFields + " were added to `InternalNormalUser`. Did you update "
                    + "the HSQL inside of `HibernateStashUserDao`?");
        }
    }

    private static ApplicationUser applicationUser() {
        ApplicationUser user = mock(ApplicationUser.class);
        when(user.accept(any())).thenAnswer(a ->  a.getArgumentAt(0, ApplicationUserVisitor.class).visit(user));
        return user;
    }

    private static DetailedUser detailedUser(InternalApplicationUser delegate) {
        return new InternalDetailedUser.Builder(delegate).build();
    }

    private static <T> Matcher<Optional<T>> hasValue(T value) {
        return equalTo(Optional.of(value));
    }

    private static InternalServiceUser internalServiceUser() {
        return new InternalServiceUser();
    }

    private static <T> Matcher<Optional<T>> isEmpty() {
        return equalTo(Optional.empty());
    }

    private static InternalNormalUser internalNormalUser() {
        return new InternalNormalUser();
    }

    private static ServiceUser serviceUser() {
        ServiceUser user = mock(ServiceUser.class);
        when(user.accept(any())).thenAnswer(a ->  a.getArgumentAt(0, ApplicationUserVisitor.class).visit(user));
        return user;
    }

    private InternalNormalUser.Builder builder() {
        return new InternalNormalUser.Builder();
    }

    private static String normalise(String name) {
        return Normalizer.normalize(name, Normalizer.Form.NFKD);
    }
}
