#  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.

from typing import Any
from unittest.mock import Mock
from unittest.mock import sentinel

from stevedore import dispatch
from stevedore import driver
from stevedore import enabled
from stevedore import extension
from stevedore import hook
from stevedore import named
from stevedore.tests import utils


test_extension = extension.Extension(
    'test_extension',
    None,  # type: ignore
    None,  # type: ignore
    None,
)
test_extension2 = extension.Extension(
    'another_one',
    None,  # type: ignore
    None,  # type: ignore
    None,
)

mock_entry_point = Mock(module_name='test.extension', attrs=['obj'])
a_driver = extension.Extension(
    'test_driver',
    mock_entry_point,
    sentinel.driver_plugin,
    sentinel.driver_obj,
)


# base ExtensionManager
class TestTestManager(utils.TestCase):
    def test_instance_should_use_supplied_extensions(self):
        extensions = [test_extension, test_extension2]
        em = extension.ExtensionManager.make_test_instance(extensions)
        self.assertEqual(extensions, em.extensions)

    def test_instance_should_have_default_namespace(self):
        em: extension.ExtensionManager[Any]
        em = extension.ExtensionManager.make_test_instance([])
        self.assertEqual(em.namespace, 'TESTING')

    def test_instance_should_use_supplied_namespace(self):
        namespace = 'testing.1.2.3'
        em: extension.ExtensionManager[Any]
        em = extension.ExtensionManager.make_test_instance(
            [], namespace=namespace
        )
        self.assertEqual(namespace, em.namespace)

    def test_extension_name_should_be_listed(self):
        em = extension.ExtensionManager.make_test_instance([test_extension])
        self.assertIn(test_extension.name, em.names())

    def test_iterator_should_yield_extension(self):
        em = extension.ExtensionManager.make_test_instance([test_extension])
        self.assertEqual(test_extension, next(iter(em)))

    def test_manager_should_allow_name_access(self):
        em = extension.ExtensionManager.make_test_instance([test_extension])
        self.assertEqual(test_extension, em[test_extension.name])

    def test_manager_should_call(self):
        em = extension.ExtensionManager.make_test_instance([test_extension])
        func = Mock()
        em.map(func)
        func.assert_called_once_with(test_extension)

    def test_manager_should_call_all(self):
        em = extension.ExtensionManager.make_test_instance(
            [test_extension2, test_extension]
        )
        func = Mock()
        em.map(func)
        func.assert_any_call(test_extension2)
        func.assert_any_call(test_extension)

    def test_manager_return_values(self):
        def mapped(ext, /, *args, **kwds):
            return ext.name

        em = extension.ExtensionManager.make_test_instance(
            [test_extension2, test_extension]
        )
        results = em.map(mapped)
        self.assertEqual(sorted(results), ['another_one', 'test_extension'])

    def test_manager_should_eat_exceptions(self):
        em = extension.ExtensionManager.make_test_instance([test_extension])

        func = Mock(side_effect=RuntimeError('hard coded error'))

        results = em.map(func, 1, 2, a='A', b='B')
        self.assertEqual(results, [])

    def test_manager_should_propagate_exceptions(self):
        em = extension.ExtensionManager.make_test_instance(
            [test_extension], propagate_map_exceptions=True
        )
        func = Mock(side_effect=RuntimeError('hard coded error'))
        self.assertRaises(RuntimeError, em.map, func, 1, 2, a='A', b='B')

    # NamedExtensionManager
    def test_named_manager_should_use_supplied_extensions(self):
        extensions = [test_extension, test_extension2]
        em = named.NamedExtensionManager.make_test_instance(extensions)
        self.assertEqual(extensions, em.extensions)

    def test_named_manager_should_have_default_namespace(self):
        em: named.NamedExtensionManager[Any]
        em = named.NamedExtensionManager.make_test_instance([])
        self.assertEqual(em.namespace, 'TESTING')

    def test_named_manager_should_use_supplied_namespace(self):
        namespace = 'testing.1.2.3'
        em: named.NamedExtensionManager[Any]
        em = named.NamedExtensionManager.make_test_instance(
            [], namespace=namespace
        )
        self.assertEqual(namespace, em.namespace)

    def test_named_manager_should_populate_names(self):
        extensions = [test_extension, test_extension2]
        em = named.NamedExtensionManager.make_test_instance(extensions)
        self.assertEqual(em.names(), ['test_extension', 'another_one'])

    # HookManager
    def test_hook_manager_should_use_supplied_extensions(self):
        extensions = [test_extension, test_extension2]
        em = hook.HookManager.make_test_instance(extensions)
        self.assertEqual(extensions, em.extensions)

    def test_hook_manager_should_be_first_extension_name(self):
        extensions = [test_extension, test_extension2]
        em = hook.HookManager.make_test_instance(extensions)
        # This will raise KeyError if the names don't match
        assert em[test_extension.name]

    def test_hook_manager_should_have_default_namespace(self):
        em = hook.HookManager.make_test_instance([test_extension])
        self.assertEqual(em.namespace, 'TESTING')

    def test_hook_manager_should_use_supplied_namespace(self):
        namespace = 'testing.1.2.3'
        em = hook.HookManager.make_test_instance(
            [test_extension], namespace=namespace
        )
        self.assertEqual(namespace, em.namespace)

    def test_hook_manager_should_return_named_extensions(self):
        hook1 = extension.Extension(
            'captain',
            None,  # type: ignore
            None,  # type: ignore
            None,
        )
        hook2 = extension.Extension(
            'captain',
            None,  # type: ignore
            None,  # type: ignore
            None,
        )
        em = hook.HookManager.make_test_instance([hook1, hook2])
        self.assertEqual([hook1, hook2], em['captain'])

    # DriverManager
    def test_driver_manager_should_use_supplied_extension(self):
        em = driver.DriverManager.make_test_instance(a_driver)
        self.assertEqual([a_driver], em.extensions)

    def test_driver_manager_should_have_default_namespace(self):
        em = driver.DriverManager.make_test_instance(a_driver)
        self.assertEqual(em.namespace, 'TESTING')

    def test_driver_manager_should_use_supplied_namespace(self):
        namespace = 'testing.1.2.3'
        em = driver.DriverManager.make_test_instance(
            a_driver, namespace=namespace
        )
        self.assertEqual(namespace, em.namespace)

    def test_instance_should_use_driver_name(self):
        em = driver.DriverManager.make_test_instance(a_driver)
        self.assertEqual(['test_driver'], em.names())

    def test_instance_call(self):
        def invoke(ext, /, *args, **kwds):
            return ext.name, args, kwds

        em = driver.DriverManager.make_test_instance(a_driver)
        result = em(invoke, 'a', b='C')
        self.assertEqual(result, ('test_driver', ('a',), {'b': 'C'}))

    def test_instance_driver_property(self):
        em = driver.DriverManager.make_test_instance(a_driver)
        self.assertEqual(sentinel.driver_obj, em.driver)

    # EnabledExtensionManager
    def test_enabled_instance_should_use_supplied_extensions(self):
        extensions = [test_extension, test_extension2]
        em = enabled.EnabledExtensionManager.make_test_instance(extensions)
        self.assertEqual(extensions, em.extensions)

    # DispatchExtensionManager
    def test_dispatch_instance_should_use_supplied_extensions(self):
        extensions = [test_extension, test_extension2]
        em = dispatch.DispatchExtensionManager.make_test_instance(extensions)
        self.assertEqual(extensions, em.extensions)

    def test_dispatch_map_should_invoke_filter_for_extensions(self):
        em = dispatch.DispatchExtensionManager.make_test_instance(
            [test_extension, test_extension2]
        )
        filter_func = Mock(return_value=False)
        args = ('A',)
        kw = {'big': 'Cheese'}
        em.map(filter_func, None, *args, **kw)  # type: ignore
        filter_func.assert_any_call(test_extension, *args, **kw)
        filter_func.assert_any_call(test_extension2, *args, **kw)

    # NameDispatchExtensionManager
    def test_name_dispatch_instance_should_use_supplied_extensions(self):
        extensions = [test_extension, test_extension2]
        em = dispatch.NameDispatchExtensionManager.make_test_instance(
            extensions
        )

        self.assertEqual(extensions, em.extensions)

    def test_name_dispatch_instance_should_build_extension_name_map(self):
        extensions = [test_extension, test_extension2]
        em = dispatch.NameDispatchExtensionManager.make_test_instance(
            extensions
        )
        self.assertEqual(test_extension, em.by_name[test_extension.name])
        self.assertEqual(test_extension2, em.by_name[test_extension2.name])

    def test_named_dispatch_map_should_invoke_filter_for_extensions(self):
        em = dispatch.NameDispatchExtensionManager.make_test_instance(
            [test_extension, test_extension2]
        )
        func = Mock()
        args = ('A',)
        kw = {'BIGGER': 'Cheese'}
        em.map(['test_extension'], func, *args, **kw)
        func.assert_called_once_with(test_extension, *args, **kw)
