package com.atlassian.stash.internal.backup;

import com.atlassian.bitbucket.AbstractServiceTest;
import com.atlassian.bitbucket.validation.ArgumentValidationException;
import com.atlassian.bitbucket.NoSuchEntityException;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.i18n.SimpleI18nService;
import com.atlassian.stash.internal.ApplicationConstants;
import com.atlassian.stash.internal.ApplicationSettings;
import com.atlassian.stash.internal.backup.liquibase.LiquibaseDao;
import com.atlassian.stash.internal.maintenance.*;
import com.atlassian.stash.internal.maintenance.backup.BackupTask;
import com.atlassian.stash.internal.maintenance.backup.ExternalBackupTask;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageUtils;
import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import com.hazelcast.core.IExecutorService;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
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.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Set;

import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.same;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
@SuppressWarnings("ConstantConditions")
public class DefaultBackupServiceTest extends AbstractServiceTest {

    @Rule
    public TemporaryFolder folder = new TemporaryFolder();

    @Mock
    private IExecutorService clusterExecutorService;
    @Spy
    @SuppressWarnings("unused")
    private I18nService i18nService = new SimpleI18nService();
    @Mock
    private LiquibaseDao liquibaseDao;
    @Mock
    private MaintenanceService maintenanceService;
    @Mock
    private MaintenanceTaskFactory maintenanceTaskFactory;
    @InjectMocks
    private DefaultBackupService service;
    @Mock
    private ApplicationSettings settings;
    @Mock
    private MaintenanceTaskMonitor taskMonitor;

    public DefaultBackupServiceTest() {
        super(DefaultBackupService.class, BackupService.class);
    }

    @Before
    public void setup() {
        when(settings.getBackupDir()).thenReturn(folder.getRoot());
    }

    @Test
    public void testBackup() {
        BackupTask task = mock(BackupTask.class);
        when(maintenanceTaskFactory.backupTask()).thenReturn(task);
        when(maintenanceService.start(any(MaintenanceTask.class), any(MaintenanceType.class))).thenReturn(taskMonitor);

        service.backup();

        verify(maintenanceService).start(same(task), eq(MaintenanceType.BACKUP));
        verify(maintenanceTaskFactory).backupTask();

    }

    @Test
    public void testExternalBackup() {
        ExternalBackupTask task = mock(ExternalBackupTask.class);
        when(maintenanceTaskFactory.externalBackupTask()).thenReturn(task);
        when(maintenanceService.start(any(MaintenanceTask.class), any(MaintenanceType.class))).thenReturn(taskMonitor);

        service.externalBackup();

        verify(maintenanceService).start(same(task), eq(MaintenanceType.BACKUP));
        verify(maintenanceTaskFactory).externalBackupTask();

    }

    @Test(expected = BackupException.class)
    public void testBackupThrowsWhenMaintenanceInProgress() {
        BackupTask task = mock(BackupTask.class);
        when(maintenanceTaskFactory.backupTask()).thenReturn(task);

        doThrow(IllegalStateException.class).when(maintenanceService).start(same(task), eq(MaintenanceType.BACKUP));

        try {
            service.backup();
        } finally {
            verify(maintenanceService).start(same(task), eq(MaintenanceType.BACKUP));
            verify(maintenanceTaskFactory).backupTask();
        }
    }

    @Test(expected = BackupException.class)
    public void testExternalBackupThrowsWhenMaintenanceInProgress() {
        ExternalBackupTask task = mock(ExternalBackupTask.class);
        when(maintenanceTaskFactory.externalBackupTask()).thenReturn(task);

        doThrow(IllegalStateException.class).when(maintenanceService).start(same(task), eq(MaintenanceType.BACKUP));

        try {
            service.externalBackup();
        } finally {
            verify(maintenanceService).start(same(task), eq(MaintenanceType.BACKUP));
            verify(maintenanceTaskFactory).externalBackupTask();
        }
    }

