from __future__ import unicode_literals

import locale
import sys
from datetime import date

from django.apps import apps
from django.contrib.auth import management, models
from django.contrib.auth.checks import check_user_model
from django.contrib.auth.management import create_permissions
from django.contrib.auth.management.commands import (
    changepassword, createsuperuser,
)
from django.contrib.auth.models import Group, User
from django.contrib.auth.tests.custom_user import CustomUser
from django.contrib.contenttypes.models import ContentType
from django.core import checks, exceptions
from django.core.management import call_command
from django.core.management.base import CommandError
from django.test import (
    SimpleTestCase, TestCase, override_settings, override_system_checks,
)
from django.utils import six
from django.utils.encoding import force_str
from django.utils.translation import ugettext_lazy as _

from .models import (
    CustomUserBadRequiredFields, CustomUserNonListRequiredFields,
    CustomUserNonUniqueUsername, CustomUserWithFK, Email,
)


def mock_inputs(inputs):
    """
    Decorator to temporarily replace input/getpass to allow interactive
    createsuperuser.
    """
    def inner(test_func):
        def wrapped(*args):
            class mock_getpass:
                @staticmethod
                def getpass(prompt=b'Password: ', stream=None):
                    if six.PY2:
                        # getpass on Windows only supports prompt as bytestring (#19807)
                        assert isinstance(prompt, six.binary_type)
                    if callable(inputs['password']):
                        return inputs['password']()
                    return inputs['password']

            def mock_input(prompt):
                # prompt should be encoded in Python 2. This line will raise an
                # Exception if prompt contains unencoded non-ASCII on Python 2.
                prompt = str(prompt)
                assert str('__proxy__') not in prompt
                response = ''
                for key, val in inputs.items():
                    if force_str(key) in prompt.lower():
                        response = val
                        break
                return response

            old_getpass = createsuperuser.getpass
            old_input = createsuperuser.input
            createsuperuser.getpass = mock_getpass
            createsuperuser.input = mock_input
            try:
                test_func(*args)
            finally:
                createsuperuser.getpass = old_getpass
                createsuperuser.input = old_input
        return wrapped
    return inner


class MockTTY(object):
    """
    A fake stdin object that pretends to be a TTY to be used in conjunction
    with mock_inputs.
    """
    def isatty(self):
        return True


class GetDefaultUsernameTestCase(TestCase):

    def setUp(self):
        self.old_get_system_username = management.get_system_username

    def tearDown(self):
        management.get_system_username = self.old_get_system_username

    def test_actual_implementation(self):
        self.assertIsInstance(management.get_system_username(), six.text_type)

    def test_simple(self):
        management.get_system_username = lambda: 'joe'
        self.assertEqual(management.get_default_username(), 'joe')

    def test_existing(self):
        models.User.objects.create(username='joe')
        management.get_system_username = lambda: 'joe'
        self.assertEqual(management.get_default_username(), '')
        self.assertEqual(
            management.get_default_username(check_db=False), 'joe')

    def test_i18n(self):
        # 'Julia' with accented 'u':
        management.get_system_username = lambda: 'J\xfalia'
        self.assertEqual(management.get_default_username(), 'julia')


