package com.atlassian.stash.internal.repository.ref.restriction.rest;

import com.atlassian.bitbucket.NoSuchEntityException;
import com.atlassian.bitbucket.auth.AuthenticationContext;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.i18n.KeyedMessage;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.permission.PermissionService;
import com.atlassian.bitbucket.repository.RefService;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.repository.ResolveRefRequest;
import com.atlassian.bitbucket.repository.ref.restriction.*;
import com.atlassian.bitbucket.rest.enrich.AvatarEnricher;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.user.TestApplicationUser;
import com.atlassian.bitbucket.user.UserService;
import com.atlassian.bitbucket.util.PageImpl;
import com.atlassian.bitbucket.util.PageRequestImpl;
import com.atlassian.stash.internal.repository.ref.restriction.DefaultRefRestrictionService;
import com.atlassian.stash.internal.repository.ref.restriction.SimpleRefRestriction;
import com.atlassian.stash.internal.repository.ref.restriction.provider.BranchMatcher;
import com.atlassian.stash.internal.repository.ref.restriction.provider.BranchMatcherProvider;
import com.atlassian.stash.internal.repository.ref.restriction.provider.BranchRestrictionType;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Set;

import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class RefRestrictionResourceTest {

    public static final int REF_RESTRICTION_ID = 1;
    public static final int REPOSITORY_ID = 2;
    public static final int RESTRICTION_ID = 3;
    public static final String INVALID_MATCHER_TYPE = "invalid-matcher-type";
    public static final String BRANCH_PROVIDER_ID = "BRANCH";
    public static final String PROVIDER_TARGET = "main";

    private final Repository repository = mock(Repository.class);

    @Mock
    private AvatarEnricher avatarEnricher;
    @Mock
    private UserService userService;
    @Mock
    private RefMatcherProviderRegistry refMatcherProviderRegistry;
    @Mock
    private AuthenticationContext authenticationContext;
    @Mock
    private I18nService i18nService;
    @Mock
    private PermissionService permissionService;
    @Mock
    private DefaultRefRestrictionService restrictionService;
    @Mock
    private RefService refService;

    @InjectMocks
    private RefRestrictionResource underTest;

    @Test
    public void createRestrictionWithNullMatcherGetCorrectMessageForError() {
        underTest.createRestriction(repository, requestWithNullMatcher());
        underTest.createRestrictions(repository, ImmutableList.of(requestWithNullMatcher()));

        verify(i18nService, times(2)).getMessage(RefRestrictionResource.MATCHER_REQUIRED);
    }

    @Test
    public void createRestrictionWithInvalidMatcherTypeGetCorrectErrorMessage() {
        underTest.createRestriction(repository, createRequest(invalidTypeMatcher()));
        underTest.createRestrictions(repository, ImmutableList.of(createRequest(invalidTypeMatcher())));

        verify(i18nService, times(2)).getMessage(RefRestrictionResource.INVALID_MATCHER_TYPE, "invalid");
    }

    @Test
    public void createRestrictionLooksUpMatcherProviderInRegistry() {
        underTest.createRestriction(repository, createRequest(validMatcher()));
        underTest.createRestrictions(repository, ImmutableList.of(createRequest(validMatcher())));

        verify(refMatcherProviderRegistry, times(2)).getProvider(BRANCH_PROVIDER_ID);
    }

    @Test
    public void createRestrictionChecksThatGroupsExist() {
        underTest.createRestriction(repository, createRequest(validMatcher(), validGroups()));
        underTest.createRestrictions(repository, ImmutableList.of(createRequest(validMatcher(), validGroups())));

        verify(userService, times(4)).existsGroup(startsWith("valid-group-"));
    }

    @Test
    public void createRestrictionWithInvalidGroupsGetsCorrectErrorMessage() {
        when(userService.existsGroup(startsWith("invalid-group"))).thenReturn(false);

        underTest.createRestriction(repository, createRequest(validMatcher(), invalidGroups()));
        underTest.createRestrictions(repository, ImmutableList.of(createRequest(validMatcher(), invalidGroups())));

        verify(i18nService, times(4)).getMessage(eq(RefRestrictionResource.GROUP_INVALID), startsWith("invalid-group"));
    }

    @Test
    public void createRestrictionWithInvalidUsersGetsCorrectErrorMessage() {
        doReturn(stashUsers()).when(userService).getUsersByName(users());

        underTest.createRestriction(repository, createRequest(validMatcher(), users()));
        underTest.createRestrictions(repository, ImmutableList.of(createRequest(validMatcher(), users())));

        verify(i18nService, times(4)).getMessage(eq(RefRestrictionResource.INVALID_USER), startsWith("invalid-user"));
        verify(i18nService, never()).getMessage(eq(RefRestrictionResource.INVALID_USER), startsWith("valid-user"));
    }

    @Test
    public void createRestrictionChecksUserIsLicenced() {
        doReturn(stashUsers()).when(userService).getUsersByName(users());

        underTest.createRestriction(repository, createRequest(validMatcher(), users()));
        underTest.createRestrictions(repository, ImmutableList.of(createRequest(validMatcher(), users())));

        verify(permissionService, times(2)).hasGlobalPermission(new TestApplicationUser("valid-user-1"), Permission.LICENSED_USER);
        verify(permissionService, times(2)).hasGlobalPermission(new TestApplicationUser("valid-user-2"), Permission.LICENSED_USER);
        verify(permissionService, never()).hasGlobalPermission(new TestApplicationUser("invalid-user-2"), Permission.LICENSED_USER);
        verify(permissionService, never()).hasGlobalPermission(new TestApplicationUser("invalid-user-2"), Permission.LICENSED_USER);
    }

    @Test
    public void createRestrictionChecksInvalidRestrictionTypeCorrectErrorMessage() {
        RestRestrictionRequest request = mock(RestRestrictionRequest.class);
        when(request.getType()).thenReturn("foobar");
        doReturn(stashUsers()).when(userService).getUsersByName(users());

        underTest.createRestriction(repository, request);
        underTest.createRestrictions(repository, ImmutableList.of(request));

        verify(i18nService, times(2)).getMessage(RefRestrictionResource.INVALID_RESTRICTION_TYPE, "foobar");
    }

    @Test
    public void createRestrictionSetsRestrictionWhenNoErrors() {
        RefMatcherProvider refMatcherProvider = mock(RefMatcherProvider.class);
        when(refMatcherProvider.create(repository, PROVIDER_TARGET)).thenReturn(validMatcher());
        when(refMatcherProviderRegistry.getProvider(BRANCH_PROVIDER_ID)).thenReturn(refMatcherProvider);
        when(userService.existsGroup(startsWith("valid-group"))).thenReturn(true);
        when(permissionService.hasGlobalPermission(any(ApplicationUser.class), eq(Permission.LICENSED_USER))).thenReturn(true);
        when(restrictionService.setRestriction(isA(SetRestrictionRequest.class))).thenReturn(simpleRefRestriction(validMatcher()));
        doReturn(stashUsers()).when(userService).getUsersByName(validUsers());

        underTest.createRestriction(repository, createRequest(validMatcher(), validGroups(), validUsers()));
        underTest.createRestrictions(repository, ImmutableList.of(createRequest(validMatcher(), validGroups(), validUsers())));

        verify(restrictionService).setRestriction(isA(SetRestrictionRequest.class));
        verify(restrictionService).setRestrictions(anyListOf(SetRestrictionRequest.class));
    }

    @Test
    public void getRestrictionCallsRestrictionServer() {

        RefRestriction restriction = simpleRefRestriction(validMatcher());
        when(restrictionService.getById(RESTRICTION_ID)).thenReturn(restriction);

        underTest.getRestriction(RESTRICTION_ID);

        verify(restrictionService).getById(RESTRICTION_ID);
    }

    @Test(expected = NoSuchEntityException.class)
    public void getRestrictionsDoesNotExistCorrectErrorMessage() {
        when(i18nService.createKeyedMessage(RefRestrictionResource.RESTRICTION_NOTFOUND, RESTRICTION_ID))
                .thenReturn(new KeyedMessage("", "", ""));

        underTest.getRestriction(RESTRICTION_ID);

        verify(i18nService).createKeyedMessage(RefRestrictionResource.RESTRICTION_NOTFOUND, RESTRICTION_ID);
    }

    @Test
    public void deleteRestrictionCallsServiceFound() {
        when(restrictionService.removeRefRestriction(RESTRICTION_ID)).thenReturn(true);

        underTest.deleteRestriction(RESTRICTION_ID);

        verify(restrictionService).removeRefRestriction(RESTRICTION_ID);
    }

    @Test
    public void deleteRestrictionCallsServiceNotFound() {
        when(restrictionService.removeRefRestriction(RESTRICTION_ID)).thenReturn(false);

        underTest.deleteRestriction(RESTRICTION_ID);

        verify(restrictionService).removeRefRestriction(RESTRICTION_ID);
    }

    @Test
    public void getRestrictionGetCorrectErrorMessageForInvalidTypes() {
        underTest.getRestrictions(repository, Lists.newArrayList("no-deletes", "invalid-type"), null, null, null);

        verify(i18nService).getMessage(RefRestrictionResource.INVALID_RESTRICTION_TYPE, "invalid-type");
    }

    @Test
    public void getRestrictionsGetCorrectErrorMessageWhenInvalidMatcherType() {
        underTest.getRestrictions(repository, noDeletesRestriction(), INVALID_MATCHER_TYPE, null, null);

        verify(i18nService).getMessage(RefRestrictionResource.INVALID_MATCHER_TYPE, INVALID_MATCHER_TYPE);
    }

    @Test
    public void getRestrictionsGetNoErrorMessageWhenValidMatcherType() {
        PageRequestImpl pageRequest = new PageRequestImpl(1, 10);
        RefMatcherProvider provider = mock(BranchMatcherProvider.class);
        when(provider.getType()).thenReturn(new BranchRestrictionType());
        when(refMatcherProviderRegistry.getProvider(eq(BRANCH_PROVIDER_ID))).thenReturn(provider);
        when(restrictionService.search(isA(RestrictionSearchRequest.class), eq(pageRequest)))
                .thenReturn(new PageImpl<>(pageRequest, 0, Collections.emptyList(), true));

        underTest.getRestrictions(repository, noDeletesRestriction(), BRANCH_PROVIDER_ID, null, pageRequest);

        verifyZeroInteractions(i18nService);
    }

    private ArrayList<String> noDeletesRestriction() {
        return Lists.newArrayList("no-deletes");
    }

    @Test
    public void getRestrictionsCallsRestrictionService() {
        PageRequestImpl pageRequest = new PageRequestImpl(1, 10);

        RefMatcherProvider refMatcherProvider = mock(RefMatcherProvider.class);
        when(refMatcherProviderRegistry.getProvider(BRANCH_PROVIDER_ID)).thenReturn(refMatcherProvider);
        when(refMatcherProvider.create(repository, PROVIDER_TARGET)).thenReturn(validMatcher());
        when(restrictionService.search(isA(RestrictionSearchRequest.class), eq(pageRequest)))
                .thenReturn(new PageImpl<>(pageRequest, 0, Collections.emptyList(), true));

        underTest.getRestrictions(repository, noDeletesRestriction(), BRANCH_PROVIDER_ID, PROVIDER_TARGET, pageRequest);

        verify(restrictionService).search(isA(RestrictionSearchRequest.class), eq(pageRequest));
    }

    private RestRestrictionRequest createRequest(RefMatcher matcher) {
        return new RestRestrictionRequest(simpleRefRestriction(matcher),
                new ArrayList<>(validGroups()), new ArrayList<>(users()));
    }

    private RestRestrictionRequest createRequest(RefMatcher matcher, Set<String> groups, Set<String> users) {
        return new RestRestrictionRequest(simpleRefRestriction(matcher),
                new ArrayList<>(groups), new ArrayList<>(users));
    }


    private RestRestrictionRequest createRequest(RefMatcher matcher, Set<String> groups) {
        return new RestRestrictionRequest(simpleRefRestriction(matcher),
                new ArrayList<>(groups), new ArrayList<>(users()));
    }

    private SimpleRefRestriction simpleRefRestriction(RefMatcher matcher) {
        return new SimpleRefRestriction(REF_RESTRICTION_ID, REPOSITORY_ID, matcher,
                RefRestrictionType.NO_DELETES, Collections.emptyList());
    }

    private RefMatcher invalidTypeMatcher() {
        return new UnknownRefMatcher.Builder()
                .matcherId("invalid")
                .typeId("invalid").build();
    }

    private Set<String> invalidGroups() {
        return Sets.newHashSet("invalid-group-1", "invalid-group-2");
    }

    private Set<String> validGroups() {
        return Sets.newHashSet("valid-group-1", "valid-group-2");
    }

    private RefMatcher validMatcher() {
        return new BranchMatcher(PROVIDER_TARGET, true,
                () ->  refService.resolveRef(new ResolveRefRequest.Builder(repository)
                        .refId(PROVIDER_TARGET)
                        .build()));
    }

    private Set<ApplicationUser> stashUsers() {

        ApplicationUser testStashUser1 = new TestApplicationUser("valid-user-1");
        ApplicationUser testStashUser2 = new TestApplicationUser("valid-user-2");
        return Sets.newHashSet(testStashUser1, testStashUser2);
    }

    private Set<String> validUsers() {
        return Sets.newHashSet("valid-user-1", "valid-user-2");
    }

    private Set<String> users() {
        return Sets.newHashSet("valid-user-1", "valid-user-2", "invalid-user-1", "invalid-user-2");
    }

    private RestRestrictionRequest requestWithNullMatcher() {
        return mock(RestRestrictionRequest.class);
    }
}