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

import com.atlassian.bitbucket.internal.mirroring.mirror.MirrorKeyUploadFailedException;
import com.atlassian.bitbucket.internal.mirroring.mirror.MirroringConfig;
import com.atlassian.bitbucket.internal.mirroring.mirror.UpstreamUserHelper;
import com.atlassian.bitbucket.internal.mirroring.mirror.auth.SyncCredentials;
import com.atlassian.bitbucket.internal.mirroring.mirror.auth.SyncCredentialsManager;
import com.atlassian.bitbucket.internal.mirroring.repositories.RepositoryContentHashService;
import com.atlassian.bitbucket.mirroring.mirror.RepositorySynchronizedEvent;
import com.atlassian.bitbucket.project.Project;
import com.atlassian.bitbucket.repository.*;
import com.atlassian.bitbucket.scm.Command;
import com.atlassian.bitbucket.scm.ScmService;
import com.atlassian.bitbucket.scm.git.command.fetch.GitFetchAuthenticationException;
import com.atlassian.bitbucket.scm.mirror.*;
import com.atlassian.bitbucket.user.DummyEscalatedSecurityContext;
import com.atlassian.event.api.EventPublisher;
import com.atlassian.util.concurrent.Promises;
import com.google.common.collect.ImmutableList;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import java.util.Collections;

