from enum import Enum
from unittest import TestCase
from unittest.mock import Mock, call

from samtranslator.plugins import BasePlugin, LifeCycleEvents
from samtranslator.plugins.sam_plugins import SamPlugins


class TestSamPluginsRegistration(TestCase):
    def setUp(self):
        # Setup the plugin to be a "subclass" of the BasePlugin
        self.mock_plugin_name = "mock_plugin"
        self.mock_plugin = Mock(spec=BasePlugin)
        self.mock_plugin.name = self.mock_plugin_name

        self.sam_plugins = SamPlugins()

    def test_register_must_work(self):
        self.sam_plugins.register(self.mock_plugin)

        self.assertEqual(self.mock_plugin, self.sam_plugins._get(self.mock_plugin_name))

    def test_register_must_raise_on_duplicate_plugin(self):
        self.sam_plugins.register(self.mock_plugin)

        with self.assertRaises(ValueError):
            self.sam_plugins.register(self.mock_plugin)

    def test_register_must_raise_on_invalid_plugin_type(self):
        # Plugin which is not an instance of BaseClass
        bad_plugin = Mock()
        bad_plugin.name = "some name"

        with self.assertRaises(ValueError):
            self.sam_plugins.register(bad_plugin)

    def test_register_must_append_plugins_to_end(self):
        plugin1 = _make_mock_plugin("plugin1")
        plugin2 = _make_mock_plugin("plugin2")
        plugin3 = _make_mock_plugin("plugin3")

        self.sam_plugins.register(plugin1)
        self.sam_plugins.register(plugin2)
        self.sam_plugins.register(plugin3)

        expected = [plugin1, plugin2, plugin3]

        self.assertEqual(expected, self.sam_plugins._plugins)

    def test_must_register_plugins_list_on_initialization(self):
        plugin1 = _make_mock_plugin("plugin1")
        plugin2 = _make_mock_plugin("plugin2")

        plugins_list = [plugin1, plugin2]

        new_sam_plugin = SamPlugins(plugins_list)

        # Also make sure the plugins are in the same order as in input
        self.assertEqual(plugins_list, new_sam_plugin._plugins)

    def test_must_register_single_plugin_on_initialization(self):
        new_sam_plugin = SamPlugins(self.mock_plugin)

        self.assertTrue(new_sam_plugin.is_registered(self.mock_plugin_name))

    def test_len_must_get_num_registered_plugins(self):
        plugin1 = _make_mock_plugin("plugin1")
        plugin2 = _make_mock_plugin("plugin2")

        self.sam_plugins.register(plugin1)
        self.sam_plugins.register(plugin2)

        self.assertEqual(2, len(self.sam_plugins))

    def test_is_registered_must_find_registered_plugins(self):
        self.sam_plugins.register(self.mock_plugin)

        self.assertTrue(self.sam_plugins.is_registered(self.mock_plugin_name))

    def test_is_registered_must_return_false_when_no_plugins_registered(self):
        # No Plugins are registered
        self.assertFalse(self.sam_plugins.is_registered(self.mock_plugin_name))

    def test_is_registered_must_return_false_for_non_existent_plugins(self):
        # Register a plugin but check with some random name
        self.sam_plugins.register(self.mock_plugin)
        self.assertFalse(self.sam_plugins.is_registered("some plugin name"))

    def test_get_must_return_a_registered_plugin(self):
        plugin1 = _make_mock_plugin("plugin1")
        plugin2 = _make_mock_plugin("plugin2")

        self.sam_plugins.register(plugin1)
        self.sam_plugins.register(plugin2)

        self.assertEqual(plugin1, self.sam_plugins._get(plugin1.name))
        self.assertEqual(plugin2, self.sam_plugins._get(plugin2.name))

    def test_get_must_handle_no_registered_plugins(self):
        # NO plugins registered
        self.assertIsNone(self.sam_plugins._get("some plugin"))

    def test_get_must_handle_non_registered_plugins(self):
        # Register one plugin, but try to retrieve some other non-existent plugin
        self.sam_plugins.register(self.mock_plugin)
        self.assertIsNone(self.sam_plugins._get("some plugin"))


