/*
 *
 *  Copyright 2013 Netflix, Inc.
 *
 *     Licensed under the Apache License, Version 2.0 (the "License");
 *     you may not use this file except in compliance with the License.
 *     You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 *     Unless required by applicable law or agreed to in writing, software
 *     distributed under the License is distributed on an "AS IS" BASIS,
 *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *     See the License for the specific language governing permissions and
 *     limitations under the License.
 *
 */
package com.netflix.nicobar.core.persistence;

import static com.netflix.nicobar.core.testutil.CoreTestResourceUtil.TestResource.TEST_MODULE_SPEC_JAR;
import static com.netflix.nicobar.core.testutil.CoreTestResourceUtil.TestResource.TEST_TEXT_JAR;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;

import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import org.apache.commons.io.FileUtils;
import org.mockito.Mockito;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import com.netflix.nicobar.core.archive.JarScriptArchive;
import com.netflix.nicobar.core.archive.ModuleId;
import com.netflix.nicobar.core.module.ScriptModule;
import com.netflix.nicobar.core.module.ScriptModuleListener;
import com.netflix.nicobar.core.module.ScriptModuleLoader;
import com.netflix.nicobar.core.plugin.TestCompilerPlugin;
import com.netflix.nicobar.core.plugin.ScriptCompilerPluginSpec;
import com.netflix.nicobar.core.testutil.CoreTestResourceUtil.TestResource;

/**
 * Base integration tests for {@link PathArchiveRepository}
 *
 * @author James Kojo
 * @author Vasanth Asokan
 */
public abstract class ArchiveRepositoryPollerTest {
    private final static Logger logger = LoggerFactory.getLogger(ScriptModuleLoader.class);
    protected ArchiveRepository archiveRepository;
    protected ScriptModuleLoader moduleLoader;

    /**
     * Create the repository to plug in to the integration gets
     * @return
     */
    public abstract ArchiveRepository createArchiveRepository(Path rootArchiveDirectory);

    @BeforeClass
    public void classSetup() throws Exception {
        Path rootArchiveDirectory = Files.createTempDirectory(ArchiveRepositoryPollerTest.class.getSimpleName()+"_");
        logger.info("rootArchiveDirectory: {}", rootArchiveDirectory);
        FileUtils.forceDeleteOnExit(rootArchiveDirectory.toFile());

        archiveRepository = createArchiveRepository(rootArchiveDirectory);
        long now = System.currentTimeMillis();
        deployJarArchive(TEST_TEXT_JAR, now);
        deployJarArchive(TEST_MODULE_SPEC_JAR, now);
    }

    @BeforeMethod
    public void testSetup() throws Exception {
        ScriptCompilerPluginSpec pluginSpec = new ScriptCompilerPluginSpec.Builder(TestCompilerPlugin.PLUGIN_ID)
            .withPluginClassName(TestCompilerPlugin.class.getName())
            .build();
        moduleLoader = new ScriptModuleLoader.Builder().addPluginSpec(pluginSpec).build();
    }

    /**
     * Simulate what a client might do at startup
     */
    @Test
    public void testInitialLoad() throws Exception {
        ArchiveRepositoryPoller poller = new ArchiveRepositoryPoller.Builder(moduleLoader).build();
        poller.addRepository(archiveRepository, 10, TimeUnit.SECONDS, true);
        Map<ModuleId, ScriptModule> scriptModules = moduleLoader.getAllScriptModules();

        assertEquals(scriptModules.keySet(), new HashSet<ModuleId>(Arrays.asList(TEST_TEXT_JAR.getModuleId(), TEST_MODULE_SPEC_JAR.getModuleId())));
        List<ArchiveSummary> archiveSummaries = archiveRepository.getDefaultView().getArchiveSummaries();
        for (ArchiveSummary archiveSummary : archiveSummaries) {
            ScriptModule scriptModule = moduleLoader.getScriptModule(archiveSummary.getModuleId());
            assertNotNull(scriptModule);
            assertEquals(scriptModule.getCreateTime(), archiveSummary.getLastUpdateTime());
        }
        poller.shutdown();
    }

