package com.atlassian.stash.internal.license;

import com.atlassian.bitbucket.topic.TopicService;
import com.atlassian.crowd.embedded.api.Directory;
import com.atlassian.crowd.embedded.api.DirectorySynchronisationInformation;
import com.atlassian.crowd.event.group.GroupMembershipCreatedEvent;
import com.atlassian.crowd.event.group.GroupMembershipDeletedEvent;
import com.atlassian.crowd.event.user.AutoUserUpdatedEvent;
import com.atlassian.crowd.event.user.UserDeletedEvent;
import com.atlassian.crowd.event.user.UserEditedEvent;
import com.atlassian.crowd.event.user.UserRenamedEvent;
import com.atlassian.crowd.manager.directory.SynchronisationStatusManager;
import com.atlassian.crowd.model.membership.MembershipType;
import com.atlassian.crowd.model.user.User;
import com.atlassian.hazelcast.test.HazelcastCluster;
import com.atlassian.bitbucket.event.permission.GlobalPermissionGrantedEvent;
import com.atlassian.scheduler.JobRunner;
import com.atlassian.scheduler.JobRunnerRequest;
import com.atlassian.scheduler.SchedulerService;
import com.atlassian.scheduler.SchedulerServiceException;
import com.atlassian.scheduler.config.JobConfig;
import com.atlassian.scheduler.config.JobId;
import com.atlassian.scheduler.config.JobRunnerKey;
import com.atlassian.stash.internal.crowd.CrowdControl;
import com.atlassian.stash.internal.user.InternalPermissionService;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.bitbucket.util.PageUtils;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.ArgumentCaptor;
import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;