class TestSamPluginsAct(TestCase):
    def setUp(self):
        self.sam_plugins = SamPlugins()

        # Create a mock of LifeCycleEvents object with ONE event called "my_event"
        self.mock_lifecycle_events = Mock(spec=LifeCycleEvents)
        self.my_event = Mock(spec=LifeCycleEvents)
        self.my_event.name = "my_event"
        self.my_event.value = "my_event"
        self.mock_lifecycle_events.my_event = self.my_event

    def test_act_must_invoke_correct_hook_method(self):
        # Setup the plugin to return a mock when the "on_" method is invoked
        plugin = _make_mock_plugin("plugin")
        hook_method = Mock()
        setattr(plugin, "on_" + self.my_event.name, hook_method)

        self.sam_plugins.register(plugin)

        # Call the act method with some arguments and verify that the hook was invoked with correct arguments
        arg1 = "foo"
        arg2 = "bar"
        kwargs1 = "kwargs1"
        kwargs2 = "kwargs2"
        self.sam_plugins.act(self.my_event, arg1, arg2, kwargs1=kwargs1, kwargs2=kwargs2)

        hook_method.assert_called_once_with(arg1, arg2, kwargs1=kwargs1, kwargs2=kwargs2)

    def test_act_must_invoke_hook_on_all_plugins(self):
        # Create three plugins, and setup hook methods on it
        plugin1 = _make_mock_plugin("plugin1")
        setattr(plugin1, "on_" + self.my_event.name, Mock())
        plugin2 = _make_mock_plugin("plugin2")
        setattr(plugin2, "on_" + self.my_event.name, Mock())
        plugin3 = _make_mock_plugin("plugin3")
        setattr(plugin3, "on_" + self.my_event.name, Mock())

        self.sam_plugins.register(plugin1)
        self.sam_plugins.register(plugin2)
        self.sam_plugins.register(plugin3)

        # Call the act method with some arguments and verify that the hook was invoked with correct arguments
        arg1 = "foo"
        arg2 = "bar"
        kwargs1 = "kwargs1"
        kwargs2 = "kwargs2"
        self.sam_plugins.act(self.my_event, arg1, arg2, kwargs1=kwargs1, kwargs2=kwargs2)

        plugin1.on_my_event.assert_called_once_with(arg1, arg2, kwargs1=kwargs1, kwargs2=kwargs2)
        plugin2.on_my_event.assert_called_once_with(arg1, arg2, kwargs1=kwargs1, kwargs2=kwargs2)
        plugin3.on_my_event.assert_called_once_with(arg1, arg2, kwargs1=kwargs1, kwargs2=kwargs2)

    def test_act_must_invoke_plugins_in_sequence(self):
        # Create three plugins, and setup hook methods on it
        plugin1 = _make_mock_plugin("plugin1")
        setattr(plugin1, "on_" + self.my_event.name, Mock())
        plugin2 = _make_mock_plugin("plugin2")
        setattr(plugin2, "on_" + self.my_event.name, Mock())
        plugin3 = _make_mock_plugin("plugin3")
        setattr(plugin3, "on_" + self.my_event.name, Mock())

        # Create a parent mock and attach child mocks to help assert order of the calls
        # https://stackoverflow.com/questions/32463321/how-to-assert-method-call-order-with-python-mock
        parent_mock = Mock()
        parent_mock.attach_mock(plugin1.on_my_event, "plugin1_hook")
        parent_mock.attach_mock(plugin2.on_my_event, "plugin2_hook")
        parent_mock.attach_mock(plugin3.on_my_event, "plugin3_hook")

        self.sam_plugins.register(plugin1)
        self.sam_plugins.register(plugin2)
        self.sam_plugins.register(plugin3)

        # Call the act method
        self.sam_plugins.act(self.my_event)

        # Verify calls were made in the specific sequence
        parent_mock.assert_has_calls([call.plugin1_hook(), call.plugin2_hook(), call.plugin3_hook()])

    def test_act_must_skip_if_no_plugins_are_registered(self):
        # Create three plugins, and setup hook methods on it
        plugin1 = _make_mock_plugin("plugin1")
        setattr(plugin1, "on_" + self.my_event.name, Mock())

        # Don't register any plugin

        # Call the act method
        self.sam_plugins.act(self.my_event)

        plugin1.on_my_event.assert_not_called()

    def test_act_must_fail_on_invalid_event_type_string(self):
        with self.assertRaises(ValueError):
            self.sam_plugins.act("some event")

    def test_act_must_fail_on_invalid_event_type_object(self):
        with self.assertRaises(ValueError):
            self.sam_plugins.act(Mock())

    def test_act_must_fail_on_invalid_event_type_enum(self):
        class SomeEnum(Enum):
            A = 1

        with self.assertRaises(ValueError):
            self.sam_plugins.act(SomeEnum.A)

    def test_act_must_fail_on_non_existent_hook_method(self):
        # Create a plugin but setup hook method with wrong name
        plugin1 = _make_mock_plugin("plugin1")
        plugin1.on_unknown_event = Mock()
        self.sam_plugins.register(plugin1)

        with self.assertRaises(NameError):
            self.sam_plugins.act(self.my_event)

        plugin1.on_unknown_event.assert_not_called()

    def test_act_must_raise_exceptions_raised_by_plugins(self):
        # Create a plugin but setup hook method with wrong name
        plugin1 = _make_mock_plugin("plugin1")
        setattr(plugin1, "on_" + self.my_event.name, Mock())
        self.sam_plugins.register(plugin1)

        # Setup the hook to raise exception
        plugin1.on_my_event.side_effect = IOError

        with self.assertRaises(IOError):
            self.sam_plugins.act(self.my_event)

        plugin1.on_my_event.assert_called_once_with()

    def test_act_must_abort_hooks_after_exception(self):
        # ie. after a hook raises an exception, subsequent hooks must NOT be run

        # Create three plugins, and setup hook methods on it
        plugin1 = _make_mock_plugin("plugin1")
        setattr(plugin1, "on_" + self.my_event.name, Mock())
        plugin2 = _make_mock_plugin("plugin2")
        setattr(plugin2, "on_" + self.my_event.name, Mock())
        plugin3 = _make_mock_plugin("plugin3")
        setattr(plugin3, "on_" + self.my_event.name, Mock())

        # Create a parent mock and attach child mocks to help assert order of the calls
        # https://stackoverflow.com/questions/32463321/how-to-assert-method-call-order-with-python-mock
        parent_mock = Mock()
        parent_mock.attach_mock(plugin1.on_my_event, "plugin1_hook")
        parent_mock.attach_mock(plugin2.on_my_event, "plugin2_hook")
        parent_mock.attach_mock(plugin3.on_my_event, "plugin3_hook")

        self.sam_plugins.register(plugin1)
        self.sam_plugins.register(plugin2)
        self.sam_plugins.register(plugin3)

        # setup plugin2 to raise exception
        plugin2.on_my_event.side_effect = IOError

        # Call the act method
        with self.assertRaises(IOError):
            self.sam_plugins.act(self.my_event)

        # Since Plugin2 raised the exception, plugin3's hook must NEVER be called
        parent_mock.assert_has_calls([call.plugin1_hook(), call.plugin2_hook()])


class Yoyoyo(BasePlugin):
    pass


class TestBasePlugin(TestCase):
    def test_initialization_should_set_name(self):
        plugin = Yoyoyo()
        self.assertEqual("Yoyoyo", plugin.name)

    def test_initialization_should_set_custom_name(self):
        plugin = Yoyoyo("custom-name")
        self.assertEqual("custom-name", plugin.name)

    def test_on_methods_must_not_do_anything(self):
        data_mock = Mock()

        # Simple test to verify that all methods starting with "on_" prefix must not implement any functionality
        plugin = Yoyoyo()

        # Add every on_ method here
        plugin.on_before_transform_resource(data_mock, data_mock, data_mock)

        # `method_calls` tracks calls to the mock, its methods, attributes, and *their* methods & attributes
        self.assertEqual([], data_mock.method_calls)


def _make_mock_plugin(name):
    m = Mock(spec=BasePlugin)
    m.name = name
    return m
