package com.atlassian.bitbucket.internal.mirroring.upstream;

import com.atlassian.bitbucket.auth.AuthenticationContext;
import com.atlassian.bitbucket.i18n.SimpleI18nService;
import com.atlassian.bitbucket.internal.mirroring.MirroringRequest;
import com.atlassian.bitbucket.internal.mirroring.MirroringRequestState;
import com.atlassian.bitbucket.internal.mirroring.SimpleMirroringRequest;
import com.atlassian.bitbucket.internal.mirroring.upstream.dao.AoMirrorServer;
import com.atlassian.bitbucket.internal.mirroring.upstream.dao.AoMirroringRequest;
import com.atlassian.bitbucket.internal.mirroring.upstream.dao.MirroringRequestDao;
import com.atlassian.bitbucket.mirroring.upstream.*;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.permission.PermissionValidationService;
import com.atlassian.bitbucket.server.ApplicationPropertiesService;
import com.atlassian.bitbucket.server.IncompatibleApplicationModeException;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.user.TestApplicationUser;
import com.atlassian.bitbucket.user.UserService;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.bitbucket.util.PageUtils;
import com.atlassian.event.api.EventPublisher;
import com.atlassian.sal.api.transaction.TransactionCallback;
import com.atlassian.sal.api.transaction.TransactionTemplate;
import com.google.common.base.Function;
import com.google.common.collect.Sets;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner;

import java.util.Arrays;
import java.util.Collections;
import java.util.Set;

