package com.atlassian.stash.internal.project;

import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.project.Project;
import com.atlassian.bitbucket.project.ProjectService;
import com.atlassian.bitbucket.user.EscalatedSecurityContext;
import com.atlassian.bitbucket.user.SecurityService;
import com.atlassian.bitbucket.validation.groups.Create;
import com.atlassian.bitbucket.validation.groups.TrustedCreate;
import com.atlassian.bitbucket.validation.groups.TrustedUpdate;
import com.atlassian.bitbucket.validation.groups.Update;
import com.atlassian.stash.internal.validation.AbstractValidationTest;
import com.atlassian.stash.internal.validation.ProjectValidator;
import com.google.common.collect.ImmutableMap;
import org.apache.commons.lang3.StringUtils;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorFactory;
import javax.validation.constraints.Pattern;
import java.util.Map;

import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.when;

/**
 * Tests validation as the annotations are applied to {@link InternalNormalProject}. Note that this test does
 * <i>not</i> cover uniqueness tests for key/name; those permutations are handled by the unit tests for the
 * validator classes that actually apply the constraints.
 */
@RunWith(MockitoJUnitRunner.class)
public class InternalNormalProjectTest extends AbstractValidationTest {

    private static final String KEY_PATTERN_MESSAGE = getPatternMessageInList(Project.class, "getKey", Project.KEY_REGEXP);
    private static final String KEY_TRUSTED_PATTERN_MESSAGE = getPatternMessageInList(Project.class, "getKey", Project.KEY_TRUSTED_REGEXP);
    private static final String NAME_PATTERN_MESSAGE = getMessage(Project.class, "getName", Pattern.class);

    @Mock
    private ProjectService projectService;
    @Mock
    private SecurityService securityService;
    @Mock
    private EscalatedSecurityContext securityContext;

    @Before
    public void setup() {
        when(securityService.withPermission(eq(Permission.PROJECT_VIEW), anyString())).thenReturn(securityContext);
    }

    @Test
    public void testValidation() {
        ImmutableMap<String, String> expected = ImmutableMap.<String, String>builder()
                .put("description", OPTIONAL_MESSAGE)
                .put("key", REQUIRED_MESSAGE)
                .put("name", REQUIRED_MESSAGE)
                .build();

        InternalNormalProject project = new InternalNormalProject.Builder()
                .description(StringUtils.repeat("DdDd", 64)) //256 character description
                .key(StringUtils.repeat("k", 129)) //129 character key
                .name(StringUtils.repeat("n", 129)) //129 character name
                .build();
        validate(project, expected);
        validate(project, expected, Create.class);
        validate(project, expected, TrustedCreate.class);
        validate(project, expected, TrustedUpdate.class);
    }

    @Test
    public void testValidationOnCreateForKey() {
        validateKeyFor(Create.class, KEY_PATTERN_MESSAGE, false);
    }

    @Test
    public void testValidationOnCreateForName() {
        validateNameFor(Create.class, NAME_PATTERN_MESSAGE);
    }

    @Test
    public void testValidationOnTrustedCreateForKey() {
        validateKeyFor(TrustedCreate.class, KEY_TRUSTED_PATTERN_MESSAGE, true);
    }

    @Test
    public void testValidationOnTrustedUpdateForKey() {
        validateKeyFor(TrustedUpdate.class, KEY_TRUSTED_PATTERN_MESSAGE, true);
    }

    @Test
    public void testValidationOnUpdateForKey() {
        validateKeyFor(Update.class, KEY_PATTERN_MESSAGE, false);
    }

    @Test
    public void testValidationOnUpdateForName() {
        validateNameFor(Update.class, NAME_PATTERN_MESSAGE);
    }

    public static InternalNormalProject createValidProject() {
        return new InternalNormalProject.Builder().key("KEY").name("Name").build();
    }

    @Override
    protected ConstraintValidatorFactory buildConstraintValidatorFactory() {
        return new ConstraintValidatorFactory() {

            @Override
            public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) {
                try {
                    T instance = key.newInstance();
                    if (instance instanceof ProjectValidator) {
                        ProjectValidator<?> validator = (ProjectValidator) instance;
                        validator.setProjectService(projectService);
                        validator.setSecurityService(securityService);
                    }
                    return instance;
                } catch (Exception e) {
                    throw new RuntimeException("Problem creating instance of " + key.getCanonicalName(), e);
                }
            }

            @Override
            public void releaseInstance(ConstraintValidator<?, ?> constraintValidator) {
                // Intentionally not implemented
            }
        };
    }

    private void validateKeyFor(Class<?> group, String pattern, boolean personalProjectKeysValid) {
        //Create a base project which passes validation, to allow focusing on key validation cases
        InternalNormalProject base = createValidProject();
        validate(base, group);

        //Positive validation

        //Can contain lowercase letters
        validate(base.copy().key("key").build(), group);
        //Can contain numbers after starting with a letter
        validate(base.copy().key("A1").build(), group);
        //Can contain underscores between words
        validate(base.copy().key("A_B").build(), group);
        //Can contain hyphens after the first character
        validate(base.copy().key("CAV-MASTER").build(), group);

        if (personalProjectKeysValid) {
            validate(base.copy().key("~user").build(), group);
        }

        //Negative validation
        Map<String, String> expected = ImmutableMap.of("key", pattern);

        //Can't start with a number
        validate(base.copy().key("1A").build(), expected, group);
        //Can't start with a hyphen
        validate(base.copy().key("-A1").build(), expected, group);
        //Can't contain punctuation
        validate(base.copy().key("A.B").build(), expected, group);
        if (!personalProjectKeysValid) {
            //Can't use a personal project key
            validate(base.copy().key("~user").build(), expected, group);
        }
    }

    private void validateNameFor(Class<?> group, String pattern) {
        InternalNormalProject base = createValidProject();
        validate(base, group);

        validate(base.copy().name("name").build(), group);

        //Can't use a personal project name
        Map<String, String> expected = ImmutableMap.of("name", pattern);
        validate(base.copy().name("~name").build(), expected, group);
    }
}
