package com.atlassian.stash.internal.repository;

import com.atlassian.bitbucket.AbstractServiceTest;
import com.atlassian.bitbucket.NoSuchEntityException;
import com.atlassian.bitbucket.ServerException;
import com.atlassian.bitbucket.auth.AuthenticationContext;
import com.atlassian.bitbucket.event.CancelableEvent;
import com.atlassian.bitbucket.event.repository.*;
import com.atlassian.bitbucket.hamcrest.PageMatchers;
import com.atlassian.bitbucket.i18n.KeyedMessage;
import com.atlassian.bitbucket.i18n.SimpleI18nService;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.permission.PermissionPredicateFactory;
import com.atlassian.bitbucket.project.NoSuchProjectException;
import com.atlassian.bitbucket.project.Project;
import com.atlassian.bitbucket.repository.*;
import com.atlassian.bitbucket.scm.*;
import com.atlassian.bitbucket.server.ApplicationPropertiesService;
import com.atlassian.bitbucket.server.FeatureManager;
import com.atlassian.bitbucket.server.StandardFeature;
import com.atlassian.bitbucket.util.*;
import com.atlassian.bitbucket.validation.groups.Create;
import com.atlassian.bitbucket.validation.groups.Update;
import com.atlassian.cache.*;
import com.atlassian.event.api.EventPublisher;
import com.atlassian.security.random.DefaultSecureTokenGenerator;
import com.atlassian.security.random.SecureTokenGenerator;
import com.atlassian.stash.internal.project.InternalNormalProject;
import com.atlassian.stash.internal.project.InternalPersonalProject;
import com.atlassian.stash.internal.project.InternalProject;
import com.atlassian.stash.internal.project.InternalProjectService;
import com.atlassian.stash.internal.scm.InternalScmService;
import com.atlassian.stash.internal.user.InternalNormalUser;
import com.atlassian.stash.internal.user.InternalPermissionService;
import com.atlassian.utils.process.ProcessException;
import com.google.common.collect.ImmutableSet;
import org.hamcrest.FeatureMatcher;
import org.hamcrest.Matcher;
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.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;

import javax.validation.Validator;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Predicate;

import static com.atlassian.bitbucket.mockito.MockitoUtils.returnFirst;
import static org.hamcrest.Matchers.*;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.isA;

@RunWith(MockitoJUnitRunner.class)
@SuppressWarnings({"ConstantConditions", "unchecked"})
public class DefaultRepositoryServiceTest extends AbstractServiceTest {

    private static final String SCM_ID = "git";

    private static InternalNormalUser ADMIN = new InternalNormalUser.Builder()
            .id(1).name("admin").build();
    private static InternalNormalUser USER = new InternalNormalUser.Builder()
            .id(2).name("user").build();
    private static final InternalNormalProject EXISTING_PROJECT = new InternalNormalProject.Builder()
            .description("Description").id(5).key("KEY").name("Name").build();
    private static final InternalRepository EXISTING_REPO = new InternalRepository.Builder()
            .hierarchyId("a").id(1).name("Existing NAME").project(EXISTING_PROJECT)
            .scmId(SCM_ID).state(Repository.State.AVAILABLE).build();
    private static final InternalPersonalProject PERSONAL_PROJECT = new InternalPersonalProject.Builder()
            .id(42).owner(ADMIN).build();
    private static final InternalPersonalProject USER_PERSONAL_PROJECT = new InternalPersonalProject.Builder()
            .id(100).owner(USER).build();

    @Rule
    public TemporaryFolder folder = new TemporaryFolder();

    @Mock
    private AuthenticationContext authenticationContext;
    @Mock
    private Cache cache;
    @Mock
    private CacheFactory cacheFactory;
    @Mock
    private EventPublisher eventPublisher;
    @Mock
    private ScheduledExecutorService executorService;
    @Mock
    private FeatureManager featureManager;
    @Mock
    private PermissionPredicateFactory predicateFactory;
    @Mock
    private InternalPermissionService permissionService;
    @Mock
    private InternalProjectService projectService;
    @Mock
    private ApplicationPropertiesService propertiesService;
    @Mock
    private RepositoryDao repositoryDao;
    private DefaultRepositoryService repositoryService;
    @Mock
    private InternalScmService scmService;
    @Mock
    private SecureTokenGenerator tokenGenerator;
    @Mock
    private PlatformTransactionManager transactionManager;
    @Mock
    private Validator validator;

    public DefaultRepositoryServiceTest() {
        super(DefaultRepositoryService.class, RepositoryService.class);
    }

    @Before
    public void setup() throws Exception {
        when(cacheFactory.getCache(eq(DefaultRepositoryService.CACHE_NAME), isNotNull(CacheLoader.class),
                isNotNull(CacheSettings.class))).thenReturn(cache);

        when(featureManager.isEnabled(StandardFeature.FORKS)).thenReturn(true);
        when(featureManager.isEnabled(StandardFeature.PERSONAL_REPOS)).thenReturn(true);

        //Can't @Spy on DefaultSecureTokenGenerator; the class is final. This workaround enables using @InjectMocks
        when(tokenGenerator.generateToken()).thenAnswer(new Answer<Object>() {

            private final SecureTokenGenerator tokenGenerator = DefaultSecureTokenGenerator.getInstance();

            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                return tokenGenerator.generateToken();
            }
        });

