package de.renew.plugin.load;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Stream;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.aggregator.ArgumentsAccessor;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;
import org.junit.jupiter.params.provider.ArgumentsSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;

import de.renew.plugin.DependencyNotFulfilledException;
import de.renew.plugin.IPlugin;
import de.renew.plugin.PluginAdapter;
import de.renew.plugin.PluginClassLoader;
import de.renew.plugin.PluginManager;
import de.renew.plugin.PluginProperties;
import de.renew.plugin.jpms.impl.ModuleManager;
import de.renew.plugin.locate.PluginLocationFinders;

import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;

/**
 * Unit tests for {@link PluginLoaderComposition}.
 *
 * @author Kjell Ehlers
 */
@ExtendWith(MockitoExtension.class)
final class PluginLoaderCompositionTest {
    // Resources
    private static final String TEST_MODULAR_JAR_NAME = "testModule",
        TEST_NON_MODULAR_JAR_NAME = "testNoModule",
        TEST_MULTI_JAR_CFG_A_NAME = "a" + File.separator + "plugin",
        TEST_MULTI_JAR_CFG_B_NAME = "b" + File.separator + "plugin";

    @TempDir
    private static Path _tmp;

    /**
     * Setup two plugin directories: one with modular the other with non-modular jars.
     */
    @BeforeAll
    static void setUpAll() throws IOException {
        try (
            InputStream jarModule = PluginLoaderCompositionTest.class
                .getResourceAsStream("/" + TEST_MODULAR_JAR_NAME + ".jar");
            InputStream jarNoModule = PluginLoaderCompositionTest.class
                .getResourceAsStream("/" + TEST_NON_MODULAR_JAR_NAME + ".jar")) {
            createPluginDir(TEST_MULTI_JAR_CFG_A_NAME, jarModule);
            createPluginDir(TEST_MULTI_JAR_CFG_B_NAME, jarNoModule);
        }
    }

    @Mock(name = "moduleManager")
    private ModuleManager _moduleMgr; // Gets injected

    @InjectMocks
    private PluginLoaderComposition _systemUnderTest;

    @Nested
    @DisplayName("Given a single modular plugin")
    final class LoadPluginsTest {
        @Mock
        private PluginLoader _pluginLoader;

        @BeforeEach
        void setUp() {
            _systemUnderTest.addLoader(_pluginLoader);
        }

        @ParameterizedTest(name = "Then {1} should be loaded")
        @ArgumentsSource(SingleModularPluginProvider.class)
        @DisplayName("When the PMS is bootstrapping")
        void testLoadSingleModularPlugin(
            final ArgumentsAccessor args, final @Mock MockedStatic<PluginManager> mgr,
            final @Mock MockedStatic<PluginLocationFinders> plf)
            throws DependencyNotFulfilledException
        {
            //given
            final PluginProperties props = args.get(0, PluginProperties.class);
            final IPlugin plugin = args.get(1, IPlugin.class);

            mgr.when(PluginManager::getInstance).thenReturn(_mockMgr);
            plf.when(PluginLocationFinders::getInstance).thenReturn(_mockPlf);

            given(_mockPlf.findPluginLocations()).willReturn(Collections.singleton(props));
            given(_pluginLoader.loadPlugin(props)).willReturn(plugin);

            //when
            _systemUnderTest.loadPlugins();

            //then
            then(_mockMgr).should().addPlugin(plugin, true);
        }

        @ParameterizedTest(name = "Then {1} should be loaded")
        @ArgumentsSource(SingleModularPluginProvider.class)
        @DisplayName("When the PMS is issued this plugin's URL")
        void testLoadModularPluginFromURL(
            final ArgumentsAccessor args, final @Mock MockedStatic<PluginManager> mgr,
            final @Mock MockedStatic<PluginLocationFinders> plf)
            throws DependencyNotFulfilledException
        {
            //given
            final PluginProperties props = args.get(0, PluginProperties.class);
            final IPlugin plugin = args.get(1, IPlugin.class);
            final URL url = props.getURL();

            mgr.when(PluginManager::getInstance).thenReturn(_mockMgr);
            plf.when(PluginLocationFinders::getInstance).thenReturn(_mockPlf);

            given(_mockPlf.checkPluginLocation(url)).willReturn(props);
            given(_pluginLoader.loadPlugin(props)).willReturn(plugin);
            given(_mockMgr.checkDependenciesFulfilled(props)).willReturn(true);

            //when
            _systemUnderTest.loadPluginFromURL(url);

            //then
            then(_mockMgr).should().addPlugin(plugin, true);
        }
    }

    @Nested
    @DisplayName("Given a single non-modular plugin")
    final class LoadPluginFromURLTest {
        @Mock
        private PluginLoader _pluginLoader;

        @BeforeEach
        void setUp() {
            _systemUnderTest.addLoader(_pluginLoader);
        }