import static com.atlassian.bitbucket.internal.mirroring.MirroringRequestState.*;
import static com.atlassian.bitbucket.internal.mirroring.upstream.MirrorsTestHelper.createMockAoMirroringRequest;
import static com.atlassian.bitbucket.internal.mirroring.upstream.MirrorsTestHelper.createSimpleMirroringRequest;
import static com.atlassian.bitbucket.internal.mirroring.upstream.UpstreamConstants.PROPERTY_MIRRORING_REGISTRATION_MAX_PENDING;
import static com.atlassian.bitbucket.server.ApplicationMode.DEFAULT;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class DefaultMirroringRequestServiceTest {

    @Mock
    private ApplicationPropertiesService applicationPropertiesService;
    @Mock
    private AuthenticationContext authenticationContext;
    @Mock
    private MirroringRequestDao dao;
    @Mock
    private EventPublisher eventPublisher;
    @Spy
    private SimpleI18nService i18nService;
    @Mock
    private MirrorAddonManager mirrorAddonManager;
    @Mock
    private InternalMirrorService mirrorService;
    @Mock
    private SmartMirroringFeature mirroringFeature;
    @Mock
    private PermissionValidationService permissionValidationService;
    @Mock
    private TransactionTemplate transactionTemplate;
    @Mock
    private UserService userService;
    @InjectMocks
    private DefaultMirroringRequestService service;

    @Before
    public void setUpMocks() throws Exception {
        when(transactionTemplate.execute(Matchers.<TransactionCallback<Object>>any())).then(invocationOnMock -> {
            TransactionCallback<?> callback = (TransactionCallback<?>) invocationOnMock.getArguments()[0];
            return callback.doInTransaction();
        });

        when(dao.findByMirrorId(anyString(), any(PageRequest.class)))
                .thenReturn(PageUtils.<AoMirroringRequest>createEmptyPage(PageUtils.newRequest(0, 1)));
        AoMirroringRequest createdMirroringRequest = mock(AoMirroringRequest.class);
        when(dao.create(any(MirroringRequest.class))).thenReturn(createdMirroringRequest);
        when(authenticationContext.getCurrentUser()).thenReturn(MirrorsTestHelper.DEFAULT_USER);

        when(applicationPropertiesService.getMode()).thenReturn(DEFAULT);

        when(applicationPropertiesService.getPluginProperty(
                Matchers.eq(PROPERTY_MIRRORING_REGISTRATION_MAX_PENDING), anyInt())).thenReturn(Integer.MAX_VALUE);
        when(dao.countInState(same(PENDING))).thenReturn(0);
    }

    @Test
    public void testAcceptingRequestsIsIdempotent() throws Exception {
        testResolutionIsIdempotent(ACCEPTED, request -> {
            service.accept(request);
            return null;
        });
    }

    @Test(expected = MirroringDisabledException.class)
    public void testAcceptThrowsWhenNotEnabled() throws Exception {
        doThrow(MirroringDisabledException.class).when(mirroringFeature).checkEnabled();

        MirrorAddon addon = mock(MirrorAddon.class);
        when(addon.getKey()).thenReturn("add-on-key");
        when(mirrorAddonManager.install(any(MirroringRequest.class))).thenReturn(addon);

        testCanResolveRequest(ACCEPTED, request -> {
            service.accept(request);
            return null;
        });
    }

    @Test
    public void testCanAcceptRequest() throws Exception {
        MirrorAddon addon = mock(MirrorAddon.class);
        when(addon.getKey()).thenReturn("add-on-key");
        when(mirrorAddonManager.install(any(MirroringRequest.class))).thenReturn(addon);

        testCanResolveRequest(ACCEPTED, request -> {
            service.accept(request);
            return null;
        });
    }

    @Test
    public void testCanDelete() throws Exception {
        SimpleMirroringRequest request = createSimpleMirroringRequest();
        service.delete(request);

        verify(dao).delete(request.getId());
        verify(permissionValidationService).validateForGlobal(Permission.SYS_ADMIN);
        verify(mirroringFeature).checkEnabled();
    }

    @Test
    public void testCanFindAllWhenNoneToFind() throws Exception {
        PageRequest pageRequest = PageUtils.newRequest(0, 1);

        when(dao.findAll(eq(pageRequest))).thenReturn(PageUtils.<AoMirroringRequest>createEmptyPage(pageRequest));

        Assert.assertEquals(0, service.findAll(pageRequest).getSize());

        //noinspection unchecked
        verify(userService, never()).getUsersById(any(Set.class));
        verify(permissionValidationService).validateForGlobal(Permission.SYS_ADMIN);
        verify(mirroringFeature).checkEnabled();
    }

    @Test
    public void testCanFindAllWhenSomeToFind() throws Exception {
        PageRequest pageRequest = PageUtils.newRequest(0, 2);

        TestApplicationUser user1 = new TestApplicationUser("user-1", 1);
        TestApplicationUser user2 = new TestApplicationUser("user-2", 2);

        AoMirroringRequest existingRequest1 = createMockAoMirroringRequest(1, MirrorsTestHelper.DEFAULT_MIRROR_ID + "-1", PENDING, user1);
        AoMirroringRequest existingRequest2 = createMockAoMirroringRequest(2, MirrorsTestHelper.DEFAULT_MIRROR_ID + "-2", PENDING, user2);

        when(dao.findAll(eq(pageRequest))).thenReturn(
                PageUtils.createPage(Arrays.asList(existingRequest1, existingRequest2), pageRequest));

        when(userService.getUsersById(eq(Sets.newHashSet(1, 2)))).thenReturn(Sets.<ApplicationUser>newHashSet(user1, user2));

        Assert.assertEquals(2, service.findAll(pageRequest).getSize());

        verify(userService).getUsersById(eq(Sets.newHashSet(1, 2)));
        verify(existingRequest1).initResolver(user1);
        verify(existingRequest2).initResolver(user2);
        verify(permissionValidationService).validateForGlobal(Permission.SYS_ADMIN);
        verify(mirroringFeature).checkEnabled();
    }

    @Test
    public void testCanGetById() throws Exception {
        service.getById(0);

        verify(dao).getById(0);
        verify(permissionValidationService).validateForGlobal(Permission.SYS_ADMIN);
        verify(mirroringFeature).checkEnabled();
    }

    @Test
    public void testCanRegisterNewRequest() throws Exception {
        MirroringRequest request = createSimpleMirroringRequest();

        service.register(request);

        //Verify no permission checking as requesting to act as a mirror cannot authenticate at this point
        verify(permissionValidationService, never()).validateForGlobal(any(Permission.class));

        verify(mirroringFeature).checkEnabled();

        verify(dao).create(same(request));

        verify(dao).countInState(same(PENDING));

        verify(eventPublisher).publish(any(MirroringRequestCreatedEvent.class));
    }

    @Test
    public void testCanRejectRequest() throws Exception {
        testCanResolveRequest(REJECTED, request -> {
            service.reject(request);
            return null;
        });
    }

    @Test(expected = MirrorAlreadyRegisteredException.class)
    public void testCantAcceptRequestIfMirrorAlreadyRegistered() throws Exception {
        SimpleMirroringRequest request = createSimpleMirroringRequest();

        AoMirroringRequest aoRequest = createMockAoMirroringRequest(MirrorsTestHelper.DEFAULT_MIRROR_ID, PENDING);
        when(dao.getById(request.getId())).thenReturn(aoRequest);

        AoMirrorServer mirror = mock(AoMirrorServer.class);
        when(mirrorService.getById(request.getMirrorId())).thenReturn(mirror);

        service.accept(request);

        verify(dao, never()).resolve(anyInt(), any(MirroringRequestState.class), anyInt());
    }

    @Test(expected = IncompatibleApplicationModeException.class)
    public void testCantPerformOperationInMirrorMode() throws Exception {
        doThrow(IncompatibleApplicationModeException.class).when(mirroringFeature).checkEnabled();

        service.register(createMockAoMirroringRequest(MirrorsTestHelper.DEFAULT_MIRROR_ID, REJECTED));
    }

    @Test(expected = IllegalMirroringRequestStateException.class)
    public void testCantRejectAlreadyAcceptedRequest() throws Exception {
        SimpleMirroringRequest request = createSimpleMirroringRequest();
        AoMirroringRequest aoRequest = createMockAoMirroringRequest(MirrorsTestHelper.DEFAULT_MIRROR_ID, ACCEPTED);

        when(dao.getById(request.getId())).thenReturn(aoRequest);

        service.reject(request);

        verify(dao, never()).resolve(anyInt(), any(MirroringRequestState.class), anyInt());
    }

    @Test(expected = MirroringRequestLimitExceededException.class)
    public void testRegisteringTooManyRequestsCausesException() throws Exception {
        when(applicationPropertiesService.getPluginProperty(
                Matchers.eq(PROPERTY_MIRRORING_REGISTRATION_MAX_PENDING), anyInt())).thenReturn(1);

        when(dao.countInState(same(PENDING))).thenReturn(1);

        service.register(createSimpleMirroringRequest());
    }

    @Test(expected = MirroringDisabledException.class)
    public void testRegisterThrowsWhenNotEnabled() {
        doThrow(MirroringDisabledException.class).when(mirroringFeature).checkEnabled();

        service.register(createSimpleMirroringRequest());
    }

    @Test
    public void testRejectingRequestsIsIdempotent() throws Exception {
        testResolutionIsIdempotent(REJECTED, request -> {
            service.reject(request);
            return null;
        });
    }

    @Test
    public void testReregisteringAlreadyPendingMirroringRequestReturnsOriginal() throws Exception {
        AoMirroringRequest request = createMockAoMirroringRequest(MirrorsTestHelper.DEFAULT_MIRROR_ID, PENDING);

        when(dao.findByMirrorId(anyString(), any(PageRequest.class)))
                .thenReturn(PageUtils.createPage(Collections.singleton(request), PageUtils.newRequest(0, 1)));

        service.register(request);

        verify(dao, never()).create(any(MirroringRequest.class));
    }

    @Test
    public void testReregisteringRejectedMirroringRequestCreatesNewRequest() throws Exception {
        AoMirroringRequest request = createMockAoMirroringRequest(MirrorsTestHelper.DEFAULT_MIRROR_ID, REJECTED);

        when(dao.findByMirrorId(anyString(), any(PageRequest.class)))
                .thenReturn(PageUtils.createPage(Collections.singleton(request), PageUtils.newRequest(0, 1)));

        service.register(request);

        verify(dao).create(same(request));
    }

    private void testCanResolveRequest(MirroringRequestState resolution, Function<SimpleMirroringRequest, Void> resolutionAction) {
        SimpleMirroringRequest request = createSimpleMirroringRequest();
        AoMirroringRequest aoRequest = createMockAoMirroringRequest(MirrorsTestHelper.DEFAULT_MIRROR_ID, PENDING);
        AoMirroringRequest resolvedRequest = createMockAoMirroringRequest(MirrorsTestHelper.DEFAULT_MIRROR_ID, resolution);

        when(dao.getById(request.getId())).thenReturn(aoRequest);
        when(dao.resolve(request.getId(), resolution, MirrorsTestHelper.DEFAULT_USER.getId())).thenReturn(resolvedRequest);

        resolutionAction.apply(request);

        verify(dao).resolve(request.getId(), resolution, MirrorsTestHelper.DEFAULT_USER.getId());
        verify(mirroringFeature).checkEnabled();
        verify(permissionValidationService).validateForGlobal(Permission.SYS_ADMIN);
        switch (resolution) {
            case ACCEPTED:
                verify(eventPublisher).publish(any(MirroringRequestAcceptedEvent.class));
                break;
            case REJECTED:
                verify(eventPublisher).publish(any(MirroringRequestRejectedEvent.class));
                break;
        }
    }

    private void testResolutionIsIdempotent(MirroringRequestState resolution, Function<SimpleMirroringRequest, Void> resolutionAction) {
        SimpleMirroringRequest request = createSimpleMirroringRequest();
        AoMirroringRequest aoRequest = createMockAoMirroringRequest(MirrorsTestHelper.DEFAULT_MIRROR_ID, resolution);

        when(dao.getById(request.getId())).thenReturn(aoRequest);

        resolutionAction.apply(request);

        verify(dao, never()).resolve(request.getId(), resolution, MirrorsTestHelper.DEFAULT_USER.getId());
        verify(permissionValidationService).validateForGlobal(Permission.SYS_ADMIN);
    }
}