import pytest
from unittest.mock import call

from asynctest import TestCase as AsyncTestCase, mock as async_mock, call

from ...config.injection_context import InjectionContext
from ...utils.classloader import ClassLoader, ModuleLoadError

from ..plugin_registry import PluginRegistry
from ..protocol_registry import ProtocolRegistry

from ..error import ProtocolDefinitionValidationError


class TestPluginRegistry(AsyncTestCase):
    def setUp(self):
        self.registry = PluginRegistry()

        self.context = InjectionContext(enforce_typing=False)
        self.proto_registry = async_mock.MagicMock(
            register_message_types=async_mock.MagicMock(),
            register_controllers=async_mock.MagicMock(),
        )
        self.context.injector.bind_instance(ProtocolRegistry, self.proto_registry)

    async def test_setup(self):
        mod_name = "test_mod"
        mod = async_mock.MagicMock()
        mod.__name__ = mod_name
        ctx = async_mock.MagicMock()
        self.registry._plugins[mod_name] = mod
        assert list(self.registry.plugin_names) == [mod_name]
        assert list(self.registry.plugins) == [mod]
        mod.setup = async_mock.CoroutineMock()
        await self.registry.init_context(ctx)
        mod.setup.assert_awaited_once_with(ctx)

    async def test_register_routes(self):
        mod_name = "test_mod"
        mod = async_mock.MagicMock()
        mod.__name__ = mod_name
        app = async_mock.MagicMock()
        self.registry._plugins[mod_name] = mod
        mod.routes.register = async_mock.CoroutineMock()
        definition = async_mock.MagicMock()
        definition.versions = [
            {
                "major_version": 1,
                "minimum_minor_version": 0,
                "current_minor_version": 0,
                "path": "v1_0",
            }
        ]

        with async_mock.patch.object(
            ClassLoader,
            "load_module",
            async_mock.MagicMock(side_effect=[definition, mod.routes]),
        ) as load_module:
            await self.registry.register_admin_routes(app)

            calls = [
                call("definition", mod_name),
                call(f"{mod_name}.{definition.versions[0]['path']}.routes"),
            ]
            load_module.assert_has_calls(calls)
        assert mod.routes.register.call_count == 1

        with async_mock.patch.object(
            ClassLoader,
            "load_module",
            async_mock.MagicMock(side_effect=[definition, ModuleLoadError()]),
        ) as load_module:
            await self.registry.register_admin_routes(app)

            calls = [
                call("definition", mod_name),
                call(f"{mod_name}.{definition.versions[0]['path']}.routes"),
            ]
            load_module.assert_has_calls(calls)
        assert mod.routes.register.call_count == 1

    async def test_register_routes_mod_no_version(self):
        mod_name = "test_mod"
        mod = async_mock.MagicMock()
        mod.__name__ = mod_name
        app = async_mock.MagicMock()
        self.registry._plugins[mod_name] = mod
        mod.routes.register = async_mock.CoroutineMock()

        with async_mock.patch.object(
            ClassLoader,
            "load_module",
            async_mock.MagicMock(side_effect=[None, mod.routes]),
        ) as load_module:
            await self.registry.register_admin_routes(app)

            calls = [call("definition", mod_name), call(f"{mod_name}.routes")]
            load_module.assert_has_calls(calls)
        assert mod.routes.register.call_count == 1

        with async_mock.patch.object(
            ClassLoader,
            "load_module",
            async_mock.MagicMock(side_effect=[None, ModuleLoadError()]),
        ) as load_module:
            await self.registry.register_admin_routes(app)

            calls = [
                call("definition", mod_name),
                call(f"{mod_name}.routes"),
            ]
            load_module.assert_has_calls(calls)
        assert mod.routes.register.call_count == 1

    async def test_post_process_routes(self):
        mod_name = "test_mod"
        mod = async_mock.MagicMock()
        mod.__name__ = mod_name
        app = async_mock.MagicMock()
        self.registry._plugins[mod_name] = mod
        mod.routes.post_process_routes = async_mock.MagicMock()
        definition = async_mock.MagicMock()
        definition.versions = [
            {
                "major_version": 1,
                "minimum_minor_version": 0,
                "current_minor_version": 0,
                "path": "v1_0",
            }
        ]

        with async_mock.patch.object(
            ClassLoader,
            "load_module",
            async_mock.MagicMock(side_effect=[definition, mod.routes]),
        ) as load_module:
            self.registry.post_process_routes(app)

            calls = [
                call("definition", mod_name),
                call(f"{mod_name}.{definition.versions[0]['path']}.routes"),
            ]
            load_module.assert_has_calls(calls)
        assert mod.routes.post_process_routes.call_count == 1

        with async_mock.patch.object(
            ClassLoader,
            "load_module",
            async_mock.MagicMock(side_effect=[definition, ModuleLoadError()]),
        ) as load_module:
            self.registry.post_process_routes(app)

            calls = [
                call("definition", mod_name),
                call(f"{mod_name}.{definition.versions[0]['path']}.routes"),
            ]
            load_module.assert_has_calls(calls)
        assert mod.routes.post_process_routes.call_count == 1

    async def test_post_process_routes_mod_no_version(self):
        mod_name = "test_mod"
        mod = async_mock.MagicMock()
        mod.__name__ = mod_name
        app = async_mock.MagicMock()
        self.registry._plugins[mod_name] = mod
        mod.routes.register = async_mock.CoroutineMock()

        with async_mock.patch.object(
            ClassLoader,
            "load_module",
            async_mock.MagicMock(side_effect=[None, mod.routes]),
        ) as load_module:
            self.registry.post_process_routes(app)

            calls = [call("definition", mod_name), call(f"{mod_name}.routes")]
            load_module.assert_has_calls(calls)
        assert mod.routes.post_process_routes.call_count == 1

        with async_mock.patch.object(
            ClassLoader,
            "load_module",
            async_mock.MagicMock(side_effect=[None, ModuleLoadError()]),
        ) as load_module:
            self.registry.post_process_routes(app)

            calls = [call("definition", mod_name), call(f"{mod_name}.routes")]
            load_module.assert_has_calls(calls)
        assert mod.routes.post_process_routes.call_count == 1

    async def test_validate_version_not_a_list(self):
        mod_name = "test_mod"
        mod = async_mock.MagicMock()
        mod.__name__ = mod_name

        versions_not_a_list = {}

        with async_mock.patch.object(
            ClassLoader, "load_module", async_mock.MagicMock()
        ) as load_module:
            with pytest.raises(ProtocolDefinitionValidationError):
                self.registry.validate_version(versions_not_a_list, mod_name)

    async def test_validate_version_list_element_not_an_object(self):
        mod_name = "test_mod"
        mod = async_mock.MagicMock()
        mod.__name__ = mod_name

        versions = [{}, []]

        with async_mock.patch.object(
            ClassLoader, "load_module", async_mock.MagicMock()
        ) as load_module:
            with pytest.raises(ProtocolDefinitionValidationError):
                self.registry.validate_version(versions, mod_name)

    async def test_validate_version_list_element_empty(self):
        mod_name = "test_mod"
        mod = async_mock.MagicMock()
        mod.__name__ = mod_name

        versions = []

        with async_mock.patch.object(
            ClassLoader, "load_module", async_mock.MagicMock()
        ) as load_module:
            with pytest.raises(ProtocolDefinitionValidationError):
                self.registry.validate_version(versions, mod_name)

    async def test_validate_version_list_missing_attribute(self):
        mod_name = "test_mod"
        mod = async_mock.MagicMock()
        mod.__name__ = mod_name

        versions = [
            {
                "major_version": 1,
                "minimum_minor_version": 0,
                "current_minor_version": 0,
                # "path": "v1_0", # missing
            }
        ]

        with async_mock.patch.object(
            ClassLoader, "load_module", async_mock.MagicMock()
        ) as load_module:
            with pytest.raises(ProtocolDefinitionValidationError):
                self.registry.validate_version(versions, mod_name)

    async def test_validate_version_negative_version(self):
        mod_name = "test_mod"
        mod = async_mock.MagicMock()
        mod.__name__ = mod_name

        versions = [
            {
                "major_version": -1,
                "minimum_minor_version": 0,
                "current_minor_version": 0,
                "path": "v1_0",
            }
        ]

        with async_mock.patch.object(
            ClassLoader, "load_module", async_mock.MagicMock()
        ) as load_module:
            with pytest.raises(ProtocolDefinitionValidationError):
                self.registry.validate_version(versions, mod_name)

    async def test_validate_version_min_greater_current(self):
        mod_name = "test_mod"
        mod = async_mock.MagicMock()
        mod.__name__ = mod_name

        versions = [
            {
                "major_version": 1,
                "minimum_minor_version": 1,
                "current_minor_version": 0,
                "path": "v1_0",
            }
        ]

        with async_mock.patch.object(
            ClassLoader, "load_module", async_mock.MagicMock()
        ) as load_module:
            with pytest.raises(ProtocolDefinitionValidationError):
                self.registry.validate_version(versions, mod_name)

    async def test_validate_version_multiple_major(self):
        mod_name = "test_mod"
        mod = async_mock.MagicMock()
        mod.__name__ = mod_name

        versions = [
            {
                "major_version": 1,
                "minimum_minor_version": 0,
                "current_minor_version": 0,
                "path": "v1_0",
            },
            {
                "major_version": 1,
                "minimum_minor_version": 0,
                "current_minor_version": 1,
                "path": "v1_1",
            },
        ]

        with async_mock.patch.object(
            ClassLoader, "load_module", async_mock.MagicMock()
        ) as load_module:
            with pytest.raises(ProtocolDefinitionValidationError):
                self.registry.validate_version(versions, mod_name)

    async def test_validate_version_bad_path(self):
        mod_name = "test_mod"
        mod = async_mock.MagicMock()
        mod.__name__ = mod_name

        versions = [
            {
                "major_version": 1,
                "minimum_minor_version": 1,
                "current_minor_version": 0,
                "path": "v1_0",
            }
        ]

        with async_mock.patch.object(
            ClassLoader, "load_module", async_mock.MagicMock(return_value=None)
        ) as load_module:
            with pytest.raises(ProtocolDefinitionValidationError):
                self.registry.validate_version(versions, mod_name)

    async def test_validate_version_list_correct(self):
        mod_name = "test_mod"
        mod = async_mock.MagicMock()
        mod.__name__ = mod_name

        versions = [
            {
                "major_version": 1,
                "minimum_minor_version": 0,
                "current_minor_version": 0,
                "path": "v1_0",
            },
            {
                "major_version": 2,
                "minimum_minor_version": 0,
                "current_minor_version": 0,
                "path": "v2_0",
            },
        ]

        with async_mock.patch.object(
            ClassLoader, "load_module", async_mock.MagicMock()
        ) as load_module:
            assert self.registry.validate_version(versions, mod_name) is True

            load_module.has_calls(
                call(versions[0]["path"], mod_name),
                call(versions[1]["path"], mod_name),
            )

    async def test_validate_version_list_extra_attributes_ok(self):
        mod_name = "test_mod"
        mod = async_mock.MagicMock()
        mod.__name__ = mod_name

        versions = [
            {
                "major_version": 1,
                "minimum_minor_version": 0,
                "current_minor_version": 0,
                "path": "v1_0",
                "not": "an attribute",
            }
        ]

        with async_mock.patch.object(
            ClassLoader, "load_module", async_mock.MagicMock()
        ) as load_module:
            assert self.registry.validate_version(versions, mod_name) is True

    async def test_validate_version_no_such_mod(self):
        mod_name = "no_mod"
        mod = async_mock.MagicMock()
        mod.__name__ = mod_name

        versions = [
            {
                "major_version": 1,
                "minimum_minor_version": 0,
                "current_minor_version": 0,
                "path": "v1_0",
            }
        ]

        with async_mock.patch.object(
            ClassLoader, "load_module", async_mock.MagicMock()
        ) as load_module:
            load_module.return_value = None

            with self.assertRaises(ProtocolDefinitionValidationError):
                self.registry.validate_version(versions, mod_name)

    async def test_register_plugin_already_present(self):
        mod_name = "test_mod"
        mod = async_mock.MagicMock()
        self.registry._plugins[mod_name] = mod
        assert mod == self.registry.register_plugin(mod_name)

    async def test_register_plugin_load_x(self):
        with async_mock.patch.object(
            ClassLoader, "load_module", async_mock.MagicMock()
        ) as load_module:
            load_module.side_effect = ModuleLoadError("failure to load")
            assert self.registry.register_plugin("dummy") is None

    async def test_register_plugin_no_mod(self):
        with async_mock.patch.object(
            ClassLoader, "load_module", async_mock.MagicMock()
        ) as load_module:
            load_module.return_value = None
            assert self.registry.register_plugin("dummy") is None

    async def test_register_plugin_no_definition(self):
        with async_mock.patch.object(
            ClassLoader, "load_module", async_mock.MagicMock()
        ) as load_module:
            load_module.side_effect = [
                async_mock.MagicMock(),  # module
                None,  # routes
                None,  # message types
                None,  # definition
            ]
            assert self.registry.register_plugin("dummy") is None

    async def test_register_plugin_no_versions(self):
        with async_mock.patch.object(
            ClassLoader, "load_module", async_mock.MagicMock()
        ) as load_module:
            load_module.side_effect = [
                async_mock.MagicMock(),  # module
                None,  # routes
                None,  # message types
                "str-has-no-versions-attr",  # definition without versions attr
            ]
            assert self.registry.register_plugin("dummy") is None

    async def test_register_definitions_malformed(self):
        with async_mock.patch.object(
            ClassLoader, "load_module", async_mock.MagicMock()
        ) as load_module:
            load_module.side_effect = [
                async_mock.MagicMock(),  # module
                None,  # routes
                None,  # message types
                async_mock.MagicMock(versions="not-a-list"),
            ]
            assert self.registry.register_plugin("dummy") is None

    async def test_register_package_x(self):
        with async_mock.patch.object(
            ClassLoader, "scan_subpackages", async_mock.MagicMock()
        ) as load_module:
            load_module.side_effect = ModuleLoadError()
            assert not self.registry.register_package("dummy")

    async def test_load_protocols_load_x(self):
        mock_plugin = async_mock.MagicMock(__name__="dummy")
        with async_mock.patch.object(
            ClassLoader, "load_module", async_mock.MagicMock()
        ) as load_module:
            load_module.side_effect = ModuleLoadError()
            await self.registry.load_protocols(None, mock_plugin)
            assert load_module.call_count == 1

    async def test_load_protocols_load_mod(self):
        mock_plugin = async_mock.MagicMock(__name__="dummy")
        mock_mod = async_mock.MagicMock()
        mock_mod.MESSAGE_TYPES = async_mock.MagicMock()
        mock_mod.CONTROLLERS = async_mock.MagicMock()

        with async_mock.patch.object(
            ClassLoader, "load_module", async_mock.MagicMock()
        ) as load_module:
            load_module.return_value = mock_mod
            await self.registry.load_protocols(self.context, mock_plugin)

    async def test_load_protocols_no_mod_load_x(self):
        mock_plugin = async_mock.MagicMock(__name__="dummy")

        with async_mock.patch.object(
            ClassLoader, "load_module", async_mock.MagicMock()
        ) as load_module:
            load_module.side_effect = [None, ModuleLoadError()]
            await self.registry.load_protocols(self.context, mock_plugin)
            assert load_module.call_count == 2

    async def test_load_protocols_no_mod_def_no_message_types(self):
        mock_plugin = async_mock.MagicMock(__name__="dummy")
        mock_def = async_mock.MagicMock(
            versions=[
                {
                    "major_version": 1,
                    "minimum_minor_version": 0,
                    "current_minor_version": 0,
                    "path": "v1_0",
                }
            ]
        )

        with async_mock.patch.object(
            ClassLoader, "load_module", async_mock.MagicMock()
        ) as load_module:
            load_module.side_effect = [None, mock_def, ModuleLoadError()]
            await self.registry.load_protocols(self.context, mock_plugin)
            assert load_module.call_count == 3

    async def test_load_protocols_no_mod_def_message_types(self):
        mock_plugin = async_mock.MagicMock(__name__="dummy")
        mock_def = async_mock.MagicMock(
            versions=[
                {
                    "major_version": 1,
                    "minimum_minor_version": 0,
                    "current_minor_version": 0,
                    "path": "v1_0",
                },
                {
                    "major_version": 2,
                    "minimum_minor_version": 0,
                    "current_minor_version": 0,
                    "path": "v2_0",
                },
            ]
        )
        mock_mod = async_mock.MagicMock()
        mock_mod.MESSAGE_TYPES = async_mock.MagicMock()
        mock_mod.CONTROLLERS = async_mock.MagicMock()

        with async_mock.patch.object(
            ClassLoader, "load_module", async_mock.MagicMock()
        ) as load_module:
            load_module.side_effect = [None, mock_def, mock_mod, mock_mod]
            await self.registry.load_protocols(self.context, mock_plugin)
            assert load_module.call_count == 4

    def test_repr(self):
        assert type(repr(self.registry)) is str
