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

import com.atlassian.bitbucket.auth.AuthenticationRequiredException;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.i18n.KeyedMessage;
import com.atlassian.bitbucket.internal.mirroring.mirror.dao.AoProjectMapping;
import com.atlassian.bitbucket.internal.mirroring.mirror.dao.AoRepositoryMapping;
import com.atlassian.bitbucket.internal.mirroring.mirror.dao.ProjectMappingDao;
import com.atlassian.bitbucket.internal.mirroring.mirror.dao.RepositoryMappingDao;
import com.atlassian.bitbucket.mirroring.mirror.*;
import com.atlassian.bitbucket.permission.PermissionValidationService;
import com.atlassian.bitbucket.project.Project;
import com.atlassian.bitbucket.repository.NoSuchRepositoryException;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.repository.RepositoryCloneLinksRequest;
import com.atlassian.bitbucket.repository.RepositoryService;
import com.atlassian.bitbucket.server.ApplicationPropertiesService;
import com.atlassian.bitbucket.server.IncompatibleApplicationModeException;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.user.EscalatedSecurityContext;
import com.atlassian.bitbucket.user.SecurityService;
import com.atlassian.bitbucket.util.NamedLink;
import com.atlassian.bitbucket.util.Operation;
import com.atlassian.bitbucket.util.SimpleNamedLink;
import com.google.common.collect.ImmutableSet;
import org.junit.Before;
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.Arrays;
import java.util.Date;

