package com.atlassian.bitbucket.mesh.repository;

import com.atlassian.bitbucket.mesh.event.EventPublisher;
import com.atlassian.bitbucket.mesh.git.GitAgent;
import com.atlassian.bitbucket.mesh.git.GitRepositoryLayout;
import com.atlassian.bitbucket.mesh.git.builder.GitProcess;
import com.atlassian.bitbucket.mesh.git.builder.GitProcessBuilder;
import com.atlassian.bitbucket.mesh.git.builder.GitProcessBuilderFactory;
import com.atlassian.bitbucket.mesh.git.builder.init.GitInitBuilder;
import com.atlassian.bitbucket.mesh.git.exception.CommandFailedException;
import com.atlassian.bitbucket.mesh.io.MoreFiles;
import com.atlassian.bitbucket.mesh.junit.LogListener;
import com.atlassian.bitbucket.mesh.junit.TemporaryDirectory;
import com.atlassian.bitbucket.mesh.transaction.StubTransactionManager;
import com.atlassian.bitbucket.mesh.transaction.TransactionManager;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.junit.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.ScheduledExecutorService;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;

/**
 * This test is not particularly thorough. The bulk of the testing is done by {@link DefaultRepositoryManagerTest},
 * which uses a fully wired {@link DefaultRepositoryManager} to create/delete/etc. "for real". This test is only to
 * cover codepaths that are not readily testable using real data.
 */
@RunWith(MockitoJUnitRunner.class)
public class DefaultRepositoryManagerMockTest {

    private static final String ID = BitbucketServerRepository.formatId("12233344445555543210", "1");

    @Rule
    public final LogListener logListener = new LogListener(DefaultRepositoryManager.class);
    @Rule
    public final TemporaryDirectory tempDir = new TemporaryDirectory();

    @Spy//To allow @InjectMocks to inject it
    private final TransactionManager transactionManager = new StubTransactionManager();
    @Mock
    private GitAgent agent;
    @Mock(answer = Answers.RETURNS_MOCKS)
    private GitProcessBuilderFactory builderFactory;
    @Mock
    private DataStoreManager dataStoreManager;
    @Mock
    private EventPublisher eventPublisher;
    @Mock
    private ScheduledExecutorService executorService;
    @Mock
    private Repository repository;
    @Mock
    private RepositoryAccessListener repositoryAccessListener;
    @Mock
    private GitRepositoryLayout repositoryLayout;
    @Mock
    private RepositoryAttributeManager repositoryAttributeManager;
    @Mock
    private WorkTreeManager workTreeManager;

    @InjectMocks
    private DefaultRepositoryManager repositoryManager;

    @Test
    public void testCreateAlreadyAttached() {
        when(dataStoreManager.attachStorage(any())).thenAnswer(new AttachStorageAnswer(StorageStatus.ATTACHED));

        RepositoryAlreadyExistsException e = assertThrows(RepositoryAlreadyExistsException.class,
                () -> repositoryManager.create(new CreateRepositoryParameters.Builder(ID).build()));
        assertEquals(ID, e.getId());
        assertThat(e.getMessage(), containsString("a directory exists at the same location"));
        logListener.assertError("the repository already exists (status = ATTACHED)");
    }

    @Test
    public void testCreateAlreadyDeleted() {
        when(dataStoreManager.attachStorage(any())).thenAnswer(new AttachStorageAnswer(StorageStatus.DELETED));

        RepositoryAlreadyExistsException e = assertThrows(RepositoryAlreadyExistsException.class,
                () -> repositoryManager.create(new CreateRepositoryParameters.Builder(ID).build()));
        assertEquals(ID, e.getId());
        assertThat(e.getMessage(), containsString("a directory exists at the same location"));
        logListener.assertError("the repository already exists (status = DELETED)");
    }

    @Test
    public void testCreateCleansUpAfterFailure() throws Exception {
        String ID = BitbucketServerRepository.formatId("12233344445555543210", "1");

        Path repositoriesDir = tempDir.newFolder("repositories");
        Path repositoryDir = repositoriesDir.resolve("1");

        @SuppressWarnings("unchecked")
        GitProcess<Void> process = mock(GitProcess.class);
        when(process.run()).thenAnswer(invocation -> {
            //When the init process "runs", create some files on disk for the repository
            try {
                Files.createDirectory(repositoryDir);
                MoreFiles.write(repositoryDir.resolve("config"), "[core]\n\tfoo = bar\n");
            } catch (IOException e) {
                throw new UncheckedIOException("Failed to prepare repository contents", e);
            }

            return null;
        });

        GitInitBuilder initBuilder = mock(GitInitBuilder.class, InvocationOnMock::getMock);
        doReturn(process).when(initBuilder).build();

        GitProcessBuilder builder = mock(GitProcessBuilder.class);
        when(builder.init()).thenReturn(initBuilder);

        //When trying to set the default branch, simulate a failure
        doThrow(CommandFailedException.class).when(agent).setHead(any(), eq("refs/heads/main"));
        when(builderFactory.builder()).thenReturn(builder);
        when(dataStoreManager.attachStorage(any()))
                .thenAnswer(new AttachStorageAnswer(repositoriesDir, StorageStatus.NONEXISTENT));

        assertThrows(CommandFailedException.class,
                () -> repositoryManager.create(new CreateRepositoryParameters.Builder(ID)
                        .defaultBranch("main")
                        .build()));
        assertFalse("The repository directory should have been cleaned up", Files.exists(repositoryDir));
        logListener.assertError("Failed to create repository");
    }

