package com.atlassian.bitbucket.mesh.repository;

import com.atlassian.bitbucket.mesh.app.BitbucketProperties;
import com.atlassian.bitbucket.mesh.git.hook.GitHookService;
import com.atlassian.bitbucket.mesh.io.MoreFiles;
import com.atlassian.bitbucket.mesh.junit.TemporaryDirectory;
import com.atlassian.bitbucket.mesh.layout.StorageLayout;
import com.google.common.collect.ImmutableMap;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;

import static com.atlassian.bitbucket.mesh.repository.SharedHomeBitbucketServerShard.ID;
import static org.junit.Assert.*;
import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.class)
public class BitbucketServerDataStoreTest {

    private static final String HIERARCHY_ID = "12345abcdeedcba54321";

    @Rule
    public final TemporaryDirectory tempDir = new TemporaryDirectory();

    @Mock
    private GitHookService hookService;
    @Mock
    private StorageLayout storageLayout;

    private Path sharedHome;
    private BitbucketServerDataStore store;

    @Before
    public void setup() throws IOException {
        sharedHome = tempDir.newFolder("shared");
        when(storageLayout.getTempDir()).thenReturn(tempDir.newFolder("tmp"));
    }

    @After
    public void tearDown() {
        if (store != null) {
            store.stop();
        }
    }

    @Test
    public void testAddShard() throws IOException {
        Path one = tempDir.newFolder("stores", "one");

        store = createStore();
        store.start();

        assertTrue("The shard was not added", store.addShard(new AddShardParameters.Builder("1", one).build()));

        //Verify we can address repositories on the new shard
        DefaultBitbucketServerRepository repository = new DefaultBitbucketServerRepository("1", HIERARCHY_ID, "1");
        assertSame("Shard storage could not be attached", StorageStatus.NONEXISTENT, store.attachStorage(repository));
        //Verify the repository shows a path on the new shard
        assertTrue("The repository is not in the new shard", MoreFiles.isWithin(repository.getPath(), one));
    }

    @Test
    public void testAddShardAgain() {
        store = createStore();
        store.start();

        assertFalse("The shard should not have been added",
                store.addShard(new AddShardParameters.Builder(ID, sharedHome).build()));
    }

    //These attach tests deal with how BitbucketServerDataStore _locates_ the shard to attach a repository
    //to, but they do not test how attachment works. That's done by the shard class tests instead

    @Test
    public void testAttachStorageInLegacyDataStore() throws IOException {
        Path one = tempDir.newFolder("stores", "one");

        DefaultBitbucketServerRepository repository = new DefaultBitbucketServerRepository("1", HIERARCHY_ID, "1");

        store = createStore(ImmutableMap.of("1", one));
        store.start();

        StorageStatus status = store.attachStorage(repository);
        assertSame(StorageStatus.NONEXISTENT, status);

        //Verify the repository shows a path in the legacy data store
        assertTrue("The repository is not in the data store", MoreFiles.isWithin(repository.getPath(), one));
    }

    @Test
    public void testAttachStorageInSharedHome() throws IOException {
        DefaultBitbucketServerRepository repository = new DefaultBitbucketServerRepository(ID, HIERARCHY_ID, "1");

        store = createStore();
        store.start();

        StorageStatus status = store.attachStorage(repository);
        assertSame(StorageStatus.NONEXISTENT, status);

        //Verify the repository shows a path in the shared home
        assertTrue("The repository is not in the shared home", MoreFiles.isWithin(repository.getPath(), sharedHome));
    }

    @Test
    public void testAttachStorageWithoutShard() {
        DefaultBitbucketServerRepository repository = new DefaultBitbucketServerRepository("1", HIERARCHY_ID, "1");

        store = createStore();
        store.start();

        StorageStatus status = store.attachStorage(repository);
        assertSame(StorageStatus.NO_STORE, status);
    }

    @Test
    public void testStart() throws IOException {
        Path one = tempDir.newFolder("stores", "one");
        Path two = tempDir.newFolder("stores", "two");

        store = createStore(ImmutableMap.of("1", one, "2", two));
        store.start();

        //For the shared home, and each data store, verify the existence of a directory that is created
        //when start unpacks the layout tar
        assertTrue(Files.isDirectory(sharedHome.resolve("config")));
        assertTrue(Files.isDirectory(one.resolve("repositories")));
        assertTrue(Files.isDirectory(two.resolve("repositories")));
    }

    private BitbucketServerDataStore createStore() {
        return createStore(ImmutableMap.of());
    }

    private BitbucketServerDataStore createStore(Map<String, Path> dataStores) {
        BitbucketProperties properties = new BitbucketProperties();
        properties.setSharedHome(sharedHome);

        Map<String, Path> configuredStores = properties.getDataStores();
        configuredStores.putAll(dataStores);
        if (!configuredStores.containsKey(ID)) {
            //When the sidecar is launched at runtime, Bitbucket Server always sets both `bitbucket.shared-home`
            //and `bitbucket.data-stores[0]` with the path to the shared home. This emulates that behavior and
            //ensures the configured stores always include the shared home
            configuredStores.put(ID, sharedHome);
        }

        return new BitbucketServerDataStore(properties, hookService, storageLayout);
    }
}