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

import ddt
from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import Group, User
from django.core.management import call_command, CommandError
from django.test import TestCase

TEST_EMAIL = 'test@example.com'
TEST_USERNAME = 'test-user'


@ddt.ddt
class TestManageUserCommand(TestCase):
    """
    Tests the `manage_user` command.
    """

    def test_user(self):
        """
        Ensures that users are created if they don't exist and reused if they do.
        """
        # pylint: disable=no-member
        self.assertEqual([], list(User.objects.all()))
        call_command('manage_user', TEST_USERNAME, TEST_EMAIL)
        user = User.objects.get(username=TEST_USERNAME)
        self.assertEqual(user.username, TEST_USERNAME)
        self.assertEqual(user.email, TEST_EMAIL)
        self.assertIsNotNone(user.profile)

        # check idempotency
        call_command('manage_user', TEST_USERNAME, TEST_EMAIL)
        self.assertEqual([(TEST_USERNAME, TEST_EMAIL)], [(u.username, u.email) for u in User.objects.all()])

    def test_remove(self):
        """
        Ensures that users are removed if they exist and exit cleanly otherwise.
        """
        # pylint: disable=no-member
        User.objects.create(username=TEST_USERNAME, email=TEST_EMAIL)
        self.assertEqual([(TEST_USERNAME, TEST_EMAIL)], [(u.username, u.email) for u in User.objects.all()])
        call_command('manage_user', TEST_USERNAME, TEST_EMAIL, '--remove')
        self.assertEqual([], list(User.objects.all()))

        # check idempotency
        call_command('manage_user', TEST_USERNAME, TEST_EMAIL, '--remove')
        self.assertEqual([], list(User.objects.all()))

    def test_unusable_password(self):
        """
        Ensure that a user's password is set to an unusable_password.
        """
        user = User.objects.create(username=TEST_USERNAME, email=TEST_EMAIL)
        self.assertEqual([(TEST_USERNAME, TEST_EMAIL)], [(u.username, u.email) for u in User.objects.all()])
        user.set_password(User.objects.make_random_password())
        user.save()

        # Run once without passing --unusable-password and make sure the password is usable
        call_command('manage_user', TEST_USERNAME, TEST_EMAIL)
        user = User.objects.get(username=TEST_USERNAME, email=TEST_EMAIL)
        self.assertTrue(user.has_usable_password())

        # Make sure the user now has an unusable_password
        call_command('manage_user', TEST_USERNAME, TEST_EMAIL, '--unusable-password')
        user = User.objects.get(username=TEST_USERNAME, email=TEST_EMAIL)
        self.assertFalse(user.has_usable_password())

        # check idempotency
        call_command('manage_user', TEST_USERNAME, TEST_EMAIL, '--unusable-password')
        self.assertFalse(user.has_usable_password())

    def test_initial_password_hash(self):
        """
        Ensure that a user's password hash is set correctly when the user is created,
        and that it isn't touched for existing users.
        """
        initial_hash = make_password('hunter2')

        # Make sure the command aborts if the provided hash isn't a valid Django password hash
        with self.assertRaises(CommandError) as exc_context:
            call_command('manage_user', TEST_USERNAME, TEST_EMAIL, '--initial-password-hash', 'invalid_hash')
        self.assertIn('password hash', str(exc_context.exception).lower())

        # Make sure the hash gets set correctly for a new user
        call_command('manage_user', TEST_USERNAME, TEST_EMAIL, '--initial-password-hash', initial_hash)
        user = User.objects.get(username=TEST_USERNAME)
        self.assertEqual(user.password, initial_hash)

        # Change the password
        new_hash = make_password('correct horse battery staple')
        user.password = new_hash
        user.save()

        # Verify that calling manage_user again leaves the password untouched
        call_command('manage_user', TEST_USERNAME, TEST_EMAIL, '--initial-password-hash', initial_hash)
        user = User.objects.get(username=TEST_USERNAME)
        self.assertEqual(user.password, new_hash)

    def test_wrong_email(self):
        """
        Ensure that the operation is aborted if the username matches an
        existing user account but the supplied email doesn't match.
        """
        # pylint: disable=no-member
        User.objects.create(username=TEST_USERNAME, email=TEST_EMAIL)
        with self.assertRaises(CommandError) as exc_context:
            call_command('manage_user', TEST_USERNAME, 'other@example.com')
        self.assertIn('email addresses do not match', str(exc_context.exception).lower())
        self.assertEqual([(TEST_USERNAME, TEST_EMAIL)], [(u.username, u.email) for u in User.objects.all()])

        # check that removal uses the same check
        with self.assertRaises(CommandError) as exc_context:
            call_command('manage_user', TEST_USERNAME, 'other@example.com', '--remove')
        self.assertIn('email addresses do not match', str(exc_context.exception).lower())
        self.assertEqual([(TEST_USERNAME, TEST_EMAIL)], [(u.username, u.email) for u in User.objects.all()])

    @ddt.data(*itertools.product([(True, True), (True, False), (False, True), (False, False)], repeat=2))
    @ddt.unpack
    def test_bits(self, initial_bits, expected_bits):
        """
        Ensure that the 'staff' and 'superuser' bits are set according to the
        presence / absence of the associated command options, regardless of
        any previous state.
        """
        initial_staff, initial_super = initial_bits
        User.objects.create(
            username=TEST_USERNAME,
            email=TEST_EMAIL,
            is_staff=initial_staff,
            is_superuser=initial_super
        )

        expected_staff, expected_super = expected_bits
        args = [opt for bit, opt in ((expected_staff, '--staff'), (expected_super, '--superuser')) if bit]
        call_command('manage_user', TEST_USERNAME, TEST_EMAIL, *args)
        user = User.objects.all().first()  # pylint: disable=no-member
        self.assertEqual(user.is_staff, expected_staff)
        self.assertEqual(user.is_superuser, expected_super)

    @ddt.data(*itertools.product(('', 'a', 'ab', 'abc'), repeat=2))
    @ddt.unpack
    def test_groups(self, initial_groups, expected_groups):
        """
        Ensures groups assignments are created and deleted idempotently.
        """
        groups = {}
        for group_name in 'abc':
            groups[group_name] = Group.objects.create(name=group_name)

        user = User.objects.create(username=TEST_USERNAME, email=TEST_EMAIL)
        for group_name in initial_groups:
            user.groups.add(groups[group_name])

        call_command('manage_user', TEST_USERNAME, TEST_EMAIL, '-g', *expected_groups)
        actual_groups = [group.name for group in user.groups.all()]
        self.assertEqual(actual_groups, list(expected_groups))

    def test_nonexistent_group(self):
        """
        Ensures the command does not fail if specified groups cannot be found.
        """
        user = User.objects.create(username=TEST_USERNAME, email=TEST_EMAIL)
        groups = {}
        for group_name in 'abc':
            groups[group_name] = Group.objects.create(name=group_name)
            user.groups.add(groups[group_name])

        call_command('manage_user', TEST_USERNAME, TEST_EMAIL, '-g', 'b', 'c', 'd')
        actual_groups = [group.name for group in user.groups.all()]
        self.assertEqual(actual_groups, ['b', 'c'])