    @Test
    public void testDelete() throws IOException {
        File file = new File(folder.getRoot(), "backup-admin-20130122-125455-781Z.zip");
        Files.touch(file);
        assertTrue(file.isFile());

        assertTrue(service.delete(mockBackup("backup-admin-20130122-125455-781Z.zip")));

        assertFalse(file.isFile());
        assertFalse(file.exists());
    }

    @Test
    public void testDeleteReturnsFalseWhenDirectory() {
        assertTrue(new File(folder.getRoot(), "backup-admin-20130122-125455-781Z.zip").mkdir());

        assertFalse(service.delete(mockBackup("backup-admin-20130122-125455-781Z.zip")));
    }

    @Test
    public void testDeleteReturnsFalseWhenNotFound() {
        assertFalse(service.delete(mockBackup("backup-admin-20130122-125455-781Z.zip")));
    }

    @Test(expected = ArgumentValidationException.class)
    public void testDeleteThrowsOnEmptyName() {
        service.delete(mockBackup(""));
    }

    @Test(expected = ArgumentValidationException.class)
    public void testDeleteThrowsOnMigrationZip() {
        service.delete(mockBackup("migration-admin-20121128-163943-117Z.zip"));
    }


    @Test(expected = NullPointerException.class)
    public void testDeleteThrowsOnNullBackup() {
        service.delete(null);
    }

    @Test(expected = NullPointerException.class)
    public void testDeleteThrowsOnNullName() {
        service.delete(mockBackup(null));
    }

    @Test(expected = ArgumentValidationException.class)
    public void testDeleteThrowsOnRelativePath() {
        service.delete(mockBackup("../" + ApplicationConstants.CONFIG_FILE));
    }

    @Test(expected = ArgumentValidationException.class)
    public void testDeleteThrowsOnWrongExtension() {
        service.delete(mockBackup("backup-admin-20130122-125455-781.dat"));
    }

    @Test
    public void testDeleteTrimsWhitespace() throws IOException {
        File file = new File(folder.getRoot(), "backup-admin-20130122-125455-781Z.zip");
        Files.touch(file);
        assertTrue(file.isFile());

        assertTrue(service.delete(mockBackup("    backup-admin-20130122-125455-781Z.zip\t\n   ")));

        assertFalse(file.isFile());
        assertFalse(file.exists());
    }

    @Test
    public void testFindAll() throws Exception {
        Files.touch(new File(folder.getRoot(), "backup-admin-20130121-223652-257Z.zip"));
        Files.touch(new File(folder.getRoot(), "backup-admin-20130121-223904-273Z.zip"));

        Files.touch(new File(folder.getRoot(), "backup-admin-20130121-225321-347Z.zip"));
        Files.touch(new File(folder.getRoot(), "backup-admin-20130129-172208-374Z.zip"));
        Files.touch(new File(folder.getRoot(), "backup-admin-20130122-125455-781Z.zip"));

        Page<Backup> page = service.findAll(PageUtils.newRequest(0, 3));
        assertNotNull(page);
        assertFalse(page.getIsLastPage());
        assertPage(page, "backup-admin-20130122-125455-781Z.zip", "backup-admin-20130129-172208-374Z.zip",
                "backup-admin-20130121-225321-347Z.zip");

        page = service.findAll(page.getNextPageRequest());
        assertNotNull(page);
        assertTrue(page.getIsLastPage());
        assertPage(page, "backup-admin-20130121-223904-273Z.zip", "backup-admin-20130121-223652-257Z.zip");
    }

