"""
Unit tests for user_management management commands.
"""
import sys

import ddt
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from django.core.management import call_command, CommandError
from django.test import TestCase

TEST_EMAIL = 'test@example.com'
TEST_GROUP = 'test-group'
TEST_USERNAME = 'test-user'
TEST_DATA = (
    {},
    {
        TEST_GROUP: ['add_group', 'change_group', 'change_group'],
    },
    {
        'other-group': ['add_group', 'change_group', 'change_group'],
    },
)


@ddt.ddt
class TestManageGroupCommand(TestCase):
    """
    Tests the `manage_group` command.
    """

    def set_group_permissions(self, group_permissions):
        """
        Sets up a before-state for groups and permissions in tests, which
        can be checked afterward to ensure that a failed atomic
        operation has not had any side effects.
        """
        content_type = ContentType.objects.get_for_model(Group)
        for group_name, permission_codenames in group_permissions.items():
            group = Group.objects.create(name=group_name)
            for codename in permission_codenames:
                group.permissions.add(
                    Permission.objects.get(content_type=content_type, codename=codename)  # pylint: disable=no-member
                )

    def check_group_permissions(self, group_permissions):
        """
        Checks that the current state of the database matches the specified groups and
        permissions.
        """
        self.check_groups(group_permissions.keys())
        for group_name, permission_codenames in group_permissions.items():
            self.check_permissions(group_name, permission_codenames)

    def check_groups(self, group_names):
        """
        DRY helper.
        """
        self.assertEqual(set(group_names), {g.name for g in Group.objects.all()})  # pylint: disable=no-member

    def check_permissions(self, group_name, permission_codenames):
        """
        DRY helper.
        """
        self.assertEqual(
            set(permission_codenames),
            {p.codename for p in Group.objects.get(name=group_name).permissions.all()}  # pylint: disable=no-member
        )

    @ddt.data(
        *(
            (data, args, exception)
            for data in TEST_DATA
            for args, exception in (
                ((), 'too few arguments' if sys.version_info.major == 2 else 'required: group_name'),  # no group name
                (('x' * 81,), 'invalid group name'),  # invalid group name
                ((TEST_GROUP, 'some-other-group'), 'unrecognized arguments'),  # multiple arguments
                ((TEST_GROUP, '--some-option', 'dummy'), 'unrecognized arguments')  # unexpected option name
            )
        )
    )
    @ddt.unpack
    def test_invalid_input(self, initial_group_permissions, command_args, exception_message):
        """
        Ensures that invalid inputs result in errors with relevant output,
        and that no persistent state is changed.
        """
        self.set_group_permissions(initial_group_permissions)

        with self.assertRaises(CommandError) as exc_context:
            call_command('manage_group', *command_args)
        self.assertIn(exception_message, str(exc_context.exception).lower())
        self.check_group_permissions(initial_group_permissions)

    @ddt.data(*TEST_DATA)
    def test_invalid_permission(self, initial_group_permissions):
        """
        Ensures that a permission that cannot be parsed or resolved results in
        and error and that no persistent state is changed.
        """
        self.set_group_permissions(initial_group_permissions)

        # not parseable
        with self.assertRaises(CommandError) as exc_context:
            call_command('manage_group', TEST_GROUP, '--permissions', 'fail')
        self.assertIn('invalid permission option', str(exc_context.exception).lower())
        self.check_group_permissions(initial_group_permissions)

        # not parseable
        with self.assertRaises(CommandError) as exc_context:
            call_command('manage_group', TEST_GROUP, '--permissions', 'f:a:i:l')
        self.assertIn('invalid permission option', str(exc_context.exception).lower())
        self.check_group_permissions(initial_group_permissions)

        # invalid app label
        with self.assertRaises(CommandError) as exc_context:
            call_command('manage_group', TEST_GROUP, '--permissions', 'nonexistent-label:dummy-model:dummy-perm')
        self.assertIn('no installed app', str(exc_context.exception).lower())
        self.assertIn('nonexistent-label', str(exc_context.exception).lower())
        self.check_group_permissions(initial_group_permissions)

        # invalid model name
        with self.assertRaises(CommandError) as exc_context:
            call_command('manage_group', TEST_GROUP, '--permissions', 'auth:nonexistent-model:dummy-perm')
        self.assertIn('nonexistent-model', str(exc_context.exception).lower())
        self.check_group_permissions(initial_group_permissions)

        # invalid model name
        with self.assertRaises(CommandError) as exc_context:
            call_command('manage_group', TEST_GROUP, '--permissions', 'auth:Group:nonexistent-perm')
        self.assertIn('invalid permission codename', str(exc_context.exception).lower())
        self.assertIn('nonexistent-perm', str(exc_context.exception).lower())
        self.check_group_permissions(initial_group_permissions)

    def test_group(self):
        """
        Ensures that groups are created if they don't exist and reused if they do.
        """
        self.check_groups([])
        call_command('manage_group', TEST_GROUP)
        self.check_groups([TEST_GROUP])

        # check idempotency
        call_command('manage_group', TEST_GROUP)
        self.check_groups([TEST_GROUP])

    def test_group_remove(self):
        """
        Ensures that groups are removed if they exist and we exit cleanly otherwise.
        """
        self.set_group_permissions({TEST_GROUP: ['add_group']})
        self.check_groups([TEST_GROUP])
        call_command('manage_group', TEST_GROUP, '--remove')
        self.check_groups([])

        # check idempotency
        call_command('manage_group', TEST_GROUP, '--remove')
        self.check_groups([])

    def test_permissions(self):
        """
        Ensures that permissions are set on the group as specified.
        """
        self.check_groups([])
        call_command('manage_group', TEST_GROUP, '--permissions', 'auth:Group:add_group')
        self.check_groups([TEST_GROUP])
        self.check_permissions(TEST_GROUP, ['add_group'])

        # check idempotency
        call_command('manage_group', TEST_GROUP, '--permissions', 'auth:Group:add_group')
        self.check_groups([TEST_GROUP])
        self.check_permissions(TEST_GROUP, ['add_group'])

        # check adding a permission
        call_command('manage_group', TEST_GROUP, '--permissions', 'auth:Group:add_group', 'auth:Group:change_group')
        self.check_groups([TEST_GROUP])
        self.check_permissions(TEST_GROUP, ['add_group', 'change_group'])

        # check removing a permission
        call_command('manage_group', TEST_GROUP, '--permissions', 'auth:Group:change_group')
        self.check_groups([TEST_GROUP])
        self.check_permissions(TEST_GROUP, ['change_group'])

        # check removing all permissions
        call_command('manage_group', TEST_GROUP)
        self.check_groups([TEST_GROUP])
        self.check_permissions(TEST_GROUP, [])