@override_settings(AUTH_PASSWORD_VALIDATORS=[
    {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
])
class ChangepasswordManagementCommandTestCase(TestCase):

    def setUp(self):
        self.user = models.User.objects.create_user(username='joe', password='qwerty')
        self.stdout = six.StringIO()
        self.stderr = six.StringIO()

    def tearDown(self):
        self.stdout.close()
        self.stderr.close()

    def test_that_changepassword_command_changes_joes_password(self):
        "Executing the changepassword management command should change joe's password"
        self.assertTrue(self.user.check_password('qwerty'))
        command = changepassword.Command()
        command._get_pass = lambda *args: 'not qwerty'

        command.execute(username="joe", stdout=self.stdout)
        command_output = self.stdout.getvalue().strip()

        self.assertEqual(
            command_output,
            "Changing password for user 'joe'\nPassword changed successfully for user 'joe'"
        )
        self.assertTrue(models.User.objects.get(username="joe").check_password("not qwerty"))

    def test_that_max_tries_exits_1(self):
        """
        A CommandError should be thrown by handle() if the user enters in
        mismatched passwords three times.
        """
        command = changepassword.Command()
        command._get_pass = lambda *args: str(args) or 'foo'

        with self.assertRaises(CommandError):
            command.execute(username="joe", stdout=self.stdout, stderr=self.stderr)

    def test_password_validation(self):
        """
        A CommandError should be raised if the user enters in passwords which
        fail validation three times.
        """
        command = changepassword.Command()
        command._get_pass = lambda *args: '1234567890'

        abort_msg = "Aborting password change for user 'joe' after 3 attempts"
        with self.assertRaisesMessage(CommandError, abort_msg):
            command.execute(username="joe", stdout=self.stdout, stderr=self.stderr)
        self.assertIn('This password is entirely numeric.', self.stderr.getvalue())

    def test_that_changepassword_command_works_with_nonascii_output(self):
        """
        #21627 -- Executing the changepassword management command should allow
        non-ASCII characters from the User object representation.
        """
        # 'Julia' with accented 'u':
        models.User.objects.create_user(username='J\xfalia', password='qwerty')

        command = changepassword.Command()
        command._get_pass = lambda *args: 'not qwerty'

        command.execute(username="J\xfalia", stdout=self.stdout)


@override_settings(
    SILENCED_SYSTEM_CHECKS=['fields.W342'],  # ForeignKey(unique=True)
    AUTH_PASSWORD_VALIDATORS=[{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}],
)
class CreatesuperuserManagementCommandTestCase(TestCase):

    def test_basic_usage(self):
        "Check the operation of the createsuperuser management command"
        # We can use the management command to create a superuser
        new_io = six.StringIO()
        call_command(
            "createsuperuser",
            interactive=False,
            username="joe",
            email="joe@somewhere.org",
            stdout=new_io
        )
        command_output = new_io.getvalue().strip()
        self.assertEqual(command_output, 'Superuser created successfully.')
        u = User.objects.get(username="joe")
        self.assertEqual(u.email, 'joe@somewhere.org')

        # created password should be unusable
        self.assertFalse(u.has_usable_password())

    @mock_inputs({'password': "nopasswd"})
    def test_nolocale(self):
        """
        Check that createsuperuser does not break when no locale is set. See
        ticket #16017.
        """

        old_getdefaultlocale = locale.getdefaultlocale
        try:
            # Temporarily remove locale information
            locale.getdefaultlocale = lambda: (None, None)

            # Call the command in this new environment
            call_command(
                "createsuperuser",
                interactive=True,
                username="nolocale@somewhere.org",
                email="nolocale@somewhere.org",
                verbosity=0,
                stdin=MockTTY(),
            )

        except TypeError:
            self.fail("createsuperuser fails if the OS provides no information about the current locale")

        finally:
            # Re-apply locale information
            locale.getdefaultlocale = old_getdefaultlocale

        # If we were successful, a user should have been created
        u = User.objects.get(username="nolocale@somewhere.org")
        self.assertEqual(u.email, 'nolocale@somewhere.org')

    @mock_inputs({
        'password': "nopasswd",
        'u\u017eivatel': 'foo',  # username (cz)
        'email': 'nolocale@somewhere.org'})
    def test_non_ascii_verbose_name(self):
        username_field = User._meta.get_field('username')
        old_verbose_name = username_field.verbose_name
        username_field.verbose_name = _('u\u017eivatel')
        new_io = six.StringIO()
        try:
            call_command(
                "createsuperuser",
                interactive=True,
                stdout=new_io,
                stdin=MockTTY(),
            )
        finally:
            username_field.verbose_name = old_verbose_name

        command_output = new_io.getvalue().strip()
        self.assertEqual(command_output, 'Superuser created successfully.')

    def test_verbosity_zero(self):
        # We can suppress output on the management command
        new_io = six.StringIO()
        call_command(
            "createsuperuser",
            interactive=False,
            username="joe2",
            email="joe2@somewhere.org",
            verbosity=0,
            stdout=new_io
        )
        command_output = new_io.getvalue().strip()
        self.assertEqual(command_output, '')
        u = User.objects.get(username="joe2")
        self.assertEqual(u.email, 'joe2@somewhere.org')
        self.assertFalse(u.has_usable_password())

    def test_email_in_username(self):
        new_io = six.StringIO()
        call_command(
            "createsuperuser",
            interactive=False,
            username="joe+admin@somewhere.org",
            email="joe@somewhere.org",
            stdout=new_io
        )
        u = User._default_manager.get(username="joe+admin@somewhere.org")
        self.assertEqual(u.email, 'joe@somewhere.org')
        self.assertFalse(u.has_usable_password())

    @override_settings(AUTH_USER_MODEL='auth.CustomUser')
    def test_swappable_user(self):
        "A superuser can be created when a custom User model is in use"
        # We can use the management command to create a superuser
        # We skip validation because the temporary substitution of the
        # swappable User model messes with validation.
        new_io = six.StringIO()
        call_command(
            "createsuperuser",
            interactive=False,
            email="joe@somewhere.org",
            date_of_birth="1976-04-01",
            stdout=new_io,
        )
        command_output = new_io.getvalue().strip()
        self.assertEqual(command_output, 'Superuser created successfully.')
        u = CustomUser._default_manager.get(email="joe@somewhere.org")
        self.assertEqual(u.date_of_birth, date(1976, 4, 1))

        # created password should be unusable
        self.assertFalse(u.has_usable_password())

    @override_settings(AUTH_USER_MODEL='auth.CustomUser')
    def test_swappable_user_missing_required_field(self):
        "A Custom superuser won't be created when a required field isn't provided"
        # We can use the management command to create a superuser
        # We skip validation because the temporary substitution of the
        # swappable User model messes with validation.
        new_io = six.StringIO()
        with self.assertRaises(CommandError):
            call_command(
                "createsuperuser",
                interactive=False,
                username="joe@somewhere.org",
                stdout=new_io,
                stderr=new_io,
            )

        self.assertEqual(CustomUser._default_manager.count(), 0)

    @override_settings(
        AUTH_USER_MODEL='auth_tests.CustomUserNonUniqueUsername',
        AUTHENTICATION_BACKENDS=['my.custom.backend'],
    )
    def test_swappable_user_username_non_unique(self):
        @mock_inputs({
            'username': 'joe',
            'password': 'nopasswd',
        })
        def createsuperuser():
            new_io = six.StringIO()
            call_command(
                "createsuperuser",
                interactive=True,
                email="joe@somewhere.org",
                stdout=new_io,
                stdin=MockTTY(),
            )
            command_output = new_io.getvalue().strip()
            self.assertEqual(command_output, 'Superuser created successfully.')

        for i in range(2):
            createsuperuser()

        users = CustomUserNonUniqueUsername.objects.filter(username="joe")
        self.assertEqual(users.count(), 2)

    def test_skip_if_not_in_TTY(self):
        """
        If the command is not called from a TTY, it should be skipped and a
        message should be displayed (#7423).
        """
        class FakeStdin(object):
            """A fake stdin object that has isatty() return False."""
            def isatty(self):
                return False

        out = six.StringIO()
        call_command(
            "createsuperuser",
            stdin=FakeStdin(),
            stdout=out,
            interactive=True,
        )

        self.assertEqual(User._default_manager.count(), 0)
        self.assertIn("Superuser creation skipped", out.getvalue())

    def test_passing_stdin(self):
        """
        You can pass a stdin object as an option and it should be
        available on self.stdin.
        If no such option is passed, it defaults to sys.stdin.
        """
        sentinel = object()
        command = createsuperuser.Command()
        command.check = lambda: []
        command.execute(
            stdin=sentinel,
            stdout=six.StringIO(),
            stderr=six.StringIO(),
            interactive=False,
            verbosity=0,
            username='janet',
            email='janet@example.com',
        )
        self.assertIs(command.stdin, sentinel)

        command = createsuperuser.Command()
        command.check = lambda: []
        command.execute(
            stdout=six.StringIO(),
            stderr=six.StringIO(),
            interactive=False,
            verbosity=0,
            username='joe',
            email='joe@example.com',
        )
        self.assertIs(command.stdin, sys.stdin)

    @override_settings(AUTH_USER_MODEL='auth_tests.CustomUserWithFK')
    def test_fields_with_fk(self):
        new_io = six.StringIO()
        group = Group.objects.create(name='mygroup')
        email = Email.objects.create(email='mymail@gmail.com')
        call_command(
            'createsuperuser',
            interactive=False,
            username=email.pk,
            email=email.email,
            group=group.pk,
            stdout=new_io,
        )
        command_output = new_io.getvalue().strip()
        self.assertEqual(command_output, 'Superuser created successfully.')
        u = CustomUserWithFK._default_manager.get(email=email)
        self.assertEqual(u.username, email)
        self.assertEqual(u.group, group)

        non_existent_email = 'mymail2@gmail.com'
        with self.assertRaisesMessage(CommandError,
                'email instance with email %r does not exist.' % non_existent_email):
            call_command(
                'createsuperuser',
                interactive=False,
                username=email.pk,
                email=non_existent_email,
                stdout=new_io,
            )

    @override_settings(AUTH_USER_MODEL='auth_tests.CustomUserWithFK')
    def test_fields_with_fk_interactive(self):
        new_io = six.StringIO()
        group = Group.objects.create(name='mygroup')
        email = Email.objects.create(email='mymail@gmail.com')

        @mock_inputs({
            'password': 'nopasswd',
            'username (email.id)': email.pk,
            'email (email.email)': email.email,
            'group (group.id)': group.pk,
        })
        def test(self):
            call_command(
                'createsuperuser',
                interactive=True,
                stdout=new_io,
                stdin=MockTTY(),
            )

            command_output = new_io.getvalue().strip()
            self.assertEqual(command_output, 'Superuser created successfully.')
            u = CustomUserWithFK._default_manager.get(email=email)
            self.assertEqual(u.username, email)
            self.assertEqual(u.group, group)

        test(self)

    def test_password_validation(self):
        """
        Creation should fail if the password fails validation.
        """
        new_io = six.StringIO()

        # Returns '1234567890' the first two times it is called, then
        # 'password' subsequently.
        def bad_then_good_password(index=[0]):
            index[0] += 1
            if index[0] <= 2:
                return '1234567890'
            return 'password'

        @mock_inputs({
            'password': bad_then_good_password,
            'username': 'joe1234567890',
        })
        def test(self):
            call_command(
                "createsuperuser",
                interactive=True,
                stdin=MockTTY(),
                stdout=new_io,
                stderr=new_io,
            )
            self.assertEqual(
                new_io.getvalue().strip(),
                "This password is entirely numeric.\n"
                "Superuser created successfully."
            )

        test(self)

    def test_validation_mismatched_passwords(self):
        """
        Creation should fail if the user enters mismatched passwords.
        """
        new_io = six.StringIO()

        # The first two passwords do not match, but the second two do match and
        # are valid.
        entered_passwords = ["password", "not password", "password2", "password2"]

        def mismatched_passwords_then_matched():
            return entered_passwords.pop(0)

        @mock_inputs({
            'password': mismatched_passwords_then_matched,
            'username': 'joe1234567890',
        })
        def test(self):
            call_command(
                "createsuperuser",
                interactive=True,
                stdin=MockTTY(),
                stdout=new_io,
                stderr=new_io,
            )
            self.assertEqual(
                new_io.getvalue().strip(),
                "Error: Your passwords didn't match.\n"
                "Superuser created successfully."
            )

        test(self)

    def test_validation_blank_password_entered(self):
        """
        Creation should fail if the user enters blank passwords.
        """
        new_io = six.StringIO()

        # The first two passwords are empty strings, but the second two are
        # valid.
        entered_passwords = ["", "", "password2", "password2"]

        def blank_passwords_then_valid():
            return entered_passwords.pop(0)

        @mock_inputs({
            'password': blank_passwords_then_valid,
            'username': 'joe1234567890',
        })
        def test(self):
            call_command(
                "createsuperuser",
                interactive=True,
                stdin=MockTTY(),
                stdout=new_io,
                stderr=new_io,
            )
            self.assertEqual(
                new_io.getvalue().strip(),
                "Error: Blank passwords aren't allowed.\n"
                "Superuser created successfully."
            )

        test(self)


class CustomUserModelValidationTestCase(SimpleTestCase):
    @override_settings(AUTH_USER_MODEL='auth_tests.CustomUserNonListRequiredFields')
    @override_system_checks([check_user_model])
    def test_required_fields_is_list(self):
        "REQUIRED_FIELDS should be a list."
        errors = checks.run_checks()
        expected = [
            checks.Error(
                "'REQUIRED_FIELDS' must be a list or tuple.",
                hint=None,
                obj=CustomUserNonListRequiredFields,
                id='auth.E001',
            ),
        ]
        self.assertEqual(errors, expected)

    @override_settings(AUTH_USER_MODEL='auth_tests.CustomUserBadRequiredFields')
    @override_system_checks([check_user_model])
    def test_username_not_in_required_fields(self):
        "USERNAME_FIELD should not appear in REQUIRED_FIELDS."
        errors = checks.run_checks()
        expected = [
            checks.Error(
                ("The field named as the 'USERNAME_FIELD' for a custom user model "
                 "must not be included in 'REQUIRED_FIELDS'."),
                hint=None,
                obj=CustomUserBadRequiredFields,
                id='auth.E002',
            ),
        ]
        self.assertEqual(errors, expected)

    @override_settings(AUTH_USER_MODEL='auth_tests.CustomUserNonUniqueUsername')
    @override_system_checks([check_user_model])
    def test_username_non_unique(self):
        "A non-unique USERNAME_FIELD should raise a model validation error."
        errors = checks.run_checks()
        expected = [
            checks.Error(
                ("'CustomUserNonUniqueUsername.username' must be "
                 "unique because it is named as the 'USERNAME_FIELD'."),
                hint=None,
                obj=CustomUserNonUniqueUsername,
                id='auth.E003',
            ),
        ]
        self.assertEqual(errors, expected)

    @override_settings(AUTH_USER_MODEL='auth_tests.CustomUserNonUniqueUsername',
                       AUTHENTICATION_BACKENDS=[
                           'my.custom.backend',
                       ])
    @override_system_checks([check_user_model])
    def test_username_non_unique_with_custom_backend(self):
        """ A non-unique USERNAME_FIELD should raise an error only if we use the
        default authentication backend. Otherwise, an warning should be raised.
        """
        errors = checks.run_checks()
        expected = [
            checks.Warning(
                ("'CustomUserNonUniqueUsername.username' is named as "
                 "the 'USERNAME_FIELD', but it is not unique."),
                hint=('Ensure that your authentication backend(s) can handle '
                      'non-unique usernames.'),
                obj=CustomUserNonUniqueUsername,
                id='auth.W004',
            )
        ]
        self.assertEqual(errors, expected)


class PermissionTestCase(TestCase):

    def setUp(self):
        self._original_permissions = models.Permission._meta.permissions[:]
        self._original_default_permissions = models.Permission._meta.default_permissions
        self._original_verbose_name = models.Permission._meta.verbose_name

    def tearDown(self):
        models.Permission._meta.permissions = self._original_permissions
        models.Permission._meta.default_permissions = self._original_default_permissions
        models.Permission._meta.verbose_name = self._original_verbose_name
        ContentType.objects.clear_cache()

    def test_duplicated_permissions(self):
        """
        Test that we show proper error message if we are trying to create
        duplicate permissions.
        """
        auth_app_config = apps.get_app_config('auth')

        # check duplicated default permission
        models.Permission._meta.permissions = [
            ('change_permission', 'Can edit permission (duplicate)')]
        six.assertRaisesRegex(self, CommandError,
            "The permission codename 'change_permission' clashes with a "
            "builtin permission for model 'auth.Permission'.",
            create_permissions, auth_app_config, verbosity=0)

        # check duplicated custom permissions
        models.Permission._meta.permissions = [
            ('my_custom_permission', 'Some permission'),
            ('other_one', 'Some other permission'),
            ('my_custom_permission', 'Some permission with duplicate permission code'),
        ]
        six.assertRaisesRegex(self, CommandError,
            "The permission codename 'my_custom_permission' is duplicated for model "
            "'auth.Permission'.",
            create_permissions, auth_app_config, verbosity=0)

        # should not raise anything
        models.Permission._meta.permissions = [
            ('my_custom_permission', 'Some permission'),
            ('other_one', 'Some other permission'),
        ]
        create_permissions(auth_app_config, verbosity=0)

    def test_default_permissions(self):
        auth_app_config = apps.get_app_config('auth')

        permission_content_type = ContentType.objects.get_by_natural_key('auth', 'permission')
        models.Permission._meta.permissions = [
            ('my_custom_permission', 'Some permission'),
        ]
        create_permissions(auth_app_config, verbosity=0)

        # add/change/delete permission by default + custom permission
        self.assertEqual(models.Permission.objects.filter(
            content_type=permission_content_type,
        ).count(), 4)

        models.Permission.objects.filter(content_type=permission_content_type).delete()
        models.Permission._meta.default_permissions = []
        create_permissions(auth_app_config, verbosity=0)

        # custom permission only since default permissions is empty
        self.assertEqual(models.Permission.objects.filter(
            content_type=permission_content_type,
        ).count(), 1)

    def test_verbose_name_length(self):
        auth_app_config = apps.get_app_config('auth')

        permission_content_type = ContentType.objects.get_by_natural_key('auth', 'permission')
        models.Permission.objects.filter(content_type=permission_content_type).delete()
        models.Permission._meta.verbose_name = "some ridiculously long verbose name that is out of control" * 5

        six.assertRaisesRegex(self, exceptions.ValidationError,
            "The verbose_name of auth.permission is longer than 244 characters",
            create_permissions, auth_app_config, verbosity=0)

    def test_custom_permission_name_length(self):
        auth_app_config = apps.get_app_config('auth')

        ContentType.objects.get_by_natural_key('auth', 'permission')
        custom_perm_name = 'a' * 256
        models.Permission._meta.permissions = [
            ('my_custom_permission', custom_perm_name),
        ]
        try:
            msg = (
                "The permission name %s of auth.permission is longer than "
                "255 characters" % custom_perm_name
            )
            with self.assertRaisesMessage(exceptions.ValidationError, msg):
                create_permissions(auth_app_config, verbosity=0)
        finally:
            models.Permission._meta.permissions = []