    /**
     * Simulate what a client might do on an on-going bases
     */
    @Test
    public void testPolling() throws Exception {
        ScriptModuleListener mockListener = mock(ScriptModuleListener.class);
        moduleLoader.addListeners(Collections.singleton(mockListener));

        // initial startup phase
        ArchiveRepositoryPoller poller = new ArchiveRepositoryPoller.Builder(moduleLoader).build();
        poller.addRepository(archiveRepository, Integer.MAX_VALUE, TimeUnit.MILLISECONDS, true);
        Map<ModuleId, Long> origUpdateTimes = archiveRepository.getDefaultView().getArchiveUpdateTimes();
        verify(mockListener, times(2)).moduleUpdated(any(ScriptModule.class), eq((ScriptModule)null));
        verifyNoMoreInteractions(mockListener);

        // poll for changes
        poller.pollRepository(archiveRepository);
        verifyNoMoreInteractions(mockListener);

        // touch a file to force a reload then poll. some filesystems only have 1 second granularity, so advance by at least that much
        long updateTime = origUpdateTimes.get(TEST_MODULE_SPEC_JAR.getModuleId()) + 1000;
        deployJarArchive(TEST_MODULE_SPEC_JAR, updateTime);
        poller.pollRepository(archiveRepository);
        verify(mockListener).moduleUpdated(any(ScriptModule.class), (ScriptModule)Mockito.notNull());
        verifyNoMoreInteractions(mockListener);

        // poll one more time to make sure the state has been reset properly
        poller.pollRepository(archiveRepository);
        verifyNoMoreInteractions(mockListener);
     }

    /**
     * Simulate a deletion
     */
    @Test(priority=Integer.MAX_VALUE) // run this last
    public void testDelete() throws Exception {
        ScriptModuleListener mockListener = mock(ScriptModuleListener.class);
        moduleLoader.addListeners(Collections.singleton(mockListener));

        // initial startup phase
        ArchiveRepositoryPoller poller = new ArchiveRepositoryPoller.Builder(moduleLoader).build();
        poller.addRepository(archiveRepository, Integer.MAX_VALUE, TimeUnit.MILLISECONDS, true);
        verify(mockListener, times(2)).moduleUpdated(any(ScriptModule.class), eq((ScriptModule)null));
        verifyNoMoreInteractions(mockListener);

        // delete a module
        archiveRepository.deleteArchive(TEST_MODULE_SPEC_JAR.getModuleId());
        poller.pollRepository(archiveRepository);
        verify(mockListener).moduleUpdated(eq((ScriptModule)null), any(ScriptModule.class));
        verifyNoMoreInteractions(mockListener);


        // poll one more time to make sure the state has been reset properly
        poller.pollRepository(archiveRepository);
        verifyNoMoreInteractions(mockListener);

        // restore the module and reload
        reset(mockListener);
        deployJarArchive(TEST_MODULE_SPEC_JAR, System.currentTimeMillis());
        poller.pollRepository(archiveRepository);
        verify(mockListener).moduleUpdated(any(ScriptModule.class), eq((ScriptModule)null));
        verifyNoMoreInteractions(mockListener);
    }


    /**
     * inert the given archive resource to the test archive repository
     */
    protected void deployJarArchive(TestResource testResource, long updateTime) throws Exception {
        String testResourcePath = testResource.getResourcePath();
        URL archiveUrl = getClass().getClassLoader().getResource(testResourcePath);
        assertNotNull(archiveUrl, "couldn't find test resource with path " + testResourcePath);
        Path archiveJarPath = Paths.get(archiveUrl.toURI());
        JarScriptArchive jarScriptArchive = new JarScriptArchive.Builder(archiveJarPath)
            .setCreateTime(updateTime)
            .build();
        archiveRepository.insertArchive(jarScriptArchive);
    }
}