import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class RepositoryFetchProcessorTest {

    private static final String UPSTREAM_ID = "upstream";

    @Captor
    private ArgumentCaptor<MirrorSyncCallback> callbackCaptor;
    @Mock
    private MirroringConfig config;
    @Mock
    private Command<Void> command;
    @Mock
    private RepositoryContentHashService contentHashService;
    @Mock
    private SyncCredentials credentials;
    @Mock
    private EventPublisher eventPublisher;
    @Mock
    private SyncCredentialsManager syncCredentialsManager;
    @Mock
    private RepositoryService repositoryService;
    @Mock
    private ScmMirrorCommandFactory scmMirrorCommandFactory;
    @Mock
    private ScmService scmService;
    @Mock
    private UpstreamUserHelper securityHelper;

    private RepositoryFetchProcessor processor;

    @Before
    public void setUp() {
        when(credentials.configure(any())).thenAnswer(invocation -> invocation.getArguments()[0]);
        when(syncCredentialsManager.refresh(any(SyncCredentials.class))).thenReturn(Promises.promise(credentials));
        when(securityHelper.asUserForUpstream(UPSTREAM_ID)).thenReturn(new DummyEscalatedSecurityContext(null, Collections.emptySet()));
        when(syncCredentialsManager.getCredentials(UPSTREAM_ID)).thenReturn(Promises.promise(credentials));
        when(scmService.getMirrorCommandFactory(any(Repository.class))).thenReturn(scmMirrorCommandFactory);
        when(scmMirrorCommandFactory.synchronize(any(MirrorSyncCommandParameters.class), callbackCaptor.capture())).thenReturn(command);
        when(command.call()).thenAnswer(invocation -> {
            MirrorSyncContext context = mock(MirrorSyncContext.class);
            MirrorSyncSummary summary = mock(MirrorSyncSummary.class);
            MirrorSyncCallback callback = callbackCaptor.getValue();
            MinimalRef ref = mock(MinimalRef.class);
            callback.onStart(context);
            callback.onRefChange(new SimpleRefChange.Builder().ref(ref).fromHash("from-hash").toHash("to-hash").type(RefChangeType.UPDATE).build());
            callback.onEnd(summary);
            return null;
        });
        processor = new RepositoryFetchProcessor(config, contentHashService, eventPublisher,
                repositoryService, scmService, syncCredentialsManager, securityHelper);
    }

    @Test
    public void testFullSynchronizationOfHierarchyFetchesOriginsBeforeForks() {
        // mock a fork hierarchy
        Project rootProject = mockProject(101, "Root project");
        Project personalProject1 = mockProject(102, "Personal project for user-1");
        Project personalProject2 = mockProject(103, "Personal project for user-2");

        Repository root = mockRepo("hierarchy", 1001, "root", rootProject);
        Repository user1Fork = mockRepo("hierarchy", 1002, "root", root, personalProject1);
        Repository user1ForkFork = mockRepo("hierarchy", 1003, "root", user1Fork, personalProject1);
        Repository user2Fork = mockRepo("hierarchy", 1004, "root", root, personalProject2);

        // mock a page with out of order repositories; i.e. some forks come before their root and vice versa
        processor.process("hierarchy-id", ImmutableList.of(
                fetchTask(user1ForkFork, "1ff"), fetchTask(user1Fork, "1f"), fetchTask(root, "r"),
                fetchTask(user2Fork, "2f")));

        InOrder inOrder = inOrder(scmService);
        inOrder.verify(scmService).getMirrorCommandFactory(root);
        inOrder.verify(scmService).getMirrorCommandFactory(user1Fork);
        inOrder.verify(scmService).getMirrorCommandFactory(user1ForkFork);
        inOrder.verify(scmService).getMirrorCommandFactory(user2Fork);

        verify(scmMirrorCommandFactory, times(4)).synchronize(any(MirrorSyncCommandParameters.class), any(MirrorRefChangeCallback.class));
        verify(eventPublisher, times(4)).publish(any(RepositorySynchronizedEvent.class));
    }

    @Test
    public void testFetchRetriedWhenAuthenticationFails() {
        Project project = mockProject(101, "project");
        Repository repository = mockRepo("hierarchy", 1001, "repo", project);

        doThrow(GitFetchAuthenticationException.class).when(command).call();

        processor.process("hierarchy-id", ImmutableList.of(fetchTask(repository, "hash")));

        verify(syncCredentialsManager, times(RepositoryFetchProcessor.MAX_AUTH_FAILURE_RETRIES)).refresh(any(SyncCredentials.class));
        verify(command, times(RepositoryFetchProcessor.MAX_AUTH_FAILURE_RETRIES + 1)).call();
    }

    @Test
    public void testFetchRetryWhenAuthenticationAndRegisterKeyFails() {
        Project project = mockProject(101, "project");
        Repository repository1 = mockRepo("hierarchy", 1001, "repo", project);
        Repository repository2 = mockRepo("hierarchy", 1002, "repo", project);

        doThrow(GitFetchAuthenticationException.class).when(command).call();
        MirrorKeyUploadFailedException exception = mock(MirrorKeyUploadFailedException.class);
        when(syncCredentialsManager.refresh(any(SyncCredentials.class))).thenThrow(exception);

        processor.process("hierarchy-id", ImmutableList.of(fetchTask(repository1, "hash"), fetchTask(repository2, "h")));

        // refreshing the credentials should be tried MAX_AUTH_FAILURE_RETRIES times, after which the fetch for the
        // whole hierarchy should be aborted
        verify(syncCredentialsManager, times(RepositoryFetchProcessor.MAX_AUTH_FAILURE_RETRIES)).refresh(any(SyncCredentials.class));
        verify(command, times(RepositoryFetchProcessor.MAX_AUTH_FAILURE_RETRIES + 1)).call();
    }

    @Test
    public void testMultipleFetchTasksForRepoResultsInNewestHash() {
        Project project = mockProject(101, "project");
        Repository repository = mockRepo("hierachy-id", 1001, "repo", project);

        processor.process("hierarchy-id", ImmutableList.of(
                fetchTask(repository, "hash-1"), fetchTask(repository, "hash-2"), fetchTask(repository, "hash-3")
        ));

        verify(scmMirrorCommandFactory).synchronize(any(MirrorSyncCommandParameters.class), any(MirrorRefChangeCallback.class));
        verify(contentHashService).setHash(repository, "hash-3");
    }

    private RepositoryFetchTask fetchTask(int repoId, String hierarchyId, String contentHash) {
        return new RepositoryFetchTask(UPSTREAM_ID, repoId, hierarchyId, contentHash, "clone" + repoId);
    }

    private RepositoryFetchTask fetchTask(Repository repository, String contentHash) {
        return fetchTask(repository.getId(), repository.getHierarchyId(), contentHash);
    }

    private Project mockProject(int id, String name) {
        Project project = mock(Project.class);
        when(project.getId()).thenReturn(id);
        when(project.getName()).thenReturn(name);
        return project;
    }

    private Repository mockRepo(String hierarchyId, int id, String name, Project project) {
        return mockRepo(hierarchyId, id, name, null, project);
    }

    private Repository mockRepo(String hierarchyId, int id, String name, Repository origin, Project project) {
        Repository repository = mock(Repository.class);
        when(repository.getId()).thenReturn(id);
        when(repository.getHierarchyId()).thenReturn(hierarchyId);
        when(repository.getName()).thenReturn(name);
        when(repository.getProject()).thenReturn(project);
        when(repository.getOrigin()).thenReturn(origin);

        when(repositoryService.getById(id)).thenReturn(repository);

        return repository;
    }

}