    @Test
    public void testFindAllIgnoresNonBackupFiles() throws IOException {
        Files.touch(new File(folder.getRoot(), "backup-admin-20130121-223652-257Z.zip"));
        Files.touch(new File(folder.getRoot(), "backup-admin-20130121-225321-347Z.zip"));
        Files.touch(new File(folder.getRoot(), "backup-admin-20130129-172208-374Z.zip"));
        Files.touch(new File(folder.getRoot(), "backup-ad.min-20130129-172208-374Z.zip"));
        Files.touch(new File(folder.getRoot(), "migration-admin-20130110-103951-764Z.zip"));
        Files.touch(new File(folder.getRoot(), "migration-admin-20130110-130231-902Z.zip"));
        Files.touch(new File(folder.getRoot(), "backup-ad.min-20130129-172208-374Z.zip"));
        Files.touch(new File(folder.getRoot(), "backup-..-20130129-172208-374Z.zip"));

        Page<Backup> page = service.findAll(PageUtils.newRequest(0, 5));
        assertNotNull(page);
        assertTrue(page.getIsLastPage());
        assertPage(page, "backup-admin-20130121-223652-257Z.zip", "backup-admin-20130121-225321-347Z.zip",
                "backup-admin-20130129-172208-374Z.zip");
    }

    @Test
    public void testFindAllReturnsEmptyPageWhenNoBackupsExist() {
        Page<Backup> page = service.findAll(PageUtils.newRequest(0, 10));
        assertNotNull(page);
        assertEquals(0, page.getSize());
        assertTrue(page.getIsLastPage());

        Iterable<Backup> values = page.getValues();
        assertNotNull(values);
        assertTrue(Iterables.isEmpty(values));
    }

    @Test
    public void testFindAllReturnsEmptyPageWhenStartExceedsCount() {
        Page<Backup> page = service.findAll(PageUtils.newRequest(5, 10));
        assertNotNull(page);
        assertEquals(0, page.getSize());
        assertTrue(page.getIsLastPage());

        Iterable<Backup> values = page.getValues();
        assertNotNull(values);
        assertTrue(Iterables.isEmpty(values));
    }

    @Test(expected = NullPointerException.class)
    public void testFindAllThrowsOnNullPageRequest() {
        service.findAll(null);
    }

    @Test
    public void testFindByName() throws IOException {
        File file = new File(folder.getRoot(), "backup-admin-20130121-223652-257Z.zip");
        Files.write("Backup content", file, Charsets.UTF_8);

        Backup backup = service.findByName("backup-admin-20130121-223652-257Z.zip");
        assertNotNull(backup);
        assertEquals("backup-admin-20130121-223652-257Z.zip", backup.getName());
        assertEquals(file.lastModified(), backup.getModified());
        assertEquals(file.length(), backup.getSize());
    }

    @Test
    public void testFindByNameIgnoresDirectories() {
        assertTrue(new File(folder.getRoot(), "backup-admin-20130121-223652-257Z.zip").mkdir());

        assertNull(service.findByName("backup-admin-20130121-223652-257Z.zip"));
    }

    @Test(expected = ArgumentValidationException.class)
    public void testFileWithDotAreInvalid() {
        assertNull(service.findByName("backup-foo.bar-20130121-223652-257Z.zip"));
    }

    @Test(expected = ArgumentValidationException.class)
    public void testFileWithForwardSlashesAreInvalid() {
        assertNull(service.findByName("backup-foo/bar-20130121-223652-257Z.zip"));
    }

    @Test(expected = ArgumentValidationException.class)
    public void testFileWithBackSlashesAreInvalid() {
        assertNull(service.findByName("backup-foo\\bar-20130121-223652-257Z.zip"));
    }

    @Test(expected = ArgumentValidationException.class)
    public void testStashConfigProperties1Invalid() {
        assertNull(service.findByName("../" + ApplicationConstants.CONFIG_FILE));
    }

    @Test(expected = ArgumentValidationException.class)
    public void testStashConfigProperties2Invalid() {
        assertNull(service.findByName("..\\" + ApplicationConstants.CONFIG_FILE));
    }

    @Test
    public void testFindByNameReturnsNullOnNotFound() {
        assertNull(service.findByName("backup-admin-20130121-223652-257Z.zip"));
    }

