package com.atlassian.bitbucket.mesh.repository;

import com.atlassian.bitbucket.mesh.git.hook.GitHookService;
import com.atlassian.bitbucket.mesh.io.MoreFiles;
import com.atlassian.bitbucket.mesh.junit.TemporaryDirectory;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.FileTime;
import java.util.concurrent.TimeUnit;

import static com.atlassian.bitbucket.mesh.repository.SharedHomeBitbucketServerShard.ID;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThan;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;

public class SharedHomeBitbucketServerShardTest {

    private static final String HIERARCHY_ID = "12345123451234512345";

    @Rule
    public TemporaryDirectory tempDir = new TemporaryDirectory();

    private Path rootDir;
    private SharedHomeBitbucketServerShard shard;

    @Before
    public void setup() throws IOException {
        rootDir = tempDir.newFolder("shared");

        Path workTreesDir = tempDir.newFolder("work-trees");
        shard = new SharedHomeBitbucketServerShard(rootDir, workTreesDir);
    }

    @Test
    public void testAttachStorage() {
        Path repositoriesDir = shard.getRepositoriesDir(HIERARCHY_ID);
        Path repositoryDir = MoreFiles.mkdir(repositoriesDir.resolve("1"));

        DefaultBitbucketServerRepository repository = new DefaultBitbucketServerRepository(ID, HIERARCHY_ID, "1");
        assertSame(StorageStatus.ATTACHED, shard.attachStorage(repository));

        assertEquals(repositoryDir.resolve("DELETED"), repository.getDeletedMarker());
        assertEquals(MoreFiles.resolve(rootDir, "config", "git", "hooks"), repository.getHooksDir());
        assertEquals(MoreFiles.resolve(rootDir, "data", "git-lfs", "storage", HIERARCHY_ID), repository.getLfsDir());
        assertEquals(repositoryDir, repository.getPath());
        assertEquals(MoreFiles.resolve(rootDir, "config", "git", "templates"), repository.getTemplateDir());
    }

    @Test
    public void testAttachStorageToDeletedRepository() throws IOException {
        Path repositoriesDir = shard.getRepositoriesDir(HIERARCHY_ID);
        Path repositoryDir = MoreFiles.mkdir(repositoriesDir.resolve("1"));
        MoreFiles.write(repositoryDir.resolve("DELETED"), HIERARCHY_ID);

        DefaultBitbucketServerRepository repository = new DefaultBitbucketServerRepository(ID, HIERARCHY_ID, "1");
        assertSame(StorageStatus.DELETED, shard.attachStorage(repository));
    }

    @Test
    public void testAttachStorageToNonexistentRepository() {
        DefaultBitbucketServerRepository repository = new DefaultBitbucketServerRepository(ID, HIERARCHY_ID, "1");
        assertSame(StorageStatus.NONEXISTENT, shard.attachStorage(repository));

        //This doesn't verify all the paths like testAttachStorage, but we do check path is set
        assertEquals(shard.getRepositoriesDir(HIERARCHY_ID).resolve("1"), repository.getPath());
    }

    @Test
    public void testAttachStorageToOccludedRepository() throws IOException {
        Path repositoriesDir = MoreFiles.mkdir(shard.getRepositoriesDir(HIERARCHY_ID));
        Path repositoryDir = repositoriesDir.resolve("1");
        MoreFiles.touch(repositoryDir);

        DefaultBitbucketServerRepository repository = new DefaultBitbucketServerRepository(ID, HIERARCHY_ID, "1");
        assertSame(StorageStatus.OCCLUDED, shard.attachStorage(repository));
    }

    @Test
    public void testGetHooksDir() {
        assertEquals(MoreFiles.resolve(rootDir, "config", "git", "hooks"), shard.getHooksDir());
    }

    @Test
    public void testGetId() {
        assertEquals(ID, shard.getId());
    }

    @Test
    public void testGetLayout() {
        assertEquals("/templates/dc-sharedhome-datastore.tar", shard.getLayoutTar());
    }

    @Test
    public void testGetLfsDir() {
        Path lfsDir = shard.getLfsDir(HIERARCHY_ID);
        assertEquals(MoreFiles.resolve(rootDir, "data", "git-lfs", "storage", HIERARCHY_ID), lfsDir);
        assertTrue("The LFS storage directory should exist", Files.isDirectory(lfsDir.getParent()));
    }

    @Test
    public void testGetRepositoriesDir() {
        assertEquals(MoreFiles.resolve(rootDir, "data", "repositories"),
                shard.getRepositoriesDir(HIERARCHY_ID));
    }

    @Test
    public void testGetRootDir() {
        assertEquals(rootDir, shard.getRootDir());
    }

    @Test
    public void testGetState() {
        assertSame(ShardState.AVAILABLE, shard.getState());
    }

    @Test
    public void testGetTemplateDir() {
        assertEquals(MoreFiles.resolve(rootDir, "config", "git", "templates"), shard.getTemplateDir());
    }

    @Test
    public void testGetUsableSpace() throws IOException {
        //This verifies that we get back a positive size and no exception is thrown. Trying to validate
        //the number any more closely just ends up flaky
        assertThat(shard.getUsableSpace(), greaterThan(0L));
    }

    @Test
    public void testStart() throws IOException {
        Path hooksDir = MoreFiles.mkdir(shard.getHooksDir());
        Path newHook = hooksDir.resolve("new");
        Path oldHook = hooksDir.resolve("old");

        //Create an "old" file in hooksDir to verify it's pruned
        MoreFiles.write(oldHook, "Some test content", StandardOpenOption.CREATE_NEW);
        //Modify the modification timestamp to ensure the file is old
        BasicFileAttributeView attributes = Files.getFileAttributeView(oldHook, BasicFileAttributeView.class);
        FileTime lastModified = attributes.readAttributes().lastModifiedTime();
        lastModified = FileTime.fromMillis(lastModified.toMillis() - TimeUnit.HOURS.toMillis(1L));
        attributes.setTimes(lastModified, lastModified, lastModified);

        GitHookService hookService = mock(GitHookService.class);
        doAnswer(invocation -> {
            //Write a "new" hook to verify it's not pruned
            MoreFiles.write(newHook, "More test content", StandardOpenOption.CREATE_NEW);

            return null;
        }).when(hookService).installHooks(eq(hooksDir));

        shard.start(hookService);

        assertTrue("system-config was not unpacked",
                Files.isRegularFile(MoreFiles.resolve(rootDir, "config", "git", "system-config")));
        assertFalse("The stale hook was not pruned", Files.exists(oldHook));
        assertTrue("The new hook was pruned", Files.isRegularFile(newHook));
    }
}
