"""This module holds the test cases for the hooks of the ``sync`` app"""

from django.conf import settings
from django.test import TestCase
from django.test.utils import override_settings

from ..hooks import (
    _hooks,
    pre_sync_hook,
    post_sync_hook,
    run_pre_sync_hooks,
    run_post_sync_hooks,
)


class HooksTestCase(TestCase):
    """Test case to test the ``sync.hooks`` module."""

    def setUp(self):
        """Clear registered hooks before each test."""

        super().setUp()
        _hooks.clear()

    @override_settings(INSTANCE_IS_MASTER=True, INSTANCE_IS_SLAVE=False)
    def test_pre_sync_for_master(self):
        """Test the ``pre_sync_hook`` decorator on a master instance."""

        result = []

        # This function should be run only on master.
        @pre_sync_hook(settings.INSTANCE_IS_MASTER)
        def function_to_pre_sync_on_master():  # pylint: disable=unused-variable
            """Sample decorated pre-sync master method"""

            result.append('pre master')

        # This function should be run only on slave.
        @pre_sync_hook(settings.INSTANCE_IS_SLAVE)
        def function_to_pre_sync_on_slave():  # pylint: disable=unused-variable
            """Sample decorated pre-sync slave method"""

            result.append('pre slave')

        # This function should be run on all servers.
        @pre_sync_hook
        def function_to_pre_sync_on_all():  # pylint: disable=unused-variable
            """Sample decorated pre-sync method"""

            result.append('pre all')

        # We should only have ``pre`` hooks, and not the one declared with ``INSTANCE_IS_SLAVE``.
        self.assertEqual(list(_hooks.keys()), ['pre'])
        self.assertListEqual(
            [f.__name__ for f in _hooks['pre']],
            ['function_to_pre_sync_on_master', 'function_to_pre_sync_on_all']
        )

        # Running the hook should execute both activated functions.
        run_pre_sync_hooks()
        self.assertListEqual(result, ['pre master', 'pre all'])

    @override_settings(INSTANCE_IS_MASTER=False, INSTANCE_IS_SLAVE=True)
    def test_pre_sync_for_slave(self):
        """Test the ``pre_sync_hook`` decorator on a slave instance."""

        result = []

        # This function should be run only on master.
        @pre_sync_hook(settings.INSTANCE_IS_MASTER)
        def function_to_pre_sync_on_master():  # pylint: disable=unused-variable
            """Sample decorated pre-sync master method"""

            result.append('pre master')

        # This function should be run only on slave.
        @pre_sync_hook(settings.INSTANCE_IS_SLAVE)
        def function_to_pre_sync_on_slave():  # pylint: disable=unused-variable
            """Sample decorated pre-sync slave method"""

            result.append('pre slave')

        # This function should be run on all servers.
        @pre_sync_hook
        def function_to_pre_sync_on_all():  # pylint: disable=unused-variable
            """Sample decorated pre-sync method"""

            result.append('pre all')

        # We should only have ``pre`` hooks, and not the one declared with ``INSTANCE_IS_MASTER``.
        self.assertEqual(list(_hooks.keys()), ['pre'])
        self.assertListEqual(
            [f.__name__ for f in _hooks['pre']],
            ['function_to_pre_sync_on_slave', 'function_to_pre_sync_on_all']
        )

        # Running the hook should execute both activated functions.
        run_pre_sync_hooks()
        self.assertListEqual(result, ['pre slave', 'pre all'])

    @override_settings(INSTANCE_IS_MASTER=True, INSTANCE_IS_SLAVE=False)
    def test_post_sync_for_master(self):
        """Test the ``post_sync_hook`` decorator on a master instance."""

        result = []

        # This function should be run only on master.
        @post_sync_hook(settings.INSTANCE_IS_MASTER)
        def function_to_post_sync_on_master():  # pylint: disable=unused-variable
            """Sample decorated pre-sync master method"""

            result.append('post master')

        # This function should be run only on slave.
        @post_sync_hook(settings.INSTANCE_IS_SLAVE)
        def function_to_post_sync_on_slave():  # pylint: disable=unused-variable
            """Sample decorated pre-sync slave method"""

            result.append('post slave')

        # This function should be run on all servers.
        @post_sync_hook
        def function_to_post_sync_on_all():  # pylint: disable=unused-variable
            """Sample decorated pre-sync method"""

            result.append('post all')

        # We should only have ``pre`` hooks, and not the one declared with ``INSTANCE_IS_SLAVE``.
        self.assertEqual(list(_hooks.keys()), ['post'])
        self.assertListEqual(
            [f.__name__ for f in _hooks['post']],
            ['function_to_post_sync_on_master', 'function_to_post_sync_on_all']
        )

        # Running the hook should execute both activated functions.
        run_post_sync_hooks()
        self.assertListEqual(result, ['post master', 'post all'])

    @override_settings(INSTANCE_IS_MASTER=False, INSTANCE_IS_SLAVE=True)
    def test_post_sync_for_slave(self):
        """Test the ``post_sync_hook`` decorator on a slave instance."""

        result = []

        # This function should be run only on master.
        @post_sync_hook(settings.INSTANCE_IS_MASTER)
        def function_to_post_sync_on_master():  # pylint: disable=unused-variable
            """Sample decorated post-sync master method"""

            result.append('post master')

        # This function should be run only on slave.
        @post_sync_hook(settings.INSTANCE_IS_SLAVE)
        def function_to_post_sync_on_slave():  # pylint: disable=unused-variable
            """Sample decorated post-sync slave method"""

            result.append('post slave')

        # This function should be run on all servers.
        @post_sync_hook
        def function_to_post_sync_on_all():  # pylint: disable=unused-variable
            """Sample decorated post-sync method"""

            result.append('post all')

        # We should only have ``pre`` hooks, and not the one declared with ``INSTANCE_IS_MASTER``.
        self.assertEqual(list(_hooks.keys()), ['post'])
        self.assertListEqual(
            [f.__name__ for f in _hooks['post']],
            ['function_to_post_sync_on_slave', 'function_to_post_sync_on_all']
        )

        # Running the hook should execute both activated functions.
        run_post_sync_hooks()
        self.assertListEqual(result, ['post slave', 'post all'])

    def test_hook_decorator_without_argument_should_succed(self):
        """Test that the decorators could be called with no arguments."""

        # It should work with no parentheses.
        @pre_sync_hook
        def function1():  # pylint: disable=unused-variable
            """Sample decorated pre-sync method"""

            pass

        # It should work with parentheses and no arguments.
        @pre_sync_hook()
        def function2():  # pylint: disable=unused-variable
            """Sample decorated pre-sync method"""

            pass

        # Check that we have both methods registered.
        self.assertEqual(list(_hooks.keys()), ['pre'])
        self.assertListEqual([f.__name__ for f in _hooks['pre']], ['function1', 'function2'])

    def test_hook_decorator_with_named_argument_should_succed(self):
        """Test that the decorators could be called with named argument."""

        # It should work with using the default value.
        @post_sync_hook(activated=True)
        def function1():  # pylint: disable=unused-variable
            """Sample post-sync hook method"""

            pass

        # It should work with using the non-default value (but not registered).
        @post_sync_hook(activated=False)
        def function2():  # pylint: disable=unused-variable
            """Sample post-sync hook method"""

            pass

        # Check that we have only the hook with ``activated=True``.
        self.assertEqual(list(_hooks.keys()), ['post'])
        self.assertListEqual([f.__name__ for f in _hooks['post']], ['function1'])

    def test_hook_decorator_on_function_with_non_default_arguments_should_fail(self):
        """Test that a function with arguments without default values are rejected."""

        with self.assertRaises(ValueError) as context:
            @pre_sync_hook
            # pylint: disable=unused-variable,unused-argument
            def function1(first_arg):
                """Sample pre-sync hook method"""

                pass

        self.assertIn('default value', str(context.exception))

        with self.assertRaises(ValueError) as context:
            @pre_sync_hook
            # pylint: disable=unused-variable,unused-argument
            def function2(first_arg, second_arg=2):
                """Sample pre-sync hook method"""

                pass

        self.assertIn('default value', str(context.exception))

    def test_hook_decorator_on_function_with_default_arguments_should_succeed(self):
        """Test that a function with arguments with default values are accepted."""

        result = []

        # Check with only one argument with default value.
        @pre_sync_hook
        def function1(first_arg=1):  # pylint: disable=unused-variable
            """Sample pre-sync hook method"""

            result.append('called with %s' % first_arg)

        # Check with more than one argument with default values.
        @pre_sync_hook
        def function2(first_arg=1, second_arg=2):  # pylint: disable=unused-variable
            """Sample pre-sync hook method"""

            result.append('called with %s, %s' % (first_arg, second_arg))

        # Check that both our methods are correctly called with their arguments.
        run_pre_sync_hooks()
        self.assertListEqual(result, ['called with 1', 'called with 1, 2'])

    def test_hook_decorator_work_only_with_simple_functions(self):
        """Test that the decorator only works with normal functions."""

        # Check instance methods.
        with self.assertRaises(ValueError) as context:
            class Klass1:  # pylint: disable=unused-variable
                """Sample class"""

                @pre_sync_hook(True)
                def method(self):  # pylint: disable=unused-variable
                    """Sample decorated method"""

                    pass

        # We cannot check that it's an instance method but we have a failure because
        # of the ``self`` argument
        self.assertIn('default value', str(context.exception))

        # Check class methods.
        with self.assertRaises(ValueError) as context:
            class Klass2:  # pylint: disable=unused-variable
                """Sample class"""

                @pre_sync_hook(True)
                @classmethod
                def method(cls):  # pylint: disable=unused-variable
                    """Sample decorated method"""

                    pass

        self.assertIn('simple functions', str(context.exception))

        # Check static methods.
        with self.assertRaises(ValueError) as context:
            class Klass3:  # pylint: disable=unused-variable
                """Sample class"""

                @pre_sync_hook(True)
                @staticmethod
                def method():  # pylint: disable=unused-variable
                    """Sample decorated method"""

                    pass

        self.assertIn('simple functions', str(context.exception))
