import os
from contextlib import contextmanager

from pelican.plugins._utils import (
    get_namespace_plugins,
    get_plugin_name,
    load_plugins,
    plugin_enabled,
)
from pelican.plugins.signals import signal
from pelican.tests.dummy_plugins.normal_plugin import normal_plugin
from pelican.tests.support import unittest


@contextmanager
def tmp_namespace_path(path):
    """Context manager for temporarily appending namespace plugin packages

    path: path containing the `pelican` folder

    This modifies the `pelican.__path__` and lets the `pelican.plugins`
    namespace package resolve it from that.
    """
    # This avoids calls to internal `pelican.plugins.__path__._recalculate()`
    # as it should not be necessary
    import pelican  # noqa: PLC0415

    old_path = pelican.__path__[:]
    try:
        pelican.__path__.append(os.path.join(path, "pelican"))
        yield
    finally:
        pelican.__path__ = old_path


class PluginTest(unittest.TestCase):
    _PLUGIN_FOLDER = os.path.join(
        os.path.abspath(os.path.dirname(__file__)), "dummy_plugins"
    )
    _NS_PLUGIN_FOLDER = os.path.join(_PLUGIN_FOLDER, "namespace_plugin")
    _NORMAL_PLUGIN_FOLDER = os.path.join(_PLUGIN_FOLDER, "normal_plugin")

    def test_namespace_path_modification(self):
        import pelican  # noqa: PLC0415
        import pelican.plugins  # noqa: PLC0415

        old_path = pelican.__path__[:]

        # not existing path
        path = os.path.join(self._PLUGIN_FOLDER, "foo")
        with tmp_namespace_path(path):
            self.assertIn(os.path.join(path, "pelican"), pelican.__path__)
            # foo/pelican does not exist, so it won't propagate
            self.assertNotIn(
                os.path.join(path, "pelican", "plugins"), pelican.plugins.__path__
            )
        # verify that we restored path back
        self.assertEqual(pelican.__path__, old_path)

        # existing path
        with tmp_namespace_path(self._NS_PLUGIN_FOLDER):
            self.assertIn(
                os.path.join(self._NS_PLUGIN_FOLDER, "pelican"), pelican.__path__
            )
            # /namespace_plugin/pelican exists, so it should be in
            self.assertIn(
                os.path.join(self._NS_PLUGIN_FOLDER, "pelican", "plugins"),
                pelican.plugins.__path__,
            )
        self.assertEqual(pelican.__path__, old_path)

    def test_get_namespace_plugins(self):
        # existing namespace plugins
        existing_ns_plugins = get_namespace_plugins()

        # with plugin
        with tmp_namespace_path(self._NS_PLUGIN_FOLDER):
            ns_plugins = get_namespace_plugins()
            self.assertEqual(len(ns_plugins), len(existing_ns_plugins) + 1)
            self.assertIn("pelican.plugins.ns_plugin", ns_plugins)
            self.assertEqual(
                ns_plugins["pelican.plugins.ns_plugin"].NAME, "namespace plugin"
            )

        # should be back to existing namespace plugins outside `with`
        ns_plugins = get_namespace_plugins()
        self.assertEqual(ns_plugins, existing_ns_plugins)

    def test_load_plugins(self):
        def get_plugin_names(plugins):
            return {get_plugin_name(p) for p in plugins}

        # existing namespace plugins
        existing_ns_plugins = load_plugins({})

        with tmp_namespace_path(self._NS_PLUGIN_FOLDER):
            # with no `PLUGINS` setting, load namespace plugins
            plugins = load_plugins({})
            self.assertEqual(len(plugins), len(existing_ns_plugins) + 1, plugins)
            self.assertEqual(
                {"pelican.plugins.ns_plugin"} | get_plugin_names(existing_ns_plugins),
                get_plugin_names(plugins),
            )

            # disable namespace plugins with `PLUGINS = []`
            SETTINGS = {"PLUGINS": []}
            plugins = load_plugins(SETTINGS)
            self.assertEqual(len(plugins), 0, plugins)

            # with `PLUGINS`, load only specified plugins

            # normal plugin
            SETTINGS = {
                "PLUGINS": ["normal_plugin"],
                "PLUGIN_PATHS": [self._NORMAL_PLUGIN_FOLDER],
            }
            plugins = load_plugins(SETTINGS)
            self.assertEqual(len(plugins), 1, plugins)
            self.assertEqual({"normal_plugin"}, get_plugin_names(plugins))

            # normal submodule/subpackage plugins
            SETTINGS = {
                "PLUGINS": [
                    "normal_submodule_plugin.subplugin",
                    "normal_submodule_plugin.subpackage.subpackage",
                ],
                "PLUGIN_PATHS": [self._NORMAL_PLUGIN_FOLDER],
            }
            plugins = load_plugins(SETTINGS)
            self.assertEqual(len(plugins), 2, plugins)
            self.assertEqual(
                {
                    "normal_submodule_plugin.subplugin",
                    "normal_submodule_plugin.subpackage.subpackage",
                },
                get_plugin_names(plugins),
            )

            # ensure normal plugins are loaded only once
            SETTINGS = {
                "PLUGINS": ["normal_plugin"],
                "PLUGIN_PATHS": [self._NORMAL_PLUGIN_FOLDER],
            }
            plugins = load_plugins(SETTINGS)
            for plugin in load_plugins(SETTINGS):
                # The second load_plugins() should return the same plugin
                # objects as the first one
                self.assertIn(plugin, plugins)

            # namespace plugin short
            SETTINGS = {"PLUGINS": ["ns_plugin"]}
            plugins = load_plugins(SETTINGS)
            self.assertEqual(len(plugins), 1, plugins)
            self.assertEqual({"pelican.plugins.ns_plugin"}, get_plugin_names(plugins))

            # namespace plugin long
            SETTINGS = {"PLUGINS": ["pelican.plugins.ns_plugin"]}
            plugins = load_plugins(SETTINGS)
            self.assertEqual(len(plugins), 1, plugins)
            self.assertEqual({"pelican.plugins.ns_plugin"}, get_plugin_names(plugins))

            # normal and namespace plugin
            SETTINGS = {
                "PLUGINS": ["normal_plugin", "ns_plugin"],
                "PLUGIN_PATHS": [self._NORMAL_PLUGIN_FOLDER],
            }
            plugins = load_plugins(SETTINGS)
            self.assertEqual(len(plugins), 2, plugins)
            self.assertEqual(
                {"normal_plugin", "pelican.plugins.ns_plugin"},
                get_plugin_names(plugins),
            )

    def test_get_plugin_name(self):
        self.assertEqual(
            get_plugin_name(normal_plugin),
            "pelican.tests.dummy_plugins.normal_plugin.normal_plugin",
        )

        class NoopPlugin:
            def register(self):
                pass

        self.assertEqual(
            get_plugin_name(NoopPlugin),
            "PluginTest.test_get_plugin_name.<locals>.NoopPlugin",
        )
        self.assertEqual(
            get_plugin_name(NoopPlugin()),
            "PluginTest.test_get_plugin_name.<locals>.NoopPlugin",
        )

    def test_plugin_enabled(self):
        def get_plugin_names(plugins):
            return [get_plugin_name(p) for p in plugins]

        with tmp_namespace_path(self._NS_PLUGIN_FOLDER):
            # with no `PLUGINS` setting, load namespace plugins
            SETTINGS = {}
            plugins = get_plugin_names(load_plugins(SETTINGS))
            self.assertTrue(plugin_enabled("ns_plugin", plugins))
            self.assertTrue(plugin_enabled("pelican.plugins.ns_plugin", plugins))
            self.assertFalse(plugin_enabled("normal_plugin", plugins))
            self.assertFalse(plugin_enabled("unknown", plugins))

            # disable namespace plugins with `PLUGINS = []`
            SETTINGS = {"PLUGINS": []}
            plugins = get_plugin_names(load_plugins(SETTINGS))
            self.assertFalse(plugin_enabled("ns_plugin", plugins))
            self.assertFalse(plugin_enabled("pelican.plugins.ns_plugin", plugins))
            self.assertFalse(plugin_enabled("normal_plugin", plugins))
            self.assertFalse(plugin_enabled("unknown", plugins))

            # with `PLUGINS`, load only specified plugins

            # normal plugin
            SETTINGS = {
                "PLUGINS": ["normal_plugin"],
                "PLUGIN_PATHS": [self._NORMAL_PLUGIN_FOLDER],
            }
            plugins = get_plugin_names(load_plugins(SETTINGS))
            self.assertFalse(plugin_enabled("ns_plugin", plugins))
            self.assertFalse(plugin_enabled("pelican.plugins.ns_plugin", plugins))
            self.assertTrue(plugin_enabled("normal_plugin", plugins))
            self.assertFalse(plugin_enabled("unknown", plugins))

            # normal submodule/subpackage plugins
            SETTINGS = {
                "PLUGINS": [
                    "normal_submodule_plugin.subplugin",
                    "normal_submodule_plugin.subpackage.subpackage",
                ],
                "PLUGIN_PATHS": [self._NORMAL_PLUGIN_FOLDER],
            }
            plugins = get_plugin_names(load_plugins(SETTINGS))
            self.assertFalse(plugin_enabled("ns_plugin", plugins))
            self.assertFalse(plugin_enabled("pelican.plugins.ns_plugin", plugins))
            self.assertFalse(plugin_enabled("normal_plugin", plugins))
            self.assertFalse(plugin_enabled("unknown", plugins))

            # namespace plugin short
            SETTINGS = {"PLUGINS": ["ns_plugin"]}
            plugins = get_plugin_names(load_plugins(SETTINGS))
            self.assertTrue(plugin_enabled("ns_plugin", plugins))
            self.assertTrue(plugin_enabled("pelican.plugins.ns_plugin", plugins))
            self.assertFalse(plugin_enabled("normal_plugin", plugins))
            self.assertFalse(plugin_enabled("unknown", plugins))

            # namespace plugin long
            SETTINGS = {"PLUGINS": ["pelican.plugins.ns_plugin"]}
            plugins = get_plugin_names(load_plugins(SETTINGS))
            self.assertTrue(plugin_enabled("ns_plugin", plugins))
            self.assertTrue(plugin_enabled("pelican.plugins.ns_plugin", plugins))
            self.assertFalse(plugin_enabled("normal_plugin", plugins))
            self.assertFalse(plugin_enabled("unknown", plugins))

            # normal and namespace plugin
            SETTINGS = {
                "PLUGINS": ["normal_plugin", "ns_plugin"],
                "PLUGIN_PATHS": [self._NORMAL_PLUGIN_FOLDER],
            }
            plugins = get_plugin_names(load_plugins(SETTINGS))
            self.assertTrue(plugin_enabled("ns_plugin", plugins))
            self.assertTrue(plugin_enabled("pelican.plugins.ns_plugin", plugins))
            self.assertTrue(plugin_enabled("normal_plugin", plugins))
            self.assertFalse(plugin_enabled("unknown", plugins))

    def test_blinker_is_ordered(self):
        """ensure that call order is connetion order"""
        dummy_signal = signal("dummpy_signal")

        functions = []
        expected = []
        for i in range(50):
            # function appends value of i to a list
            def func(dummy_input, i=i):
                dummy_input.append(i)

            functions.append(func)
            # we expect functions to be run in the connection order
            dummy_signal.connect(func)
            expected.append(i)

        dummy_input = []
        dummy_signal.send(dummy_input)
        self.assertEqual(dummy_input, expected)
