package com.atlassian.stash.internal.home;

import com.atlassian.bitbucket.util.FileUtils;
import com.atlassian.stash.internal.ApplicationConstants;
import com.atlassian.stash.internal.ApplicationSettings;
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.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import java.io.File;
import java.io.IOException;
import java.util.Properties;

import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class HomeDirectoryMigrationTest {
    @Rule
    public TemporaryFolder folder = new TemporaryFolder();

    @Mock
    private HomeFileMover homeFileMover;
    @Mock
    private HomeDirectoryDetails homeDirectoryDetails;
    @Mock
    private HomeSubdirectoryResolver homeSubdirectoryResolver;
    @Mock
    private Properties properties;
    @InjectMocks
    private HomeDirectoryMigration migration;

    @Before
    public void setup() throws IOException {
        File homeDir = folder.newFolder("home");
        File sharedHomeDir = folder.newFolder("shared-home");

        when(homeDirectoryDetails.getHome()).thenReturn(homeDir);
        when(homeDirectoryDetails.getSharedHome()).thenReturn(sharedHomeDir);

        when(properties.getProperty(eq(ApplicationSettings.STASH_HOME_BUILD_VERSION_PROPERTY))).thenReturn("2.12.9");
    }

    @Test(expected = HomeDirectoryMigrationException.class)
    public void testHomeMissing() {
        assertTrue(homeDirectoryDetails.getHome().delete());

        migration.perform();
    }

    @Test(expected = HomeDirectoryMigrationException.class)
    public void testSharedHomeMissing() {
        assertTrue(homeDirectoryDetails.getSharedHome().delete());

        migration.perform();
    }

    @Test
    public void testCalls() throws IOException {
        DirectoryStructure homeStructure = DirectoryStructure.create(homeDirectoryDetails.getHome());

        migration.perform();

        verify(homeSubdirectoryResolver).getHomeSubdirectoryOverride(ApplicationSettings.CONFIG_DIR_NAME);
        verify(homeSubdirectoryResolver).getHomeSubdirectoryOverride(ApplicationSettings.DATA_DIR_NAME);
        verify(homeSubdirectoryResolver).getHomeSubdirectoryOverride(ApplicationSettings.PLUGINS_DIR_NAME);

        verify(homeFileMover).move(homeStructure.configDir,
                new File(homeDirectoryDetails.getSharedHome(), ApplicationSettings.CONFIG_DIR_NAME));
        verify(homeFileMover).move(homeStructure.dataDir,
                new File(homeDirectoryDetails.getSharedHome(), ApplicationSettings.DATA_DIR_NAME));
        verify(homeFileMover).move(homeStructure.installedPluginsDir,
                new File(new File(homeDirectoryDetails.getSharedHome(), ApplicationSettings.PLUGINS_DIR_NAME),
                        ApplicationSettings.INSTALLED_PLUGINS_DIR_NAME));
        verify(homeFileMover).move(homeStructure.stashConfigFile,
                new File(homeDirectoryDetails.getSharedHome(), ApplicationConstants.LEGACY_CONFIG_FILE));
        verifyNoMoreInteractions(homeFileMover);
        homeStructure.cleanup();
    }

    @Test(expected = HomeDirectoryMigrationException.class)
    public void testConfigException() throws IOException {
        DirectoryStructure homeStructure = DirectoryStructure.create(homeDirectoryDetails.getHome());
        doThrow(new IOException("xxx")).when(homeFileMover).move(eq(homeStructure.configDir), any(File.class));

        try {
            migration.perform();
        } finally {
            verify(homeFileMover).move(eq(homeStructure.configDir), any(File.class));
            verifyNoMoreInteractions(homeFileMover);
        }
    }

    @Test(expected = HomeDirectoryMigrationException.class)
    public void testConfigExistsInBothPlaces() throws IOException {
        DirectoryStructure.create(homeDirectoryDetails.getHome());
        DirectoryStructure.createConfigDirOnly(homeDirectoryDetails.getSharedHome());

        try {
            migration.perform();
        } finally {
            verifyNoMoreInteractions(homeFileMover);
        }
    }

    @Test(expected = HomeDirectoryMigrationException.class)
    public void testDataException() throws IOException {
        DirectoryStructure homeStructure = DirectoryStructure.create(homeDirectoryDetails.getHome());
        doThrow(new IOException("xxx")).when(homeFileMover).move(eq(homeStructure.dataDir), any(File.class));

        try {
            migration.perform();
        } finally {
            verify(homeFileMover).move(eq(homeStructure.configDir), any(File.class));
            verify(homeFileMover).move(eq(homeStructure.dataDir), any(File.class));
            verifyNoMoreInteractions(homeFileMover);
        }
    }

    @Test(expected = HomeDirectoryMigrationException.class)
    public void testDataExistsInBothPlaces() throws IOException {
        DirectoryStructure.create(homeDirectoryDetails.getHome());
        DirectoryStructure.createDataDirOnly(homeDirectoryDetails.getSharedHome());

        try {
            migration.perform();
        } finally {
            verifyNoMoreInteractions(homeFileMover);
        }
    }

    @Test(expected = HomeDirectoryMigrationException.class)
    public void testInstalledPluginsException() throws IOException {
        DirectoryStructure homeStructure = DirectoryStructure.create(homeDirectoryDetails.getHome());
        doThrow(new IOException("xxx")).when(homeFileMover).move(eq(homeStructure.installedPluginsDir), any(File.class));

        try {
            migration.perform();
        } finally {
            verify(homeFileMover).move(eq(homeStructure.configDir), any(File.class));
            verify(homeFileMover).move(eq(homeStructure.dataDir), any(File.class));
            verify(homeFileMover).move(eq(homeStructure.installedPluginsDir), any(File.class));
            verifyNoMoreInteractions(homeFileMover);
        }
    }

    @Test(expected = HomeDirectoryMigrationException.class)
    public void testInstalledPluginsExistsInBothPlaces() throws IOException {
        DirectoryStructure.create(homeDirectoryDetails.getHome());
        DirectoryStructure.createInstalledPluginsDirOnly(homeDirectoryDetails.getSharedHome());

        try {
            migration.perform();
        } finally {
            verifyNoMoreInteractions(homeFileMover);
        }
    }

    @Test(expected = HomeDirectoryMigrationException.class)
    public void testStashConfigException() throws IOException {
        DirectoryStructure homeStructure = DirectoryStructure.create(homeDirectoryDetails.getHome());
        doThrow(new IOException("xxx")).when(homeFileMover).move(eq(homeStructure.stashConfigFile), any(File.class));

        try {
            migration.perform();
        } finally {
            verify(homeFileMover).move(eq(homeStructure.configDir), any(File.class));
            verify(homeFileMover).move(eq(homeStructure.dataDir), any(File.class));
            verify(homeFileMover).move(eq(homeStructure.installedPluginsDir), any(File.class));
            verify(homeFileMover).move(eq(homeStructure.stashConfigFile), any(File.class));
            verifyNoMoreInteractions(homeFileMover);
        }
    }

    @Test(expected = HomeDirectoryMigrationException.class)
    public void testStashConfigExistsInBothPlaces() throws IOException {
        DirectoryStructure.create(homeDirectoryDetails.getHome());
        DirectoryStructure.createStashConfigFileOnly(homeDirectoryDetails.getSharedHome());

        try {
            migration.perform();
        } finally {
            verifyNoMoreInteractions(homeFileMover);
        }
    }

    private static class DirectoryStructure {
        private final File configDir;
        private final File dataDir;
        private final File pluginsDir;
        private final File installedPluginsDir;
        private final File stashConfigFile;

        private DirectoryStructure(File parent) {
            this.configDir           = new File(parent, ApplicationSettings.CONFIG_DIR_NAME);
            this.dataDir             = new File(parent, ApplicationSettings.DATA_DIR_NAME);
            this.pluginsDir          = new File(parent, ApplicationSettings.PLUGINS_DIR_NAME);
            this.installedPluginsDir = new File(this.pluginsDir, ApplicationSettings.INSTALLED_PLUGINS_DIR_NAME);
            this.stashConfigFile     = new File(parent, ApplicationConstants.LEGACY_CONFIG_FILE);
        }

        private void cleanup() {
            if (installedPluginsDir.exists()) {
                assertTrue(installedPluginsDir.delete());
            }
            if (pluginsDir.exists()) {
                assertTrue(pluginsDir.delete());
            }
            if (dataDir.exists()) {
                assertTrue(dataDir.delete());
            }
            if (configDir.exists()) {
                assertTrue(configDir.delete());
            }
            if (stashConfigFile.exists()) {
                assertTrue(stashConfigFile.delete());
            }
        }

        private static DirectoryStructure create(File parent) throws IOException {
            DirectoryStructure directoryStructure = new DirectoryStructure(parent);
            directoryStructure.makeConfigDir();
            directoryStructure.makeDataDir();
            directoryStructure.makeInstalledPluginsDir();
            directoryStructure.makeStashConfigFile();
            return directoryStructure;
        }

        private static DirectoryStructure createConfigDirOnly(File parent) throws IOException {
            DirectoryStructure directoryStructure = new DirectoryStructure(parent);
            directoryStructure.makeConfigDir();
            return directoryStructure;
        }

        private static DirectoryStructure createDataDirOnly(File parent) throws IOException {
            DirectoryStructure directoryStructure = new DirectoryStructure(parent);
            directoryStructure.makeDataDir();
            return directoryStructure;
        }

        private static DirectoryStructure createInstalledPluginsDirOnly(File parent) throws IOException {
            DirectoryStructure directoryStructure = new DirectoryStructure(parent);
            directoryStructure.makeInstalledPluginsDir();
            return directoryStructure;
        }

        private static DirectoryStructure createStashConfigFileOnly(File parent) throws IOException {
            DirectoryStructure directoryStructure = new DirectoryStructure(parent);
            directoryStructure.makeStashConfigFile();
            return directoryStructure;
        }

        private void makeConfigDir() throws IOException {
            FileUtils.mkdir(configDir);
        }

        private void makeDataDir() throws IOException {
            FileUtils.mkdir(dataDir);
        }

        private void makeInstalledPluginsDir() throws IOException {
            FileUtils.mkdir(pluginsDir);
            FileUtils.mkdir(installedPluginsDir);
        }

        private void makeStashConfigFile() throws IOException {
            assertTrue(stashConfigFile.createNewFile());
        }
    }
}