import java.util.Collections;
import java.util.List;
import java.util.Set;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class DefaultLicensedUserCountCacheTest {

    @Rule
    public final HazelcastCluster hazelcast = new HazelcastCluster.Builder().standalone().build();

    private DefaultLicensedUserCountCache cache;
    @Mock
    private CrowdControl crowdControl;
    @Mock
    private SynchronisationStatusManager crowdSyncStatusManager;
    private JobRunner jobRunner;
    @Mock
    private InternalPermissionService permissionService;
    @Mock
    private SchedulerService schedulerService;
    @Mock
    private DirectorySynchronisationInformation syncInfo;
    @Mock
    private TopicService topicService;

    @Before
    public void setUp() throws SchedulerServiceException {
        when(crowdSyncStatusManager.getDirectorySynchronisationInformation(isA(Directory.class)))
                .thenReturn(syncInfo);
        cache = new DefaultLicensedUserCountCache(crowdControl, crowdSyncStatusManager, permissionService,
                schedulerService, topicService);
        cache.schedule(schedulerService);
        ArgumentCaptor<JobRunner> captor = ArgumentCaptor.forClass(JobRunner.class);
        verify(schedulerService).registerJobRunner(any(JobRunnerKey.class), captor.capture());
        assertNotNull(captor.getValue());
        jobRunner = captor.getValue();
    }

    @Test
    public void testCacheClearingNestedGroup() throws SchedulerServiceException {
        when(permissionService.hasGlobalGroupPermission(eq(Permission.LICENSED_USER), eq("group"))).thenReturn(false);
        when(permissionService.getGrantedGroups(eq(Permission.LICENSED_USER), Matchers.any(PageRequest.class)))
                .thenAnswer(new Answer<Page<String>>() {
                    @Override
                    public Page<String> answer(InvocationOnMock invocation) throws Throwable {
                        PageRequest pageRequest = (PageRequest) invocation.getArguments()[1];
                        List<String> values = Collections.singletonList("outergroup");
                        return PageUtils.createPage(values, pageRequest);
                    }
                });

        when(crowdControl.isGroupMemberOfGroup("group", "outergroup")).thenReturn(true);

        Directory directory = mock(Directory.class);
        GroupMembershipCreatedEvent event
                = new GroupMembershipCreatedEvent(this, directory, "user", "group", MembershipType.GROUP_USER);

        cache.onGroupMembershipCreatedEvent(event);

        ensureCacheRepopulationWasScheduled();
    }

    @Test
    public void testCacheClearingGroup() throws SchedulerServiceException {
        when(permissionService.hasGlobalGroupPermission(eq(Permission.LICENSED_USER), eq("group"))).thenReturn(true);

        Directory directory = mock(Directory.class);
        GroupMembershipCreatedEvent event
                = new GroupMembershipCreatedEvent(this, directory, "user", "group", MembershipType.GROUP_USER);

        cache.onGroupMembershipCreatedEvent(event);

        ensureCacheRepopulationWasScheduled();
    }

    @Test
    public void testCacheClearingGroupDeletion() throws SchedulerServiceException {
        when(permissionService.hasGlobalGroupPermission(eq(Permission.LICENSED_USER), eq("group"))).thenReturn(true);

        Directory directory = mock(Directory.class);
        GroupMembershipDeletedEvent event
                = new GroupMembershipDeletedEvent(this, directory, "user", "group", MembershipType.GROUP_USER);

        cache.onGroupMembershipDeletedEvent(event);

        ensureCacheRepopulationWasScheduled();
    }

    @Test
    public void testCacheIsNotClearedDuringSynchronization() {
        when(syncInfo.isSynchronising()).thenReturn(true);
        when(permissionService.hasGlobalGroupPermission(eq(Permission.LICENSED_USER), eq("group"))).thenReturn(true);

        Directory directory = mock(Directory.class);
        GroupMembershipCreatedEvent event
                = new GroupMembershipCreatedEvent(this, directory, "user", "group", MembershipType.GROUP_USER);

        cache.onGroupMembershipCreatedEvent(event);

        verifyZeroInteractions(schedulerService);
    }

    @Test
    public void testPermissionGrantedClearing() throws SchedulerServiceException {
        ApplicationUser testUser = mock(ApplicationUser.class);

        GlobalPermissionGrantedEvent event = new GlobalPermissionGrantedEvent(this, Permission.LICENSED_USER,
                null, testUser);
        cache.onPermissionGrantedEvent(event);

        ensureCacheRepopulationWasScheduled();
    }

    @Test
    public void testRescheduleIfCacheClearedDuringRepopulation() throws SchedulerServiceException {
        cache.onUserDeletedEvent(new UserDeletedEvent(this, mock(Directory.class), "blah"));
        ensureCacheRepopulationWasScheduled();
        reset(schedulerService); // Required to reset the verify count

        cache.onUserDeletedEvent(new UserDeletedEvent(this, mock(Directory.class), "blah"));
        ensureCacheRepopulationWasScheduled();

        when(permissionService.getUsersWithPermission(eq(Permission.LICENSED_USER))).thenAnswer(new Answer<Set<String>>() {
            @Override
            public Set<String> answer(InvocationOnMock invocation) throws Throwable {
                // This is a little ugly but the best way to simulate an asynchronous event
                // in a synchronous test
                cache.onUserDeletedEvent(new UserDeletedEvent(this, mock(Directory.class), "blah"));
                return Collections.singleton("ganondorf");
            }
        });

        jobRunner.runJob(mock(JobRunnerRequest.class));
        verify(permissionService).getUsersWithPermission(eq(Permission.LICENSED_USER));
        assertEquals(1, cache.getCount());
    }

    @Test
    public void testDelayedRescheduleIfRepopulationCausedException() throws Exception {
        when(permissionService.getUsersWithPermission(Permission.LICENSED_USER)).thenThrow(new RuntimeException());
        cache.onUserDeletedEvent(new UserDeletedEvent(this, mock(Directory.class), "blah"));
        ensureCacheRepopulationWasScheduled();
        reset(schedulerService);
        jobRunner.runJob(mock(JobRunnerRequest.class));
        ensureCacheRepopulationWasScheduled();
    }

    @Test
    public void testOnUserAutoUpdatedActiveChanged() throws SchedulerServiceException {
        User oldUser = mock(User.class);
        User newUser = mock(User.class);
        when(newUser.isActive()).thenReturn(true);
        cache.onUserAutoUpdatedEvent(new AutoUserUpdatedEvent(this, mock(Directory.class), newUser, oldUser));
        ensureCacheRepopulationWasScheduled();
    }

    @Test
    public void testOnUserAutoUpdatedActiveNotChanged() {
        User oldUser = mock(User.class);
        User newUser = mock(User.class);
        cache.onUserAutoUpdatedEvent(new AutoUserUpdatedEvent(this, mock(Directory.class), newUser, oldUser));
        verifyNoMoreInteractions(schedulerService);
    }

    @Test
    public void testOnUserEditedActiveChanged() throws SchedulerServiceException {
        User oldUser = mock(User.class);
        User newUser = mock(User.class);
        when(newUser.isActive()).thenReturn(true);
        cache.onUserEditedEvent(new UserEditedEvent(this, mock(Directory.class), newUser, oldUser));
        ensureCacheRepopulationWasScheduled();
    }

    @Test
    public void testOnUserEditedActiveNotChanged() {
        User oldUser = mock(User.class);
        User newUser = mock(User.class);
        cache.onUserEditedEvent(new UserEditedEvent(this, mock(Directory.class), newUser, oldUser));
        verifyNoMoreInteractions(schedulerService);
    }

    @Test
    public void testOnUserRenamed() throws SchedulerServiceException {
        cache.onUserRenamedEvent(new UserRenamedEvent(this, mock(Directory.class), mock(User.class), "old-username"));
        ensureCacheRepopulationWasScheduled();
    }

    private void ensureCacheRepopulationWasScheduled() throws SchedulerServiceException {
        verify(schedulerService).scheduleJob(any(JobId.class), any(JobConfig.class));
    }

}