        @ParameterizedTest(name = "Then {1} should be loaded")
        @ArgumentsSource(SingleNonModularPluginProvider.class)
        @DisplayName("When the PMS is bootstrapping")
        void testLoadSingleNonModularPlugin(
            final ArgumentsAccessor args, final @Mock MockedStatic<PluginManager> mgr,
            final @Mock MockedStatic<PluginLocationFinders> plf)
            throws DependencyNotFulfilledException
        {
            //given
            final PluginProperties props = args.get(0, PluginProperties.class);
            final IPlugin plugin = args.get(1, IPlugin.class);

            mgr.when(PluginManager::getInstance).thenReturn(_mockMgr);
            plf.when(PluginLocationFinders::getInstance).thenReturn(_mockPlf);

            given(_mockPlf.findPluginLocations()).willReturn(Collections.singleton(props));
            given(_pluginLoader.loadPlugin(props)).willReturn(plugin);
            given(_mockMgr.getPluginClassLoader()).willReturn(_cl);

            //when
            _systemUnderTest.loadPlugins();

            //then
            then(_mockMgr).should().addPlugin(plugin, false);
        }

        @ParameterizedTest(name = "Then {1} should be loaded")
        @ArgumentsSource(SingleNonModularPluginProvider.class)
        @DisplayName("When the PMS is issued this plugin's URL")
        void testLoadNonModularPluginFromURL(
            final ArgumentsAccessor args, final @Mock MockedStatic<PluginManager> mgr,
            final @Mock MockedStatic<PluginLocationFinders> plf)
            throws DependencyNotFulfilledException
        {
            //given
            final PluginProperties props = args.get(0, PluginProperties.class);
            final IPlugin plugin = args.get(1, IPlugin.class);
            final URL url = props.getURL();

            mgr.when(PluginManager::getInstance).thenReturn(_mockMgr);
            plf.when(PluginLocationFinders::getInstance).thenReturn(_mockPlf);

            given(_mockPlf.checkPluginLocation(url)).willReturn(props);
            given(_pluginLoader.loadPlugin(props)).willReturn(plugin);
            given(_mockMgr.checkDependenciesFulfilled(props)).willReturn(true);
            given(_mockMgr.getPluginClassLoader()).willReturn(_cl);

            //when
            _systemUnderTest.loadPluginFromURL(url);

            //then
            then(_mockMgr).should().addPlugin(plugin, false);
        }
    }

    @Mock(name = "Mocked PluginManager")
    private PluginManager _mockMgr;

    @Mock(name = "Mocked PluginLocationFinders")
    private PluginLocationFinders _mockPlf;

    @Mock(name = "Mocked PluginClassLoader")
    private PluginClassLoader _cl;

    private static final class SingleModularPluginProvider implements ArgumentsProvider {
        private final URL _jarModule =
            PluginLoaderCompositionTest.class.getResource("/" + TEST_MODULAR_JAR_NAME + ".jar");
        private final URL _cfgMultiJarA =
            _tmp.resolve(TEST_MULTI_JAR_CFG_A_NAME + ".cfg").toUri().toURL();

        private SingleModularPluginProvider() throws MalformedURLException {}

        @Override
        public Stream<Arguments> provideArguments(ExtensionContext context) {
            return Stream.of(
                providePluginFor("Plugin 1", _jarModule, "my.service.a", true, false),
                providePluginFor("Plugin 2", _cfgMultiJarA, "my.service.b", true, true),
                // edge cases
                providePluginFor("Plugin 5", _jarModule, PluginProperties.NULL_VALUE, true, false),
                providePluginFor("Plugin 6", _jarModule, "", true, false),
                providePluginFor("Plugin 7", _jarModule, null, true, false),
                // plugin specific
                providePluginFor(
                    "Renew Splashscreen", _jarModule, "de.renew.splashscreen", true, false));
        }
    }

    private static final class SingleNonModularPluginProvider implements ArgumentsProvider {
        private final URL _jarNoModule =
            PluginLoaderCompositionTest.class.getResource("/" + TEST_NON_MODULAR_JAR_NAME + ".jar");
        private final URL _cfgMultiJarB =
            _tmp.resolve(TEST_MULTI_JAR_CFG_B_NAME + ".cfg").toUri().toURL();

        private SingleNonModularPluginProvider() throws MalformedURLException {}

        @Override
        public Stream<Arguments> provideArguments(ExtensionContext context) {
            return Stream.of(
                providePluginFor("Plugin 3", _jarNoModule, "my.service.c", false, false),
                providePluginFor("Plugin 4", _cfgMultiJarB, "my.service.d", false, true));
        }
    }

    private static Arguments providePluginFor(
        String name, URL loc, String prov, boolean isModular, boolean isMultiJar)
    {
        // Trivial plugin with no requirements
        return providePluginFor(
            name, loc, Collections.singleton(prov), Collections.emptySet(), isModular, isMultiJar);
    }

    private static Arguments providePluginFor(
        String name, URL loc, Set<String> prov, Set<String> req, boolean isModular,
        boolean isMultiJar)
    {
        PluginProperties props = mock(PluginProperties.class, "Properties of " + name);
        given(props.getProperty(PluginProperties.NAME)).willReturn(name); //Display in test overview
        given(props.getName()).willReturn(name);
        given(props.getURL()).willReturn(loc);
        given(props.getProvisions()).willReturn(prov);
        given(props.getRequirements()).willReturn(req);

        return Arguments.of(props, new PluginAdapter(props), isModular, isMultiJar);
    }

    private static void createPluginDir(String cfgName, InputStream jarFile) throws IOException {
        Path dest = _tmp.resolve(cfgName + ".cfg");

        Files.createDirectories(dest.getParent());
        Files.createFile(dest);
        Files.copy(jarFile, dest.resolveSibling("tmpJarA.jar"));
        Files.copy(dest.resolveSibling("tmpJarA.jar"), dest.resolveSibling("tmpJarB.jar"));
    }
}
