package com.atlassian.stash.internal.commit.graph.cache;

import com.atlassian.bitbucket.concurrent.ConcurrencyService;
import com.atlassian.bitbucket.concurrent.VersionTracker;
import com.atlassian.bitbucket.event.repository.RepositoryPushEvent;
import com.atlassian.bitbucket.event.repository.RepositoryRefsChangedEvent;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.repository.RefChange;
import com.atlassian.bitbucket.repository.RefChangeType;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.repository.SimpleRefChange;
import com.atlassian.bitbucket.scm.ScmService;
import com.atlassian.bitbucket.server.ApplicationMode;
import com.atlassian.bitbucket.user.EscalatedSecurityContext;
import com.atlassian.bitbucket.user.SecurityService;
import com.atlassian.bitbucket.util.Operation;
import com.atlassian.stash.internal.ApplicationSettings;
import com.atlassian.stash.internal.commit.graph.CachedCommitGraphSource;
import com.atlassian.stash.internal.repository.InternalRepository;
import com.atlassian.util.contentcache.ContentCache;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.MoreExecutors;
import com.hazelcast.core.IExecutorService;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner;

import java.util.concurrent.ExecutorService;

import static com.atlassian.bitbucket.repository.RefTestUtils.minimalBranch;
import static org.junit.Assert.assertFalse;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class DefaultCachedCommitGraphSourceTest {

    @Rule
    public TemporaryFolder temporaryFolder = new TemporaryFolder();

    @Mock
    private ApplicationSettings applicationSettings;
    @Mock
    private IExecutorService clusterExecutorService;
    @Mock
    private ConcurrencyService concurrencyService;
    @Mock
    private ContentCache contentCache;
    @Spy
    private ExecutorService executorService = MoreExecutors.newDirectExecutorService();
    @Mock
    private I18nService i18nService;
    @Mock
    private ScmService scmService;
    private DefaultCachedCommitGraphSource source;
    @Mock
    private VersionTracker<Integer> versionTracker;

    @Before
    public void setup() throws Exception {
        when(concurrencyService.<Integer>getVersionTracker(anyString())).thenReturn(versionTracker);
        when(applicationSettings.getCacheDir()).thenReturn(temporaryFolder.getRoot());
        source = newSource(ApplicationMode.DEFAULT);
    }

    public DefaultCachedCommitGraphSource newSource(final ApplicationMode mode) {
        return new DefaultCachedCommitGraphSource(applicationSettings, clusterExecutorService, concurrencyService,
                executorService, i18nService, 1500, mode, 1024, scmService) {
            @Override
            protected ContentCache createFileCache() {
                return contentCache;
            }
        };
    }

    @After
    public void tearDown() throws Exception {
        if (source.isRunning()) {
            source.stop();
        }
    }

    @SuppressWarnings("unchecked")
    @Test
    public void testClusterInvalidation() throws Exception {
        source.start();
        CachedCommitGraphSource cache = mock(CachedCommitGraphSource.class);
        Repository repository = mock(Repository.class);
        SecurityService securityService = mock(SecurityService.class);
        EscalatedSecurityContext context = mock(EscalatedSecurityContext.class);
        when(context.<Repository, RuntimeException>call(any(Operation.class))).thenReturn(repository);
        when(securityService.withPermission(eq(Permission.ADMIN), anyString())).thenReturn(context);
        DefaultCachedCommitGraphSource.RebuildCacheTask task = new DefaultCachedCommitGraphSource.RebuildCacheTask(42);
        task.cache = cache;
        task.securityservice = securityService;
        task.run();

        verify(cache).rebuildFor(same(repository));
    }

    @Test
    public void testDestroy() throws Exception {
        source.stop();

        verify(contentCache).clear();
        assertFalse(source.isRunning());
    }

    @Test
    public void testOnRepositoryRefChangesInvalidatesCache() throws Exception {
        source.start();
        RefChange change = new SimpleRefChange.Builder()
                .ref(minimalBranch("master"))
                .fromHash("abc123")
                .toHash("def456")
                .type(RefChangeType.ADD)
                .build();
        Repository repository = sampleRepository(42);
        RepositoryRefsChangedEvent event = new RepositoryPushEvent(this, repository, ImmutableList.of(change));
        source.onRepositoryRefChanges(event);
        verify(versionTracker).increment(42);
        verify(clusterExecutorService).executeOnAllMembers(any(DefaultCachedCommitGraphSource.RebuildCacheTask.class));
    }

    @Test
    public void testOnRepositoryRefChangesDoesNotInvalidateCacheWithoutChanges() throws Exception {
        source.start();
        RepositoryRefsChangedEvent event = new RepositoryPushEvent(this, mock(Repository.class), ImmutableList.<RefChange>of());
        source.onRepositoryRefChanges(event);
        verify(versionTracker, never()).increment(anyInt());
        verify(clusterExecutorService, never()).executeOnAllMembers(any(Runnable.class));
    }

    @Test
    public void testOnRepositoryRefChangesDoesNotInvalidatesCacheWithOnlyDeletions() throws Exception {
        source.start();
        RefChange elements = new SimpleRefChange.Builder()
                .ref(minimalBranch("master"))
                .fromHash("abc123").toHash("def456").type(RefChangeType.DELETE).build();
        Repository repository = sampleRepository(42);
        RepositoryRefsChangedEvent event = new RepositoryPushEvent(this, repository, ImmutableList.of(elements));
        source.onRepositoryRefChanges(event);
        verify(versionTracker, never()).increment(anyInt());
        verify(clusterExecutorService, never()).executeOnAllMembers(any(Runnable.class));
    }

    @Test
    public void testRebuildRequestDelayedUntilStarted() throws Exception {
        doNothing().when(executorService).execute(any(Runnable.class));
        Repository repository = sampleRepository(42);
        source.rebuildFor(repository);
        verifyZeroInteractions(executorService);
        source.start();
        verify(executorService).execute(isA(DefaultCachedCommitGraphSource.RetryingCacheTraversalJob.class));
    }

    @Test
    public void testOnRepositoryRefChangedDoesNothingWhenInMirrorMode() throws Exception {
        source = newSource(ApplicationMode.MIRROR);

        source.start();
        RefChange change = new SimpleRefChange.Builder()
                .ref(minimalBranch("master"))
                .fromHash("abc123")
                .toHash("def456")
                .type(RefChangeType.ADD)
                .build();
        Repository repository = sampleRepository(42);
        RepositoryRefsChangedEvent event = new RepositoryPushEvent(this, repository, ImmutableList.of(change));
        source.onRepositoryRefChanges(event);
        verify(versionTracker, never()).increment(anyInt());
        verify(clusterExecutorService, never()).executeOnAllMembers(any(DefaultCachedCommitGraphSource.RebuildCacheTask.class));
    }

    private Repository sampleRepository(int id) {
        return new InternalRepository.Builder().id(id).name("repo-" + id).build();
    }
}