        repositoryService = new DefaultRepositoryService(authenticationContext, cacheFactory, eventPublisher,
                executorService, featureManager, new SimpleI18nService(), predicateFactory, permissionService,
                projectService, propertiesService, repositoryDao, scmService, tokenGenerator, transactionManager,
                validator);
        repositoryService.setMaxRepositoriesPerPage(1000);
    }

    @Test
    public void testCount() {
        when(repositoryDao.countAll()).thenReturn(5L);

        assertEquals(5L, repositoryService.getCount());

        verify(repositoryDao).countAll();
    }

    @Test
    public void testCountByProject() throws Exception {
        InternalProject project = mock(InternalProject.class);
        when(project.getId()).thenReturn(1);

        when(permissionService.getCountOfAccessibleRepositories(same(project))).thenReturn(10);

        assertEquals(10, repositoryService.countByProject(project));
    }

    @Test
    public void testCreate() throws Exception {
        ArgumentCaptor<InternalRepository> captor = ArgumentCaptor.forClass(InternalRepository.class);

        when(propertiesService.getRepositoryDir(isA(Repository.class))).thenReturn(new File(folder.getRoot(), "1"));
        when(repositoryDao.create(captor.capture())).thenAnswer(returnFirst());
        when(repositoryDao.update(captor.capture())).thenAnswer(returnFirst());

        Repository repository = repositoryService.create(new RepositoryCreateRequest.Builder()
                .name("Test Repo").project(EXISTING_PROJECT).scmId(SCM_ID).publiclyAccessible(true).build());
        assertNotNull("Repository was not created", repository);
        assertEquals("Slug not expected value", "test-repo", repository.getSlug());
        assertEquals("Repository should be public", true, repository.isPublic());

        //Verifies that validation happens before the creation requested event, before the repository is created in the
        //database, before the repository is created in the SCM, before the repository's state is updated to available,
        //before the created event is raised
        InOrder ordered = inOrder(eventPublisher, repositoryDao, scmService, validator);
        ordered.verify(validator).validate(isA(InternalRepository.class), same(Create.class));
        ordered.verify(eventPublisher).publish(isA(RepositoryCreationRequestedEvent.class));
        ordered.verify(repositoryDao).create(isA(InternalRepository.class));
        ordered.verify(scmService).create(isA(InternalRepository.class));
        ordered.verify(repositoryDao).update(isA(InternalRepository.class));
        ordered.verify(eventPublisher).publish(isA(RepositoryCreatedEvent.class));

        List<InternalRepository> captured = captor.getAllValues();
        assertEquals(2, captured.size());
        assertSame(Repository.State.INITIALISING, captured.get(0).getState());
        assertSame(Repository.State.AVAILABLE, captured.get(1).getState());
    }

    @Test(expected = RepositoryCreationCanceledException.class)
    public void testCreateThrowsWhenCanceled() {
        doAnswer(invocationOnMock -> {
            RepositoryCreationRequestedEvent event = (RepositoryCreationRequestedEvent) invocationOnMock.getArguments()[0];
            assertNotNull(event.getRepository());
            assertFalse(event.isCanceled());

            event.cancel(new KeyedMessage("key", "localisedMessage", "rootMessage"));
            assertTrue(event.isCanceled());

            return null;
        }).when(eventPublisher).publish(isA(RepositoryCreationRequestedEvent.class));

        try {
            repositoryService.create(new RepositoryCreateRequest.Builder()
                    .project(EXISTING_PROJECT)
                    .name("Name")
                    .scmId(SCM_ID)
                    .build());
        } finally {
            verify(eventPublisher, never()).publish(isA(RepositoryCreatedEvent.class));
            verify(repositoryDao, never()).create(any(InternalRepository.class));
        }
    }

    @Test(expected = RepositoryAlreadyExistsException.class)
    public void testCreateThrowsWhenRepositoryDirBlockedByDir() throws Exception {
        Path root = folder.getRoot().toPath();
        // All paths already exist on disk
        Path repoDirectory1 = Files.createDirectory(root.resolve("13"));
        Path repoDirectory2 = Files.createDirectory(root.resolve("14"));
        Path repoDirectory3 = Files.createDirectory(root.resolve("15"));
        Path repoDirectory4 = Files.createDirectory(root.resolve("16"));
        Path repoDirectory5 = Files.createDirectory(root.resolve("17"));
        try {
            int failedCreateAttempts = 5; // The number of expected failed create attempts
            repositoryService.setMaxRepositoryCreateAttempts(5); // Ensure only 5 attempts
            int repoDaoInteractions = failedCreateAttempts * 2; // Two calls to the DAO for each create attempt
            runBlockedCreateTest(repoDaoInteractions,
                    repoDirectory1.toFile(),
                    repoDirectory2.toFile(),
                    repoDirectory3.toFile(),
                    repoDirectory4.toFile(),
                    repoDirectory5.toFile());
        } finally {
            // Clean up test folders
            assertTrue(0 == deletePath(repoDirectory1, repoDirectory2, repoDirectory3, repoDirectory4, repoDirectory5));
            // No calls to the scmService to create the repository
            verifyZeroInteractions(scmService);
            // All 5 repoDirectories were returned from getRepositoryDir()
            verify(propertiesService, times(5)).getRepositoryDir(isA(Repository.class));
        }
    }

    @Test
    public void testCreateSucceedsOnRetryWhenRepositoryBlockedByDir() throws Exception {
        Path root = folder.getRoot().toPath();
        Path repoDirectory1 = Files.createDirectory(root.resolve("223")); // Exists on disk
        Path repoDirectory2 = Files.createDirectory(root.resolve("224")); // Exists on disk
        Path repoDirectory3 = Files.createDirectory(root.resolve("225")); // Exists on disk
        Path repoDirectory4 = root.resolve("226"); // Free for use
        Path repoDirectory5 = root.resolve("227"); // Free for use - Never used because 4 is free
        try {
            int failedCreateAttempts = 4; // The number of expected failed attempts
            repositoryService.setMaxRepositoryCreateAttempts(5); // Ensure on 5 create attempts
            int repoDaoInteractions = failedCreateAttempts * 2; // Two calls to the DAO for each create attempt
            runBlockedCreateTest(repoDaoInteractions,
                    repoDirectory1.toFile(),
                    repoDirectory2.toFile(),
                    repoDirectory3.toFile(),
                    repoDirectory4.toFile(),
                    repoDirectory5.toFile());
        } finally {
            // Clean up test folders
            assertTrue(0 == deletePath(repoDirectory1, repoDirectory2, repoDirectory3, repoDirectory4));
            // Only one call to the scmService which created the repository
            verify(scmService).create(isA(Repository.class));
            // repoDirectory1 through repoDirectory4 were returned by getRepositoryDir()
            verify(propertiesService, times(4)).getRepositoryDir(isA(Repository.class));
        }
    }

    @Test(expected = RepositoryAlreadyExistsException.class)
    public void testCreateThrowsWhenRepositoryDirBlockedByFile() throws Exception {
        Path root = folder.getRoot().toPath();
        // All 5 repository folders already exist, but are files not directories
        Path repoDirectory1 = Files.createFile(root.resolve("15"));
        Path repoDirectory2 = Files.createFile(root.resolve("16"));
        Path repoDirectory3 = Files.createFile(root.resolve("17"));
        Path repoDirectory4 = Files.createFile(root.resolve("18"));
        Path repoDirectory5 = Files.createFile(root.resolve("19"));
        try {
            int failedCreateAttempts = 5; // The number of expected failed create attempts
            repositoryService.setMaxRepositoryCreateAttempts(failedCreateAttempts); // Ensure only 5 create attempts
            int repoDaoInteractions = failedCreateAttempts * 2; // Two calls to the DAO for each create
            runBlockedCreateTest(repoDaoInteractions,
                    repoDirectory1.toFile(),
                    repoDirectory2.toFile(),
                    repoDirectory3.toFile(),
                    repoDirectory4.toFile(),
                    repoDirectory5.toFile());
        } finally {
            // Clean up the test files
            assertTrue(0 == deletePath(repoDirectory1, repoDirectory2, repoDirectory3, repoDirectory4, repoDirectory5));
            // The repository was never created on disk
            verifyZeroInteractions(scmService);
            // repoDirectory1 through repoDirectory5 were returned by getRepositoryDir()
            verify(propertiesService, times(5)).getRepositoryDir(isA(Repository.class));
        }
    }

    @Test
    public void testCreateSucceedsOnRetryWhenRepositoryBlockedByFile() throws Exception {
        Path root = folder.getRoot().toPath();
        Path repoDirectory1 = Files.createFile(root.resolve("563")); // Already exists, but is file not folder
        Path repoDirectory2 = root.resolve("564"); // Free
        Path repoDirectory3 = root.resolve("565"); // Free
        Path repoDirectory4 = root.resolve("566"); // Free
        Path repoDirectory5 = root.resolve("567"); // Free
        try {
            runBlockedCreateTest(4,
                    repoDirectory1.toFile(),
                    repoDirectory2.toFile(),
                    repoDirectory3.toFile(),
                    repoDirectory4.toFile(),
                    repoDirectory5.toFile());
        } finally {
            // Clean up the test files
            assertTrue(0 == deletePath(repoDirectory1, repoDirectory2));
            // The repository was created and only 1 call to scmService
            verify(scmService).create(isA(Repository.class));
            // repoDirectory1 and repoDirectory2 were returned by getRepositoryDir()
            verify(propertiesService, times(2)).getRepositoryDir(isA(Repository.class));
        }
    }

    @Test
    public void testCreateThrowsProcessException() throws Exception {
        TransactionStatus status = mock(TransactionStatus.class);
        when(transactionManager.getTransaction(any(TransactionDefinition.class))).thenReturn(status);

        ServerException exception = new ServerException(mock(KeyedMessage.class), new ProcessException("fatal: cannot mkdir /foo"));
        doThrow(exception).when(scmService).create(notNull(InternalRepository.class));

        when(propertiesService.getRepositoryDir(isA(Repository.class))).thenReturn(new File(folder.getRoot(), "testcreate"));
        when(repositoryDao.create(notNull(InternalRepository.class))).thenAnswer(returnFirst());
        when(repositoryDao.update(notNull(InternalRepository.class))).thenAnswer(returnFirst());

        RepositoryCreateRequest request = new RepositoryCreateRequest.Builder().project(EXISTING_PROJECT).name("foo").scmId(SCM_ID).build();
        try {
            repositoryService.create(request);
        } catch (ServerException e) {
            assertNull(e.getCause());
            assertThat(e.getLocalizedMessage(), not(containsString("mkdir")));
            verify(eventPublisher, times(1)).publish(isA(RepositoryCreationFailedEvent.class));
        }
    }

    @Test(expected = NullPointerException.class)
    public void testCreateWithNullName() throws Exception {
        repositoryService.create(new RepositoryCreateRequest.Builder()
                .project(EXISTING_PROJECT)
                .name(null)
                .scmId(SCM_ID)
                .build());
    }

    @Test(expected = NullPointerException.class)
    public void testCreateWithNullProject() throws Exception {
        repositoryService.create(new RepositoryCreateRequest.Builder()
                .project(null)
                .name("Test Repo")
                .scmId(SCM_ID)
                .build());
    }

    @Test(expected = NullPointerException.class)
    public void testCreateWithNullScmId() throws Exception {
        repositoryService.create(new RepositoryCreateRequest.Builder()
                .project(EXISTING_PROJECT)
                .name("Test Repo")
                .scmId(null)
                .build());
    }

    @Test
    public void testDelete() {
        TransactionStatus status = mock(TransactionStatus.class);

        doAnswer(invocation -> {
            ((Runnable) invocation.getArguments()[0]).run();
            return null;
        }).when(executorService).submit(isA(Runnable.class));
        when(repositoryDao.getForkIds(same(EXISTING_REPO))).thenReturn(Collections.emptyList());
        when(transactionManager.getTransaction(any(TransactionDefinition.class))).thenReturn(status);

        repositoryService.delete(EXISTING_REPO);

        ArgumentCaptor<DeleteCommandParameters> paramtersCaptor = ArgumentCaptor.forClass(DeleteCommandParameters.class);

        InOrder ordered = inOrder(eventPublisher, executorService, repositoryDao, scmService, status);
        ordered.verify(eventPublisher).publish(isA(RepositoryDeletionRequestedEvent.class));
        ordered.verify(repositoryDao).getForkIds(same(EXISTING_REPO));
        ordered.verify(repositoryDao).deleteById(eq(EXISTING_REPO.getId()));
        ordered.verify(eventPublisher).publish(isA(RepositoryDeletedEvent.class));
        ordered.verify(executorService).submit(isA(Runnable.class));
        ordered.verify(status).setRollbackOnly(); //Ensure the transaction is marked read-only
        ordered.verify(repositoryDao).countByHierarchy(eq(EXISTING_REPO.getHierarchyId()));
        ordered.verify(scmService).delete(same(EXISTING_REPO), paramtersCaptor.capture());

        //Because EXISTING_REPO has no forks, the service should not update origins
        verify(repositoryDao, never()).updateOrigin(any(InternalRepository.class), any(InternalRepository.class));
    }

    @Test(expected = DataIntegrityViolationException.class)
    public void testDeleteThrowsOnDatabaseFailureWhileDeleting() {
        doThrow(DataIntegrityViolationException.class).when(repositoryDao).deleteById(eq(EXISTING_REPO.getId()));
        doReturn(Collections.emptyList()).when(repositoryDao).getForkIds(same(EXISTING_REPO));

        try {
            repositoryService.delete(EXISTING_REPO);
        } finally {
            //Verify we gave listeners the opportunity to cancel the deletion
            verify(eventPublisher).publish(isA(RepositoryDeletionRequestedEvent.class));
            //Verify the DAO was called
            verify(repositoryDao).getForkIds(same(EXISTING_REPO));
            verify(repositoryDao).deleteById(eq(EXISTING_REPO.getId()));

            //Verify that no attempt is made to reparent forks or delete the repository in the SCM
            verifyZeroInteractions(executorService);
            verifyZeroInteractions(scmService);
        }
    }

    @Test(expected = IllegalRepositoryStateException.class)
    public void testDeleteThrowsOnNonAvailableState() {
        Repository repository = mock(Repository.class);
        when(repository.getProject()).thenReturn(EXISTING_PROJECT);
        when(repository.getState()).thenReturn(Repository.State.INITIALISING);

        try {
            repositoryService.delete(repository);
        } finally {
            //Should have thrown before publishing a deletion requested event, or attempting actual deletion
            verifyZeroInteractions(eventPublisher);
            verifyZeroInteractions(executorService);
            verifyZeroInteractions(repositoryDao);
        }
    }

    @Test(expected = RepositoryDeletionCanceledException.class)
    public void testDeleteThrowsWhenCanceled() {
        InternalRepository repository = mock(InternalRepository.class);
        when(repository.getProject()).thenReturn(EXISTING_PROJECT);
        when(repository.getState()).thenReturn(Repository.State.AVAILABLE);

        doAnswer(invocation -> {
            CancelableEvent event = (CancelableEvent) invocation.getArguments()[0];
            event.cancel(mock(KeyedMessage.class));

            return null;
        }).when(eventPublisher).publish(isA(RepositoryDeletionRequestedEvent.class));

        try {
            repositoryService.delete(repository);
        } finally {
            verify(eventPublisher).publish(isA(RepositoryDeletionRequestedEvent.class));

            verifyZeroInteractions(executorService);
            verifyZeroInteractions(repositoryDao);
        }
    }

    @Test
    public void testFindAll() throws Exception {
        Page<InternalRepository> page = mock(Page.class);
        Predicate<Repository> predicate = mock(Predicate.class);

        PageRequest pageRequest = mock(PageRequest.class);
        when(pageRequest.buildRestrictedPageRequest(eq(1000))).thenReturn(pageRequest);

        when(predicateFactory.createRepositoryAccessiblePredicate()).thenReturn(predicate);
        when(repositoryDao.findAll(same(pageRequest), same(predicate))).thenReturn(page);

        assertSame(page, repositoryService.findAll(pageRequest));
    }

    @Test
    public void testFindByOrigin() {
        Page<InternalRepository> page = mock(Page.class);
        Predicate<Repository> predicate = mock(Predicate.class);

        PageRequest pageRequest = mock(PageRequest.class);
        when(pageRequest.buildRestrictedPageRequest(eq(1000))).thenReturn(pageRequest);

        when(predicateFactory.createRepositoryAccessiblePredicate()).thenReturn(predicate);
        when(repositoryDao.findByOrigin(same(EXISTING_REPO), same(pageRequest), same(predicate))).thenReturn(page);

        assertSame(page, repositoryService.findByOrigin(EXISTING_REPO, pageRequest));

        verify(pageRequest).buildRestrictedPageRequest(eq(1000));
        verify(repositoryDao).findByOrigin(same(EXISTING_REPO), same(pageRequest), same(predicate));
    }

    @Test(expected = NullPointerException.class)
    public void testFindByOriginWithNullPageRequest() {
        repositoryService.findByOrigin(EXISTING_REPO, null);
    }

    @Test(expected = NullPointerException.class)
    public void testFindByOriginWithNullRepository() {
        repositoryService.findByOrigin(null, PageUtils.newRequest(0, 25));
    }

    @Test
    public void testFindByProjectKey() throws Exception {
        Page<InternalRepository> page = mock(Page.class);
        Predicate<Repository> predicate = mock(Predicate.class);
        Project project = mock(Project.class);

        PageRequest pageRequest = mock(PageRequest.class);
        when(pageRequest.buildRestrictedPageRequest(eq(1000))).thenReturn(pageRequest);

        when(predicateFactory.createRepositoryAccessiblePredicate()).thenReturn(predicate);
        when(projectService.getByKey(isNull(String.class), eq("KEY"))).thenReturn(project);
        when(repositoryDao.findByProjectKey(isNull(String.class), eq("KEY"), same(pageRequest), same(predicate)))
                .thenReturn(page);

        assertSame(page, repositoryService.findByProjectKey("KEY", pageRequest));

        verify(pageRequest).buildRestrictedPageRequest(eq(1000));
    }

    @Test
    public void testFindByProjectKeyWithNamespace() throws Exception {
        Page<InternalRepository> page = mock(Page.class);
        Predicate<Repository> predicate = mock(Predicate.class);
        Project project = mock(Project.class);

        PageRequest pageRequest = mock(PageRequest.class);
        when(pageRequest.buildRestrictedPageRequest(eq(1000))).thenReturn(pageRequest);

        when(predicateFactory.createRepositoryAccessiblePredicate()).thenReturn(predicate);
        when(projectService.getByKey(eq("namespace"), eq("KEY"))).thenReturn(project);
        when(repositoryDao.findByProjectKey(eq("namespace"), eq("KEY"), same(pageRequest), same(predicate)))
                .thenReturn(page);

        assertSame(page, repositoryService.findByProjectKey("namespace", "KEY", pageRequest));

        verify(pageRequest).buildRestrictedPageRequest(eq(1000));
    }

    @Test
    public void testFindByNonExistentPersonalProjectKey() throws Exception {
        Predicate<Repository> predicate = mock(Predicate.class);

        when(predicateFactory.createRepositoryPermissionPredicate(same(Permission.REPO_READ))).thenReturn(predicate);
        when(projectService.isPersonalProject(eq("~USER"))).thenReturn(true);

        Page<Repository> repos = repositoryService.findByProjectKey("~USER", PageUtils.newRequest(0, 1000));
        assertEquals(0, repos.getSize());
    }

    @Test(expected = NoSuchProjectException.class)
    public void testFindByProjectKeyThrowsOnNonexistentProject() throws Exception {
        repositoryService.findByProjectKey("NOPE", PageUtils.newRequest(5, 10));
    }

    @Test(expected = NullPointerException.class)
    public void testFindByProjectKeyWithNullKey() {
        repositoryService.findByProjectKey(null, PageUtils.newRequest(0, 10));
    }

    @Test(expected = NullPointerException.class)
    public void testFindByProjectKeyWithNullPageRequest() {
        repositoryService.findByProjectKey("KEY", null);
    }

    @Test
    public void testFindBySlug() throws Exception {
        InternalRepository repository = mock(InternalRepository.class);

        when(repositoryDao.getBySlug(isNull(String.class), eq("KEY"), eq("slug"))).thenReturn(repository);

        assertSame(repository, repositoryService.getBySlug("KEY", "slug"));
    }

    @Test
    public void testFindBySlugWithNamespace() throws Exception {
        InternalRepository repository = mock(InternalRepository.class);

        when(repositoryDao.getBySlug(eq("namespace"), eq("KEY"), eq("slug"))).thenReturn(repository);

        assertSame(repository, repositoryService.getBySlug("namespace", "KEY", "slug"));
    }

    @Test(expected = NullPointerException.class)
    public void testFindBySlugWithNullKey() {
        repositoryService.getBySlug(null, "slug");
    }

    @Test(expected = NullPointerException.class)
    public void testFindBySlugWithNullSlug() {
        repositoryService.getBySlug("KEY", null);
    }

    @Test
    public void testFindPersonalFork() {
        InternalRepository fork = mock(InternalRepository.class);

        when(projectService.getPersonalProject(eq(false))).thenReturn(PERSONAL_PROJECT);
        when(repositoryDao.findByOriginInProject(same(EXISTING_REPO), eq(PERSONAL_PROJECT.getId())))
                .thenReturn(fork);

        assertSame(fork, repositoryService.findPersonalFork(EXISTING_REPO));

        verify(projectService).getPersonalProject(eq(false));
        verify(repositoryDao).findByOriginInProject(same(EXISTING_REPO), eq(PERSONAL_PROJECT.getId()));
    }

    @Test
    public void testFindPersonalForkReturnsNullWhenNoPersonalProject() {
        assertNull(repositoryService.findPersonalFork(EXISTING_REPO));

        verify(projectService).getPersonalProject(eq(false));
        verifyZeroInteractions(repositoryDao);
    }

    @Test(expected = NullPointerException.class)
    public void testFindPersonalForkWithNullRepository() {
        repositoryService.findPersonalFork(null);
    }

    @Test
    public void testFindRelated() {
        Page<InternalRepository> page = mock(Page.class);
        Predicate<Repository> predicate = mock(Predicate.class);

        PageRequest pageRequest = mock(PageRequest.class);
        when(pageRequest.buildRestrictedPageRequest(eq(1000))).thenReturn(pageRequest);

        when(predicateFactory.createRepositoryAccessiblePredicate()).thenReturn(predicate);
        when(repositoryDao.findByHierarchy(same(EXISTING_REPO), same(pageRequest), same(predicate))).thenReturn(page);

        assertSame(page, repositoryService.findRelated(EXISTING_REPO, pageRequest));

        verify(pageRequest).buildRestrictedPageRequest(eq(1000));
        verify(repositoryDao).findByHierarchy(same(EXISTING_REPO), same(pageRequest), same(predicate));
    }

    @Test(expected = NullPointerException.class)
    public void testFindRelatedWithNullPageRequest() {
        repositoryService.findRelated(EXISTING_REPO, null);
    }

    @Test(expected = NullPointerException.class)
    public void testFindRelatedWithNullRepository() {
        repositoryService.findRelated(null, PageUtils.newRequest(0, 25));
    }

    @Test
    public void testFork() throws Exception {
        Path root = folder.getRoot().toPath();
        // All folders are free for use
        Path repoDirectory1 = root.resolve("9");
        Path repoDirectory2 = root.resolve("10");
        Path repoDirectory3 = root.resolve("11");
        Path repoDirectory4 = root.resolve("12");
        Path repoDirectory5 = root.resolve("13");
        try {
            repositoryService.setMaxRepositoryCreateAttempts(5); // Ensure only 5 create attempts
            testFork(repoDirectory1.toFile(),
                    repoDirectory2.toFile(),
                    repoDirectory3.toFile(),
                    repoDirectory4.toFile(),
                    repoDirectory5.toFile());
        } finally {
            // Clean up temporary directories
            assertTrue(0 == deletePath(repoDirectory1));
            // The fork was created on disk
            verify(scmService).isSupported(isA(Repository.class), eq(ScmFeature.FORK));
            verify(scmService).fork(isA(Repository.class), isA(ForkCommandParameters.class));
            // Only one call to getRepositoryDir()
            verify(propertiesService, times(1)).getRepositoryDir(isA(Repository.class));
        }

    }

    @Test(expected = RepositoryAlreadyExistsException.class)
    public void testForkFailsWhenBlockedByDir() throws Exception {
        Path root = folder.getRoot().toPath();
        // All directories already exist on disk
        Path repoDirectory1 = Files.createDirectory(root.resolve("9"));
        Path repoDirectory2 = Files.createDirectory(root.resolve("10"));
        Path repoDirectory3 = Files.createDirectory(root.resolve("11"));
        Path repoDirectory4 = Files.createDirectory(root.resolve("12"));
        Path repoDirectory5 = Files.createDirectory(root.resolve("13"));
        try {
            repositoryService.setMaxRepositoryCreateAttempts(5); // Ensure only 5 create attempts
            testFork(repoDirectory1.toFile(),
                    repoDirectory2.toFile(),
                    repoDirectory3.toFile(),
                    repoDirectory4.toFile(),
                    repoDirectory5.toFile());
        } finally {
            // Delete all test folders
            assertTrue(0 == deletePath(repoDirectory1, repoDirectory2, repoDirectory3, repoDirectory4, repoDirectory5));
            // The repository was not created on disk
            verify(scmService).isSupported(isA(Repository.class), eq(ScmFeature.FORK));
            verifyNoMoreInteractions(scmService);
            // repoDirectory1 through 5 were returned from getRepositoryDir();
            verify(propertiesService, times(5)).getRepositoryDir(isA(Repository.class));
        }
    }

    @Test
    public void testForkNotPublicByDefault() throws Exception {
        // Make a public repository
        InternalRepository repo = new InternalRepository.Builder(EXISTING_REPO).publiclyAccessible(true).build();
        // The fork should _not_ be public by default
        assertEquals(false, new RepositoryForkRequest.Builder(repo).build().isPublic());
        assertEquals(true, new RepositoryForkRequest.Builder(repo).publiclyAccessible(true).build().isPublic());
    }

    @Test
    public void testForkRetainsName() {
        when(projectService.getPersonalProject()).thenReturn(PERSONAL_PROJECT);
        when(propertiesService.getRepositoryDir(isA(InternalRepository.class)))
                .thenReturn(new File(folder.getRoot(), "2"));
        when(repositoryDao.create(notNull(InternalRepository.class))).thenAnswer(returnFirst());
        when(repositoryDao.update(notNull(InternalRepository.class))).thenAnswer(returnFirst());
        when(scmService.isSupported(same(EXISTING_REPO), same(ScmFeature.FORK))).thenReturn(true);

        Repository repository = repositoryService.fork(new RepositoryForkRequest.Builder(EXISTING_REPO).build());
        assertNotNull("Repository was not created", repository);
        assertEquals("Slug not expected value", EXISTING_REPO.getName(), repository.getName());
        assertEquals("Slug not expected value", EXISTING_REPO.getSlug(), repository.getSlug());
        assertSame("Origin not set", EXISTING_REPO, repository.getOrigin());
        assertSame("Fork should be in the user's personal project", PERSONAL_PROJECT, repository.getProject());

        InOrder ordered = inOrder(eventPublisher, repositoryDao, scmService);
        ordered.verify(eventPublisher).publish(isA(RepositoryForkRequestedEvent.class));
        ordered.verify(repositoryDao).create(isA(InternalRepository.class));
        ordered.verify(scmService).fork(same(EXISTING_REPO), notNull(ForkCommandParameters.class));
        ordered.verify(repositoryDao).update(isA(InternalRepository.class));
        ordered.verify(eventPublisher).publish(isA(RepositoryForkedEvent.class));
    }

    @Test
    public void testForkSucceedsOnRetryWhenBlockedByDir() throws Exception {
        Path root = folder.getRoot().toPath();
        Path repoDirectory1 = Files.createDirectory(root.resolve("321")); // Exists on disk
        Path repoDirectory2 = Files.createDirectory(root.resolve("322")); // Exists on disk
        Path repoDirectory3 = Files.createDirectory(root.resolve("323")); // Exists on disk
        Path repoDirectory4 = Files.createDirectory(root.resolve("324")); // Exists on disk
        Path repoDirectory5 = root.resolve("325"); // Available for use
        try {
            repositoryService.setMaxRepositoryCreateAttempts(5); // Ensure only 5 create attempts
            testFork(repoDirectory1.toFile(),
                    repoDirectory2.toFile(),
                    repoDirectory3.toFile(),
                    repoDirectory4.toFile(),
                    repoDirectory5.toFile());
        } finally {
            // Cleanup the test directories
            assertTrue(0 == deletePath(repoDirectory1, repoDirectory2, repoDirectory3, repoDirectory4, repoDirectory5));
            // The fork was created on disk
            verify(scmService).isSupported(isA(Repository.class), eq(ScmFeature.FORK));
            verify(scmService).fork(isA(Repository.class), isA(ForkCommandParameters.class));
            // repoDirectory1 through repoDirectory5 were returned by getRepositoryDir()
            verify(propertiesService, times(5)).getRepositoryDir(isA(Repository.class));
        }
    }

    @Test(expected = RepositoryForkCanceledException.class)
    public void testForkThrowsWhenCanceled() {
        doAnswer(invocationOnMock -> {
            RepositoryForkRequestedEvent event = (RepositoryForkRequestedEvent) invocationOnMock.getArguments()[0];
            assertNotNull(event.getRepository());
            assertFalse(event.isCanceled());

            event.cancel(new KeyedMessage("key", "localisedMessage", "rootMessage"));
            assertTrue(event.isCanceled());

            return null;
        }).when(eventPublisher).publish(isA(RepositoryForkRequestedEvent.class));

        when(projectService.getPersonalProject()).thenReturn(PERSONAL_PROJECT);
        when(scmService.isSupported(same(EXISTING_REPO), same(ScmFeature.FORK))).thenReturn(true);

        try {
            repositoryService.fork(new RepositoryForkRequest.Builder(EXISTING_REPO).name("Name").build());
        } finally {
            verifyZeroInteractions(repositoryDao);
        }
    }

    @Test(expected = ForkingDisabledException.class)
    public void testForkThrowsWhenDisabled() {
        Repository parent = new InternalRepository.Builder(EXISTING_REPO)
                .forkable(false)
                .build();

        when(scmService.isSupported(same(parent), same(ScmFeature.FORK))).thenReturn(true);

        try {
            repositoryService.fork(new RepositoryForkRequest.Builder(parent).build());
        } finally {
            verify(scmService).isSupported(same(parent), same(ScmFeature.FORK));
            verifyNoMoreInteractions(scmService);
            verifyZeroInteractions(eventPublisher);
            verifyZeroInteractions(projectService);
            verifyZeroInteractions(repositoryDao);
        }
    }

    @Test(expected = ForkingDisabledException.class)
    public void testForkThrowsWhenGloballyDisabled() {
        reset(featureManager);
        when(featureManager.isEnabled(StandardFeature.FORKS)).thenReturn(false);

        try {
            repositoryService.fork(new RepositoryForkRequest.Builder(EXISTING_REPO).build());
        } finally {
            verifyZeroInteractions(eventPublisher);
            verifyZeroInteractions(projectService);
            verifyZeroInteractions(repositoryDao);
            verifyZeroInteractions(scmService);
        }
    }

    @Test(expected = NullPointerException.class)
    public void testForkWithNullRequest() {
        repositoryService.fork(null);
    }

    @Test(expected = PersonalRepositoryDisabledException.class)
    public void testForkWhenPersonalRepositoryDisabled() {
        Repository parent = new InternalRepository.Builder(EXISTING_REPO)
                .name("Test Repo").build();

        when(featureManager.isEnabled(StandardFeature.PERSONAL_REPOS)).thenReturn(false);
        when(scmService.isSupported(same(parent), same(ScmFeature.FORK))).thenReturn(true);

        repositoryService.fork(new RepositoryForkRequest.Builder(parent).build());
    }

    @Test(expected = FeatureUnsupportedScmException.class)
    public void testForkWithoutScmSupport() {
        when(featureManager.isEnabled(StandardFeature.PERSONAL_REPOS)).thenReturn(false);

        try {
            repositoryService.fork(new RepositoryForkRequest.Builder(EXISTING_REPO).build());
        } finally {
            verify(scmService).isSupported(same(EXISTING_REPO), same(ScmFeature.FORK));
        }
    }

    @Test
    public void testGetSize() {
        when(scmService.getSize(same(EXISTING_REPO))).thenReturn(42L);

        assertEquals(42L, repositoryService.getSize(EXISTING_REPO));
    }

    @Test
    public void testIsEmpty() {
        when(cache.get(eq(EXISTING_REPO.getId()), isNotNull(Supplier.class))).thenReturn(true);

        //Verify that the isEmpty check uses the cache, not the ScmService
        assertTrue(repositoryService.isEmpty(EXISTING_REPO));
        verify(scmService, never()).isEmpty(any(Repository.class));

        ArgumentCaptor<Supplier<Boolean>> supplierCaptor = (ArgumentCaptor) ArgumentCaptor.forClass(Supplier.class);
        verify(cache).get(eq(EXISTING_REPO.getId()), supplierCaptor.capture());

        //Verify that the Supplier provided to the get(Integer, Supplier<Boolean>) call uses the ScmService
        Supplier<Boolean> supplier = supplierCaptor.getValue();
        assertNotNull(supplier);

        supplier.get();
        verify(scmService).isEmpty(same(EXISTING_REPO));
    }

    @Test
    public void testOnRepositoryDeleted() {
        repositoryService.onRepositoryDeleted(new RepositoryDeletedEvent(this, EXISTING_REPO));

        verify(cache).remove(eq(EXISTING_REPO.getId()));
    }

    @Test
    public void testOnRepositoryRefsChanged() {
        repositoryService.onRepositoryRefsChanged(new RepositoryPushEvent(this, EXISTING_REPO,
                Collections.emptySet()));

        verify(cache).remove(eq(EXISTING_REPO.getId()));
    }

    @Test
    public void testPersonalRepositoryEnabled() {
        when(propertiesService.getRepositoryDir(isA(Repository.class))).thenReturn(new File(folder.getRoot(), "1"));
        when(repositoryDao.create(any(InternalRepository.class))).thenAnswer(returnFirst());
        when(repositoryDao.update(any(InternalRepository.class))).thenAnswer(returnFirst());

        Repository repository = repositoryService.create(new RepositoryCreateRequest.Builder()
                .name("Personal Repo").project(USER_PERSONAL_PROJECT).scmId(SCM_ID).publiclyAccessible(false).build());
        assertNotNull("Repository was not created", repository);

        verify(scmService).create(notNull(InternalRepository.class));
    }

    @Test(expected = PersonalRepositoryDisabledException.class)
    public void testPersonalRepositoryDisabled() throws Exception {
        reset(featureManager);
        when(featureManager.isEnabled(StandardFeature.PERSONAL_REPOS)).thenReturn(false);

        repositoryService.create(new RepositoryCreateRequest.Builder()
                .name("Personal Repo").project(USER_PERSONAL_PROJECT).scmId(SCM_ID).publiclyAccessible(false).build());
    }

    @Test
    public void testSearchWithEmptyRequest() {
        RepositorySearchRequest request = new RepositorySearchRequest.Builder().build();

        Page<InternalRepository> page = mock(Page.class);
        Predicate<Repository> predicate = mock(Predicate.class);

        PageRequest pageRequest = mock(PageRequest.class);
        when(pageRequest.buildRestrictedPageRequest(eq(1000))).thenReturn(pageRequest);

        when(predicateFactory.createRepositoryAccessiblePredicate()).thenReturn(predicate);
        when(repositoryDao.findAll(same(pageRequest), same(predicate))).thenReturn(page);

        assertSame(page, repositoryService.search(request, pageRequest));
    }

    @Test
    public void testSearchWithRepositoryName() {
        RepositorySearchRequest request = new RepositorySearchRequest.Builder()
                .name("Test")
                .build();

        Page<InternalRepository> page = mock(Page.class);
        Predicate<Repository> predicate = mock(Predicate.class);

        when(predicateFactory.createRepositoryAccessiblePredicate()).thenReturn(predicate);
        when(repositoryDao.search(argThat(matchesSearchRequest(request)), isA(PageRequest.class), same(predicate))).thenReturn(page);

        assertSame(page, repositoryService.search(request, PageUtils.newRequest(0, 25)));
    }

    @Test
    public void testSearchWithRepositoryAndProjectName() {
        RepositorySearchRequest request = new RepositorySearchRequest.Builder()
                .name("Test")
                .projectName("Test Project")
                .build();

        Page<InternalRepository> page = mock(Page.class);
        Predicate<Repository> predicate = mock(Predicate.class);

        when(predicateFactory.createRepositoryAccessiblePredicate()).thenReturn(predicate);
        when(repositoryDao.search(argThat(matchesSearchRequest(request)), isA(PageRequest.class), same(predicate))).thenReturn(page);

        assertSame(page, repositoryService.search(request, PageUtils.newRequest(0, 25)));
    }

    @Test
    public void testSearchAppliesPermission() {
        RepositorySearchRequest request = new RepositorySearchRequest.Builder()
                .permission(Permission.REPO_WRITE)
                .build();

        Page<InternalRepository> page = mock(Page.class);
        Predicate<Repository> predicate = mock(Predicate.class);

        when(predicateFactory.createRepositoryPermissionPredicate(same(Permission.REPO_WRITE))).thenReturn(predicate);
        when(repositoryDao.search(argThat(matchesSearchRequest(request)), isA(PageRequest.class), same(predicate))).thenReturn(page);

        assertSame(page, repositoryService.search(request, PageUtils.newRequest(0, 25)));
    }

    @Test
    public void testSearchPublicOnlyWhenPublicAccessIsEnabled() {
        when(featureManager.isEnabled(StandardFeature.PUBLIC_ACCESS)).thenReturn(true);
        RepositorySearchRequest request = new RepositorySearchRequest.Builder()
                .visibility(RepositoryVisibility.PUBLIC)
                .build();

        Page<InternalRepository> page = mock(Page.class);
        when(repositoryDao.search(argThat(matchesSearchRequest(request)), isA(PageRequest.class), isA(Predicate.class))).thenReturn(page);
        assertSame(page, repositoryService.search(request, PageUtils.newRequest(0, 25)));
    }

    @Test
    public void testSearchPublicOnlyWhenPublicAccessIsDisabled() {
        when(featureManager.isEnabled(StandardFeature.PUBLIC_ACCESS)).thenReturn(false);
        RepositorySearchRequest request = new RepositorySearchRequest.Builder()
                .visibility(RepositoryVisibility.PUBLIC)
                .build();
        Page<Repository> page = repositoryService.search(request, PageUtils.newRequest(0, 25));
        assertThat(page, PageMatchers.isEmptyPage());
        verifyZeroInteractions(repositoryDao);
    }

    @Test
    public void testSearchPrivateOnlyWhenPublicAccessIsDisabled() {
        when(featureManager.isEnabled(StandardFeature.PUBLIC_ACCESS)).thenReturn(false);
        when(predicateFactory.createRepositoryAccessiblePredicate()).thenReturn(mock(Predicate.class));
        RepositorySearchRequest request = new RepositorySearchRequest.Builder()
                .visibility(RepositoryVisibility.PRIVATE)
                .build();

        Page<InternalRepository> page = mock(Page.class);
        when(repositoryDao.search(isA(RepositorySearchCriteria.class), isA(PageRequest.class), isA(Predicate.class))).thenReturn(page);
        assertSame(page, repositoryService.search(request, PageUtils.newRequest(0, 25)));

        ArgumentCaptor<RepositorySearchCriteria> captor = ArgumentCaptor.forClass(RepositorySearchCriteria.class);
        verify(repositoryDao).search(captor.capture(), isA(PageRequest.class), isA(Predicate.class));
        assertFalse("No visibility should be passed through if a public access is disabled", captor.getValue().hasVisibility());
    }

    @Test(expected = NullPointerException.class)
    public void testSearchWithNullCriteria() {
        repositoryService.search(null, PageUtils.newRequest(0, 25));
    }

    @Test(expected = NullPointerException.class)
    public void testSearchWithNullPageRequest() {
        repositoryService.search(new RepositorySearchRequest.Builder().build(), null);
    }

    @Test
    public void testUpdate() throws Exception {
        when(repositoryDao.getById(eq(EXISTING_REPO.getId()))).thenReturn(EXISTING_REPO);
        when(repositoryDao.update(isA(InternalRepository.class))).thenAnswer(returnFirst());

        InternalRepository repository = repositoryService.update(new RepositoryUpdateRequest.Builder(EXISTING_REPO)
                .name("New Name").publiclyAccessible(true).build());
        assertNotNull("Repository was not updated", repository);
        assertEquals("Name was not updated", "New Name", repository.getName());
        assertEquals("Slug was not updated", "new-name", repository.getSlug());
        assertTrue("Repository was not made public", repository.isPublic());

        InOrder ordered = inOrder(eventPublisher, repositoryDao, validator);
        ordered.verify(validator).validate(isA(InternalRepository.class), same(Update.class));
        ordered.verify(eventPublisher).publish(isA(RepositoryModificationRequestedEvent.class));
        ordered.verify(repositoryDao).update(same(repository));
        ordered.verify(eventPublisher).publish(isA(RepositoryModifiedEvent.class));
    }

    @Test
    public void testUpdateProject() throws Exception {
        when(repositoryDao.getById(eq(EXISTING_REPO.getId()))).thenReturn(EXISTING_REPO);
        when(repositoryDao.update(isA(InternalRepository.class))).thenAnswer(returnFirst());

        InternalRepository repository = repositoryService.update(
                new RepositoryUpdateRequest.Builder(EXISTING_REPO)
                        .project(PERSONAL_PROJECT)
                        .build()
        );


        assertNotNull("Repository should not be null", repository);
        assertEquals("Project should have be updated", PERSONAL_PROJECT, repository.getProject());

        InOrder ordered = inOrder(eventPublisher, repositoryDao, validator);
        ordered.verify(validator).validate(isA(InternalRepository.class), same(Update.class));
        ordered.verify(eventPublisher).publish(isA(RepositoryModificationRequestedEvent.class));
        ordered.verify(repositoryDao).update(same(repository));
        ordered.verify(eventPublisher).publish(isA(RepositoryModifiedEvent.class));
    }

    @Test(expected = NoSuchEntityException.class)
    public void testUpdateThrowsOnNonexistentRepository() throws Exception {
        repositoryService.update(new RepositoryUpdateRequest.Builder()
                .id(20)
                .name("new name")
                .build());
    }

    @Test(expected = IllegalStateException.class)
    public void testUpdateWithNullName() {
        repositoryService.update(new RepositoryUpdateRequest.Builder()
                .id(1)
                .project(null)
                .build());
    }

    @Test
    public void testGetCloneLinksForCurrentUser() {
        when(authenticationContext.getCurrentUser()).thenReturn(USER);

        ScmProtocol httpProtocol = mock(ScmProtocol.class);
        when(httpProtocol.getCloneUrl(EXISTING_REPO, USER)).thenReturn("cloneurl1");
        when(httpProtocol.getName()).thenReturn("http");

        ScmProtocol sshProtocol = mock(ScmProtocol.class);
        when(sshProtocol.getCloneUrl(EXISTING_REPO, USER)).thenReturn("cloneurl2");
        when(sshProtocol.getName()).thenReturn("ssh");

        when(scmService.getProtocols(EXISTING_REPO)).thenReturn(ImmutableSet.of(httpProtocol, sshProtocol));

        assertEquals(
                ImmutableSet.<NamedLink>of(new SimpleNamedLink("cloneurl1", "http"), new SimpleNamedLink("cloneurl2", "ssh")),
                repositoryService.getCloneLinks(new RepositoryCloneLinksRequest.Builder().repository(EXISTING_REPO).build()));

        verify(httpProtocol).getCloneUrl(EXISTING_REPO, USER);
        verify(sshProtocol).getCloneUrl(EXISTING_REPO, USER);
    }

    @Test
    public void testGetCloneLinksForSpecificUser() {
        when(authenticationContext.getCurrentUser()).thenReturn(USER);

        ScmProtocol httpProtocol = mock(ScmProtocol.class);
        when(httpProtocol.getCloneUrl(EXISTING_REPO, ADMIN)).thenReturn("cloneurl1");
        when(httpProtocol.getName()).thenReturn("http");

        ScmProtocol sshProtocol = mock(ScmProtocol.class);
        when(sshProtocol.getCloneUrl(EXISTING_REPO, ADMIN)).thenReturn("cloneurl2");
        when(sshProtocol.getName()).thenReturn("ssh");

        when(scmService.getProtocols(EXISTING_REPO)).thenReturn(ImmutableSet.of(httpProtocol, sshProtocol));

        assertEquals(
                ImmutableSet.<NamedLink>of(new SimpleNamedLink("cloneurl1", "http"), new SimpleNamedLink("cloneurl2", "ssh")),
                repositoryService.getCloneLinks(new RepositoryCloneLinksRequest.Builder().user(ADMIN).repository(EXISTING_REPO).build()));

        verify(httpProtocol).getCloneUrl(EXISTING_REPO, ADMIN);
        verify(sshProtocol).getCloneUrl(EXISTING_REPO, ADMIN);
    }

    @Test
    public void testGetCloneLinksForNoUser() {
        when(authenticationContext.getCurrentUser()).thenReturn(USER);

        ScmProtocol httpProtocol = mock(ScmProtocol.class);
        when(httpProtocol.getCloneUrl(EXISTING_REPO, null)).thenReturn("cloneurl1");
        when(httpProtocol.getName()).thenReturn("http");

        ScmProtocol sshProtocol = mock(ScmProtocol.class);
        when(sshProtocol.getCloneUrl(EXISTING_REPO, null)).thenReturn("cloneurl2");
        when(sshProtocol.getName()).thenReturn("ssh");

        when(scmService.getProtocols(EXISTING_REPO)).thenReturn(ImmutableSet.of(httpProtocol, sshProtocol));

        assertEquals(
                ImmutableSet.<NamedLink>of(new SimpleNamedLink("cloneurl1", "http"), new SimpleNamedLink("cloneurl2", "ssh")),
                repositoryService.getCloneLinks(new RepositoryCloneLinksRequest.Builder().user(null).repository(EXISTING_REPO).build()));

        verify(httpProtocol).getCloneUrl(EXISTING_REPO, null);
        verify(sshProtocol).getCloneUrl(EXISTING_REPO, null);
    }

    @Test
    public void testGetNoCloneLinks() {
        when(scmService.getProtocols(EXISTING_REPO)).thenReturn(Collections.emptySet());

        assertEquals(
                Collections.<NamedLink>emptySet(),
                repositoryService.getCloneLinks(new RepositoryCloneLinksRequest.Builder().repository(EXISTING_REPO).build()));
    }

    @Test
    public void testGetCloneLinksWhereOneUnavailable() {
        ScmProtocol httpProtocol = mock(ScmProtocol.class);
        when(httpProtocol.getCloneUrl(EXISTING_REPO, null)).thenReturn("cloneurl1");
        when(httpProtocol.getName()).thenReturn("http");

        ScmProtocol sshProtocol = mock(ScmProtocol.class);
        when(sshProtocol.getCloneUrl(EXISTING_REPO, null)).thenReturn(null);
        when(sshProtocol.getName()).thenReturn("ssh");

        when(scmService.getProtocols(EXISTING_REPO)).thenReturn(ImmutableSet.of(httpProtocol, sshProtocol));

        assertEquals(
                ImmutableSet.<NamedLink>of(new SimpleNamedLink("cloneurl1", "http")),
                repositoryService.getCloneLinks(new RepositoryCloneLinksRequest.Builder().repository(EXISTING_REPO).build()));
    }

    /**
     * Delete files or directories if they exist
     *
     * @param paths one or more paths to delete
     * @return the number of items that could not be deleted
     */
    private static int deletePath(Path... paths) {
        int failures = 0;
        for (Path path : paths) {
            try {
                if (Files.isDirectory(path)) {
                    org.apache.commons.io.FileUtils.deleteDirectory(path.toFile());
                } else {
                    Files.deleteIfExists(path);
                }
            } catch (IOException e) {
                failures++;
            }
        }
        return failures;
    }

    private static Matcher<RepositorySearchCriteria> criteriaWithName(String expectedName) {
        return new FeatureMatcher<RepositorySearchCriteria, String>(equalTo(expectedName), "filter name that", "name") {

            @Override
            protected String featureValueOf(RepositorySearchCriteria actual) {
                return actual.getName();
            }
        };
    }

    private static Matcher<RepositorySearchCriteria> criteriaWithProjectName(String expectedName) {
        return new FeatureMatcher<RepositorySearchCriteria, String>(equalTo(expectedName), "filter name that", "name") {

            @Override
            protected String featureValueOf(RepositorySearchCriteria actual) {
                return actual.getProjectName();
            }
        };
    }

    private static Matcher<RepositorySearchCriteria> matchesSearchRequest(RepositorySearchRequest request) {
        return allOf(
                criteriaWithName(request.getName()),
                criteriaWithProjectName(request.getProjectName())
        );
    }

    private void testFork(File repositoryDir, File... additionalRepoDirs) throws Exception {
        when(projectService.getPersonalProject()).thenReturn(PERSONAL_PROJECT);
        when(propertiesService.getRepositoryDir(isA(InternalRepository.class)))
                .thenReturn(repositoryDir, additionalRepoDirs);
        when(repositoryDao.create(notNull(InternalRepository.class))).thenAnswer(returnFirst());
        when(repositoryDao.update(notNull(InternalRepository.class))).thenAnswer(returnFirst());
        when(scmService.isSupported(same(EXISTING_REPO), same(ScmFeature.FORK))).thenReturn(true);

        Repository repository = repositoryService.fork(new RepositoryForkRequest.Builder(EXISTING_REPO)
                .name("Test Repo")
                .build());
        assertNotNull("Repository was not created", repository);
        assertEquals("Slug not expected value", "Test Repo", repository.getName());
        assertEquals("Slug not expected value", "test-repo", repository.getSlug());
        assertSame("Origin not set", EXISTING_REPO, repository.getOrigin());
        assertSame("Fork should be in the user's personal project", PERSONAL_PROJECT, repository.getProject());

        InOrder ordered = inOrder(eventPublisher, repositoryDao, scmService);
        ordered.verify(eventPublisher).publish(isA(RepositoryForkRequestedEvent.class));
        ordered.verify(repositoryDao).create(isA(InternalRepository.class));
        ordered.verify(scmService).fork(same(EXISTING_REPO), notNull(ForkCommandParameters.class));
        ordered.verify(repositoryDao).update(isA(InternalRepository.class));
        ordered.verify(eventPublisher).publish(isA(RepositoryForkedEvent.class));
    }

    private void runBlockedCreateTest(int numExpectedDaoInteractions, File repositoryDir, File... additionalRepositoryDirs) {
        TransactionStatus status = mock(TransactionStatus.class);
        when(transactionManager.getTransaction(any(TransactionDefinition.class))).thenReturn(status);

        ArgumentCaptor<InternalRepository> captor = ArgumentCaptor.forClass(InternalRepository.class);

        when(propertiesService.getRepositoryDir(isA(Repository.class))).thenReturn(repositoryDir, additionalRepositoryDirs);
        when(repositoryDao.create(captor.capture())).thenAnswer(returnFirst());
        when(repositoryDao.update(captor.capture())).thenAnswer(returnFirst());

        try {
            repositoryService.create(new RepositoryCreateRequest.Builder()
                    .project(EXISTING_PROJECT)
                    .name("Test Repo")
                    .scmId(SCM_ID)
                    .build());
        } finally {
            InOrder ordered = inOrder(eventPublisher, repositoryDao);
            ordered.verify(eventPublisher).publish(isA(RepositoryCreationRequestedEvent.class));
            ordered.verify(repositoryDao).create(isA(InternalRepository.class));
            ordered.verify(repositoryDao).update(isA(InternalRepository.class));
            ordered.verify(eventPublisher).publish(isA(RepositoryCreationFailedEvent.class));

            List<InternalRepository> captured = captor.getAllValues();
            assertEquals(numExpectedDaoInteractions, captured.size());
            assertSame(Repository.State.INITIALISING, captured.get(0).getState());
            assertSame(Repository.State.INITIALISATION_FAILED, captured.get(1).getState());
        }
    }
}