import static com.atlassian.bitbucket.mirroring.mirror.MirroringMode.ALL_PROJECTS;
import static com.atlassian.bitbucket.mirroring.mirror.MirroringMode.SELECTED_PROJECTS;
import static java.util.Optional.of;
import static org.junit.Assert.*;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.anyObject;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class DefaultMirroredRepositoryServiceTest {

    private static final String EXTERNAL_PROJECT_ID = "99";
    private static final String EXTERNAL_REPO_ID = "42";
    public static final int LOCAL_REPO_ID = 1;
    public static final int LOCAL_PROJECT_ID = 2;
    public static final String UPSTREAM_ID = "upstreamId";
    @Mock
    private MirroringConfig config;
    @Mock
    private I18nService i18nService;
    @Mock
    private InternalUpstreamService upstreamService;
    @Mock
    private MirrorAuthenticationContext mirrorAuthenticationContext;
    @Mock
    private PermissionValidationService permissionValidationService;
    @Mock
    private Project project;
    @Mock
    private AoProjectMapping projectMapping;
    @Mock
    private ProjectMappingDao projectMappingDao;
    @Mock
    private ApplicationPropertiesService propertiesService;
    @Mock
    private Repository repository;
    @Mock
    private AoRepositoryMapping repoMapping;
    @Mock
    private RepositoryMappingDao repositoryMappingDao;
    @Mock
    private RepositoryService repositoryService;
    @Mock
    private SecurityService securityService;
    @Mock
    private UpstreamSettingsService upstreamSettingsService;

    @InjectMocks
    private DefaultMirroredRepositoryService mirrorRepoInfoService;

    @Before
    public void setUp() throws Throwable {
        when(i18nService.createKeyedMessage(anyString()))
                .thenReturn(new KeyedMessage("error.key", "ERROR-MESSAGE", ""));

        when(i18nService.createKeyedMessage(anyString(), any()))
                .thenReturn(new KeyedMessage("error.key", "ERROR-MESSAGE", ""));

        when(upstreamService.isMirror()).thenReturn(true);
        InternalUpstreamServer upstream = mockUpstream();
        when(upstreamService.get()).thenReturn(upstream);
        when(config.getDisplayName()).thenReturn("Mars Mirror");

        when(repositoryMappingDao.getByUpstreamId(eq(UPSTREAM_ID), eq(EXTERNAL_REPO_ID))).thenReturn(repoMapping);
        when(repositoryMappingDao.getByLocalId(eq(LOCAL_REPO_ID))).thenReturn(repoMapping);
        when(repoMapping.getExternalId()).thenReturn(EXTERNAL_REPO_ID);
        when(repoMapping.getLocalId()).thenReturn(LOCAL_REPO_ID);
        when(repoMapping.getUpstreamId()).thenReturn(UPSTREAM_ID);

        when(projectMappingDao.getByLocalId(eq(LOCAL_PROJECT_ID))).thenReturn(projectMapping);
        when(projectMapping.getExternalId()).thenReturn(EXTERNAL_PROJECT_ID);
        when(projectMapping.getLocalId()).thenReturn(LOCAL_PROJECT_ID);
        when(projectMapping.getUpstreamId()).thenReturn(UPSTREAM_ID);

        when(project.getId()).thenReturn(LOCAL_PROJECT_ID);

        when(repository.getId()).thenReturn(LOCAL_REPO_ID);
        when(repository.getProject()).thenReturn(project);
        when(repositoryService.getById(eq(repoMapping.getLocalId()))).thenReturn(repository);

        ApplicationUser user = mockUser();
        when(mirrorAuthenticationContext.getImpersonatedUser()).thenReturn(of(user));

        when(repositoryService.getCloneLinks(any(RepositoryCloneLinksRequest.class))).thenReturn(ImmutableSet.of(
                new SimpleNamedLink("http://clone", "http")
        ));

        EscalatedSecurityContext context = mock(EscalatedSecurityContext.class);
        when(context.call(anyObject()))
                .thenAnswer(invocation -> ((Operation) invocation.getArguments()[0]).perform());
        when(securityService.withPermission(any(), anyString())).thenReturn(context);

        when(upstreamSettingsService.getSettings(UPSTREAM_ID)).thenReturn(of(upstreamSettings(ALL_PROJECTS)));

        when(config.getMaxSyncFailedCount()).thenReturn(2);
    }

    @Test(expected = IncompatibleApplicationModeException.class)
    public void testGetMirroredRepositoryWhenNotMirror() {
        when(upstreamService.isMirror()).thenReturn(false);

        mirrorRepoInfoService.getRepository("bla", "blub");
    }

    @Test(expected = NoSuchRepositoryException.class)
    public void testGetNoSuchMirroredRepository() {
        when(repositoryService.getById(anyInt())).thenReturn(null);

        mirrorRepoInfoService.getRepository("bla", "blub");
    }

    @Test
    public void testGetMirroredRepository() {
        MirroredRepository repoInfo = mirrorRepoInfoService.getRepository(UPSTREAM_ID, EXTERNAL_REPO_ID);

        NamedLink firstLink = repoInfo.getCloneUrls().iterator().next();
        assertEquals("HTTP Clone URL", "http://clone", firstLink.getHref());
        assertEquals("HTTP Clone Name", "http", firstLink.getName());
        assertEquals("Upstream Repository ID", EXTERNAL_REPO_ID, repoInfo.getRepositoryId());
    }

    @Test(expected = NoSuchRepositoryException.class)
    public void testGetMirroredRepositoryWithNoMapping() {
        when(repositoryMappingDao.getByUpstreamId(anyString(), anyString())).thenReturn(null);

        mirrorRepoInfoService.getRepository(UPSTREAM_ID, EXTERNAL_REPO_ID);
    }

    @Test
    public void testGetMirroredRepositoryWithInitialSync() {
        when(repoMapping.getInitialSyncDate()).thenReturn(new Date());

        MirroredRepository repoInfo = mirrorRepoInfoService.getRepository(UPSTREAM_ID, EXTERNAL_REPO_ID);

        assertTrue(repoInfo.isAvailable());
    }

    @Test
    public void testGetMirroredRepositoryValidates() {
        when(repositoryService.getById(anyInt())).thenReturn(repository);

        mirrorRepoInfoService.getRepository(UPSTREAM_ID, EXTERNAL_REPO_ID);

        // This ensures repositoryService.getById is called, as we rely on it to validate
        // access to the repo with a @PostAuthorize. If you are refactoring the MirrorRepositoryInfoService,
        // make sure to keep this in mind.
        verify(repositoryService).getById(LOCAL_REPO_ID);
        verify(repositoryService).getCloneLinks(any());
        verifyNoMoreInteractions(repositoryService);
    }

    @Test(expected = AuthenticationRequiredException.class)
    public void testGetStatusWhenNoAuthentication() {
        doThrow(new AuthenticationRequiredException(
                new KeyedMessage("shouldnt.have.done.that", "Nope", ""), null, null
        )).when(permissionValidationService).validateRepositoryAccessible(repository);

        mirrorRepoInfoService.getStatus(repository);
    }

    @Test
    public void testGetStatusWhenNoProjectMapping() {
        when(projectMappingDao.getByLocalId(anyInt())).thenReturn(null);

        assertSame(MirroredRepositoryStatus.NOT_MIRRORED, mirrorRepoInfoService.getStatus(repository));
    }

    @Test
    public void testGetStatusWhenNoRepositoryMapping() {
        when(repositoryMappingDao.getByLocalId(anyInt())).thenReturn(null);

        assertSame(MirroredRepositoryStatus.NOT_MIRRORED, mirrorRepoInfoService.getStatus(repository));
    }

    @Test
    public void testGetStatusWhenNotMirrored() {
        when(upstreamSettingsService.getSettings(UPSTREAM_ID))
                .thenReturn(of(upstreamSettings(SELECTED_PROJECTS, EXTERNAL_PROJECT_ID + "-whatever")));

        assertSame(MirroredRepositoryStatus.NOT_MIRRORED, mirrorRepoInfoService.getStatus(repository));
    }

    @Test
    public void testGetStatusWhenMirrorInitialised() {
        when(repoMapping.getInitialSyncDate()).thenReturn(new Date());

        assertSame(MirroredRepositoryStatus.AVAILABLE, mirrorRepoInfoService.getStatus(repository));
    }

    @Test
    public void testGetStatusWhenMirrorInitialising() {
        when(repoMapping.getInitialSyncDate()).thenReturn(null);

        assertSame(MirroredRepositoryStatus.INITIALIZING, mirrorRepoInfoService.getStatus(repository));
    }

    @Test
    public void testGetStatusWhenMirrorUninitialisedAndInError() {
        when(repoMapping.getInitialSyncDate()).thenReturn(null);
        when(repoMapping.getFailedSyncCount()).thenReturn(2);

        assertSame(MirroredRepositoryStatus.ERROR_INITIALIZING, mirrorRepoInfoService.getStatus(repository));
    }

    @Test
    public void testGetStatusWhenMirrorInitialisedAndInError() {
        when(repoMapping.getInitialSyncDate()).thenReturn(new Date());
        when(repoMapping.getFailedSyncCount()).thenReturn(2);

        assertSame(MirroredRepositoryStatus.ERROR_AVAILABLE, mirrorRepoInfoService.getStatus(repository));
    }

    private static InternalUpstreamServer mockUpstream() {
        InternalUpstreamServer upstream = mock(InternalUpstreamServer.class);
        when(upstream.getId()).thenReturn("UPSTREAM");
        when(upstream.getNamespace()).thenReturn("bitbucket");
        return upstream;
    }

    private static ApplicationUser mockUser() {
        ApplicationUser user = mock(ApplicationUser.class);
        when(user.getDisplayName()).thenReturn("Impersonator");
        when(user.getId()).thenReturn(80);
        when(user.getSlug()).thenReturn("impersonator");
        when(user.isActive()).thenReturn(true);

        return user;
    }

    private UpstreamSettings upstreamSettings(MirroringMode mode, String ...projectIds) {
        return new SimpleUpstreamSettings.Builder()
                .mode(mode)
                .projectIds(Arrays.asList(projectIds))
                .build();
    }
}