    @Test
    public void testGetByName() throws Exception {
        File file = new File(folder.getRoot(), "backup-admin-20130121-223652-257Z.zip");
        Files.write("Backup content", file, Charsets.UTF_8);

        Backup backup = service.getByName("backup-admin-20130121-223652-257Z.zip");
        assertNotNull(backup);
        assertEquals("backup-admin-20130121-223652-257Z.zip", backup.getName());
        assertEquals(file.lastModified(), backup.getModified());
        assertEquals(file.length(), backup.getSize());
        try (InputStream backupStream = backup.getInput()) {
            assertEquals("Backup content", new String(ByteStreams.toByteArray(backupStream), Charsets.UTF_8));
        }
    }

    @Test(expected = NoSuchEntityException.class)
    public void testGetByNameThrowsOnNotFound() {
        service.getByName("backup-admin-20130121-223652-257Z.zip");
    }

    @Test
    public void testGetLatest() throws Exception {
        Files.touch(new File(folder.getRoot(), "backup-admin-20130121-225321-347Z.zip"));
        Files.touch(new File(folder.getRoot(), "backup-admin-20130121-223652-257Z.zip"));

        Backup backup = service.getLatest();
        assertNotNull(backup);
        assertEquals("backup-admin-20130121-225321-347Z.zip", backup.getName());
    }

    @Test(expected = NoSuchEntityException.class)
    public void testGetLatestThrowsWhenNoBackupsExist() {
        service.getLatest();
    }

    @Test
    public void testGetFeatures() {

        when(liquibaseDao.findCustomChanges()).thenReturn(ImmutableSet.<Class<?>>of(getClass()));

        List<BackupFeature> backupFeatures = service.getFeatures();

        assertThat(backupFeatures, containsFeatureLike("database", null, BackupFeatureMode.RESTORE));
        assertThat(backupFeatures, containsFeatureLike("core", "backup-support", BackupFeatureMode.BACKUP));
        assertThat(backupFeatures, containsFeatureLike("web", "json", BackupFeatureMode.BACKUP));
        assertThat(backupFeatures, containsFeatureLike("forks", "alternates", BackupFeatureMode.BOTH));
        assertThat(backupFeatures, containsFeatureLike("platform", "bitbucket.properties", BackupFeatureMode.BOTH));
        assertEquals("Incorrect amount of features returned", 5, backupFeatures.size());
    }

    private static Matcher<List<BackupFeature>> containsFeatureLike(final String group, final String name, final BackupFeatureMode mode) {
        return new BaseMatcher<List<BackupFeature>>() {
            @Override
            public boolean matches(Object item) {
                //noinspection unchecked
                List<BackupFeature> backupFeatures = (List<BackupFeature>) item;
                return Collections2.filter(backupFeatures, new Predicate<BackupFeature>() {
                    @Override
                    public boolean apply(BackupFeature input) {
                        return (group == null || input.getGroup().equals(group))
                                && (name == null || input.getName().equals(name))
                                && (mode == null || input.getMode() == mode);
                    }
                }).iterator().hasNext();
            }

            @Override
            public void describeTo(Description description) {
                description.appendText("lacks the feature " + group + ":" + name + ":" + mode);
            }
        };
    }

    private static void assertPage(Page<Backup> page, String... names) {
        //Note: For simplicity/speed, this is not testing ordering. testGetLatest will implicitly cover that. All that
        //      is tested here is that the right items, and only the right items, are returned.
        assertEquals(names.length, page.getSize());

        if (names.length > 0) {
            Set<String> expected = ImmutableSet.copyOf(names);
            Set<String> actual = ImmutableSet.copyOf(Iterables.transform(page.getValues(),
                    new Function<Backup, String>() {

                        @Override
                        public String apply(Backup backup) {
                            return backup.getName();
                        }
                    }));

            assertTrue(expected.containsAll(actual));
            assertTrue(actual.containsAll(expected));
        }
    }
    @Test(expected = ArgumentValidationException.class)
    public void testDeleteThrowsOnNestedPage() {
        service.delete(mockBackup("plugin/some-output.file"));
    }

    private static Backup mockBackup(String name) {
        Backup backup = mock(Backup.class);
        when(backup.getName()).thenReturn(name);

        return backup;
    }
}