    @Test
    public void testCreateOccluded() {
        when(dataStoreManager.attachStorage(any())).thenAnswer(new AttachStorageAnswer(StorageStatus.OCCLUDED));

        RepositoryAlreadyExistsException e = assertThrows(RepositoryAlreadyExistsException.class,
                () -> repositoryManager.create(new CreateRepositoryParameters.Builder(ID).build()));
        assertEquals(ID, e.getId());
        assertThat(e.getMessage(), containsString("a file exists at the same location"));
        logListener.assertError("a file exists at the same location");
    }

    @Test
    public void testCreateWithUnsupportedId() {
        CreateRepositoryFailedException e = assertThrows(CreateRepositoryFailedException.class,
                () -> repositoryManager.create(new CreateRepositoryParameters.Builder("This ID won't work").build()));
        assertEquals("This ID won't work", e.getId());
        assertEquals("Invalid repository ID", e.getMessage());
        logListener.assertError("Cannot create repository This ID won't work; the ID is invalid");
    }

    @Test
    public void testCreateWithoutDataStore() {
        when(dataStoreManager.attachStorage(any())).thenReturn(StorageStatus.NO_STORE);

        CreateRepositoryFailedException e = assertThrows(CreateRepositoryFailedException.class,
                () -> repositoryManager.create(new CreateRepositoryParameters.Builder(ID).build()));
        assertEquals(ID, e.getId());
        assertEquals("No DataStore is available", e.getMessage());
        logListener.assertError("no data store is available");
    }

    @Test
    public void testFindByWorkTreeIdReturnsNullForInvalidId() {
        String invalidId = "wt/" + ID;
        assertThat(repositoryManager.findById(invalidId), nullValue());
    }

    @Test
    public void testGetByIdNotFound() {
        String repositoryId = BitbucketServerRepository.formatId("11223344556677889900", "997");

        RepositoryNotFoundException e = assertThrows(RepositoryNotFoundException.class,
                () -> repositoryManager.getById(repositoryId));
        assertEquals(repositoryId, e.getId());
        assertThat(e.getMessage(), containsString("not found"));
    }

    @Test
    public void testGetByWorkTreeId() {
        String WORK_TREE_ID = WorkTree.formatId(1001L);
        WorkTree expectedWorkTree = mock(WorkTree.class);
        when(workTreeManager.findById(WORK_TREE_ID)).thenReturn(expectedWorkTree);

        Repository workTree = repositoryManager.getById(WORK_TREE_ID);

        assertEquals(expectedWorkTree, workTree);
    }

    @Test
    public void testGetByWorkTreeThrowsForInvalidId() {
        String invalidId = WorkTree.formatId(1234) + "/" + ID;

        RepositoryNotFoundException thrown = assertThrows(RepositoryNotFoundException.class,
                () -> repositoryManager.getById(invalidId));

        assertEquals(thrown.getMessage(), String.format("Repository with ID '%s' not found", invalidId));
    }

    @Test
    public void testResetHierarchyThrowsWhenStreamingRepositoriesFails() {
        doThrow(new UncheckedIOException("mocking I/O failure", new IOException("mocking failure")))
                .when(dataStoreManager).streamRepositories(anyString(), anyString(), any());

        assertThrows(ResetHierarchyFailedException.class,
                () -> repositoryManager.resetHierarchy("0000", "aaaaaaaaaaabbbbbbbbbb"));
        logListener.assertError("Failed to reset hierarchy aaaaaaaaaaabbbbbbbbbb on partition 0000");
    }

    private static class AttachStorageAnswer implements Answer<StorageStatus> {

        private final Path repositoriesDir;
        private final StorageStatus status;

        AttachStorageAnswer(Path repositoriesDir, StorageStatus status) {
            this.repositoriesDir = repositoriesDir;
            this.status = status;
        }

        AttachStorageAnswer(StorageStatus status) {
            this(Paths.get("repositories"), status);
        }

        @Override
        public StorageStatus answer(InvocationOnMock invocation) throws Throwable {
            BitbucketServerShard shard = mock(BitbucketServerShard.class);
            when(shard.getRepositoriesDir(anyString())).thenReturn(repositoriesDir);

            DefaultBitbucketServerRepository repository = invocation.getArgument(0);
            repository.attachTo(shard);

            return status;
        }
    }
}