from django.contrib.auth.checks import (
    check_middleware,
    check_models_permissions,
    check_user_model,
)
from django.contrib.auth.middleware import (
    AuthenticationMiddleware,
    LoginRequiredMiddleware,
)
from django.contrib.auth.models import AbstractBaseUser
from django.contrib.sessions.middleware import SessionMiddleware
from django.core import checks
from django.db import models
from django.db.models import Q, UniqueConstraint
from django.test import SimpleTestCase, override_settings, override_system_checks
from django.test.utils import isolate_apps

from .models import CustomUserNonUniqueUsername


@isolate_apps("auth_tests", attr_name="apps")
@override_system_checks([check_user_model])
class UserModelChecksTests(SimpleTestCase):
    @override_settings(AUTH_USER_MODEL="auth_tests.CustomUserNonListRequiredFields")
    def test_required_fields_is_list(self):
        """REQUIRED_FIELDS should be a list."""

        class CustomUserNonListRequiredFields(AbstractBaseUser):
            username = models.CharField(max_length=30, unique=True)
            date_of_birth = models.DateField()

            USERNAME_FIELD = "username"
            REQUIRED_FIELDS = "date_of_birth"

        errors = checks.run_checks(app_configs=self.apps.get_app_configs())
        self.assertEqual(
            errors,
            [
                checks.Error(
                    "'REQUIRED_FIELDS' must be a list or tuple.",
                    obj=CustomUserNonListRequiredFields,
                    id="auth.E001",
                ),
            ],
        )

    @override_settings(AUTH_USER_MODEL="auth_tests.CustomUserBadRequiredFields")
    def test_username_not_in_required_fields(self):
        """USERNAME_FIELD should not appear in REQUIRED_FIELDS."""

        class CustomUserBadRequiredFields(AbstractBaseUser):
            username = models.CharField(max_length=30, unique=True)
            date_of_birth = models.DateField()

            USERNAME_FIELD = "username"
            REQUIRED_FIELDS = ["username", "date_of_birth"]

        errors = checks.run_checks(self.apps.get_app_configs())
        self.assertEqual(
            errors,
            [
                checks.Error(
                    "The field named as the 'USERNAME_FIELD' for a custom user model "
                    "must not be included in 'REQUIRED_FIELDS'.",
                    hint=(
                        "The 'USERNAME_FIELD' is currently set to 'username', you "
                        "should remove 'username' from the 'REQUIRED_FIELDS'."
                    ),
                    obj=CustomUserBadRequiredFields,
                    id="auth.E002",
                ),
            ],
        )

    @override_settings(AUTH_USER_MODEL="auth_tests.CustomUserNonUniqueUsername")
    def test_username_non_unique(self):
        """
        A non-unique USERNAME_FIELD raises an error only if the default
        authentication backend is used. Otherwise, a warning is raised.
        """
        errors = checks.run_checks()
        self.assertEqual(
            errors,
            [
                checks.Error(
                    "'CustomUserNonUniqueUsername.username' must be "
                    "unique because it is named as the 'USERNAME_FIELD'.",
                    obj=CustomUserNonUniqueUsername,
                    id="auth.E003",
                ),
            ],
        )
        with self.settings(AUTHENTICATION_BACKENDS=["my.custom.backend"]):
            errors = checks.run_checks()
            self.assertEqual(
                errors,
                [
                    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",
                    ),
                ],
            )

    @override_settings(AUTH_USER_MODEL="auth_tests.CustomUserPartiallyUnique")
    def test_username_partially_unique(self):
        class CustomUserPartiallyUnique(AbstractBaseUser):
            username = models.CharField(max_length=30)
            USERNAME_FIELD = "username"

            class Meta:
                constraints = [
                    UniqueConstraint(
                        fields=["username"],
                        name="partial_username_unique",
                        condition=Q(password__isnull=False),
                    ),
                ]

        errors = checks.run_checks(app_configs=self.apps.get_app_configs())
        self.assertEqual(
            errors,
            [
                checks.Error(
                    "'CustomUserPartiallyUnique.username' must be unique because "
                    "it is named as the 'USERNAME_FIELD'.",
                    obj=CustomUserPartiallyUnique,
                    id="auth.E003",
                ),
            ],
        )
        with self.settings(AUTHENTICATION_BACKENDS=["my.custom.backend"]):
            errors = checks.run_checks(app_configs=self.apps.get_app_configs())
            self.assertEqual(
                errors,
                [
                    checks.Warning(
                        "'CustomUserPartiallyUnique.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=CustomUserPartiallyUnique,
                        id="auth.W004",
                    ),
                ],
            )

    @override_settings(AUTH_USER_MODEL="auth_tests.CustomUserUniqueConstraint")
    def test_username_unique_with_model_constraint(self):
        class CustomUserUniqueConstraint(AbstractBaseUser):
            username = models.CharField(max_length=30)
            USERNAME_FIELD = "username"

            class Meta:
                constraints = [
                    UniqueConstraint(fields=["username"], name="username_unique"),
                ]

        self.assertEqual(checks.run_checks(app_configs=self.apps.get_app_configs()), [])
        with self.settings(AUTHENTICATION_BACKENDS=["my.custom.backend"]):
            errors = checks.run_checks(app_configs=self.apps.get_app_configs())
            self.assertEqual(errors, [])

    @override_settings(AUTH_USER_MODEL="auth_tests.BadUser")
    def test_is_anonymous_authenticated_methods(self):
        """
        <User Model>.is_anonymous/is_authenticated must not be methods.
        """

        class BadUser(AbstractBaseUser):
            username = models.CharField(max_length=30, unique=True)
            USERNAME_FIELD = "username"

            def is_anonymous(self):
                return True

            def is_authenticated(self):
                return True

        errors = checks.run_checks(app_configs=self.apps.get_app_configs())
        self.assertEqual(
            errors,
            [
                checks.Critical(
                    "%s.is_anonymous must be an attribute or property rather than "
                    "a method. Ignoring this is a security issue as anonymous "
                    "users will be treated as authenticated!" % BadUser,
                    obj=BadUser,
                    id="auth.C009",
                ),
                checks.Critical(
                    "%s.is_authenticated must be an attribute or property rather "
                    "than a method. Ignoring this is a security issue as anonymous "
                    "users will be treated as authenticated!" % BadUser,
                    obj=BadUser,
                    id="auth.C010",
                ),
            ],
        )

    @override_settings(AUTH_USER_MODEL="auth_tests.VulnerableStaticUser")
    def test_is_anonymous_authenticated_static_methods(self):
        """
        <User Model>.is_anonymous/is_authenticated must not be static methods.
        """

        class VulnerableStaticUser(AbstractBaseUser):
            username = models.CharField(max_length=30, unique=True)
            USERNAME_FIELD = "username"

            @staticmethod
            def is_anonymous():
                return False

            @staticmethod
            def is_authenticated():
                return False

        errors = checks.run_checks(app_configs=self.apps.get_app_configs())
        self.assertEqual(
            errors,
            [
                checks.Critical(
                    "%s.is_anonymous must be an attribute or property rather than "
                    "a method. Ignoring this is a security issue as anonymous "
                    "users will be treated as authenticated!" % VulnerableStaticUser,
                    obj=VulnerableStaticUser,
                    id="auth.C009",
                ),
                checks.Critical(
                    "%s.is_authenticated must be an attribute or property rather "
                    "than a method. Ignoring this is a security issue as anonymous "
                    "users will be treated as authenticated!" % VulnerableStaticUser,
                    obj=VulnerableStaticUser,
                    id="auth.C010",
                ),
            ],
        )


@isolate_apps("auth_tests", attr_name="apps")
@override_system_checks([check_models_permissions])
class ModelsPermissionsChecksTests(SimpleTestCase):
    def test_clashing_default_permissions(self):
        class Checked(models.Model):
            class Meta:
                permissions = [("change_checked", "Can edit permission (duplicate)")]

        errors = checks.run_checks(self.apps.get_app_configs())
        self.assertEqual(
            errors,
            [
                checks.Error(
                    "The permission codenamed 'change_checked' clashes with a builtin "
                    "permission for model 'auth_tests.Checked'.",
                    obj=Checked,
                    id="auth.E005",
                ),
            ],
        )

    def test_non_clashing_custom_permissions(self):
        class Checked(models.Model):
            class Meta:
                permissions = [
                    ("my_custom_permission", "Some permission"),
                    ("other_one", "Some other permission"),
                ]

        errors = checks.run_checks(self.apps.get_app_configs())
        self.assertEqual(errors, [])

    def test_clashing_custom_permissions(self):
        class Checked(models.Model):
            class Meta:
                permissions = [
                    ("my_custom_permission", "Some permission"),
                    ("other_one", "Some other permission"),
                    (
                        "my_custom_permission",
                        "Some permission with duplicate permission code",
                    ),
                ]

        errors = checks.run_checks(self.apps.get_app_configs())
        self.assertEqual(
            errors,
            [
                checks.Error(
                    "The permission codenamed 'my_custom_permission' is duplicated for "
                    "model 'auth_tests.Checked'.",
                    obj=Checked,
                    id="auth.E006",
                ),
            ],
        )

    def test_verbose_name_max_length(self):
        class Checked(models.Model):
            class Meta:
                verbose_name = (
                    "some ridiculously long verbose name that is out of control" * 5
                )

        errors = checks.run_checks(self.apps.get_app_configs())
        self.assertEqual(
            errors,
            [
                checks.Error(
                    "The verbose_name of model 'auth_tests.Checked' must be at most "
                    "244 characters for its builtin permission names to be at most 255 "
                    "characters.",
                    obj=Checked,
                    id="auth.E007",
                ),
            ],
        )

    def test_model_name_max_length(self):
        model_name = "X" * 94
        model = type(model_name, (models.Model,), {"__module__": self.__module__})
        errors = checks.run_checks(self.apps.get_app_configs())
        self.assertEqual(
            errors,
            [
                checks.Error(
                    "The name of model 'auth_tests.%s' must be at most 93 "
                    "characters for its builtin permission codenames to be at "
                    "most 100 characters." % model_name,
                    obj=model,
                    id="auth.E011",
                ),
            ],
        )

    def test_custom_permission_name_max_length(self):
        custom_permission_name = (
            "some ridiculously long verbose name that is out of control" * 5
        )

        class Checked(models.Model):
            class Meta:
                permissions = [
                    ("my_custom_permission", custom_permission_name),
                ]

        errors = checks.run_checks(self.apps.get_app_configs())
        self.assertEqual(
            errors,
            [
                checks.Error(
                    "The permission named '%s' of model 'auth_tests.Checked' is longer "
                    "than 255 characters." % custom_permission_name,
                    obj=Checked,
                    id="auth.E008",
                ),
            ],
        )

    def test_custom_permission_codename_max_length(self):
        custom_permission_codename = "x" * 101

        class Checked(models.Model):
            class Meta:
                permissions = [
                    (custom_permission_codename, "Custom permission"),
                ]

        errors = checks.run_checks(self.apps.get_app_configs())
        self.assertEqual(
            errors,
            [
                checks.Error(
                    "The permission codenamed '%s' of model 'auth_tests.Checked' "
                    "is longer than 100 characters." % custom_permission_codename,
                    obj=Checked,
                    id="auth.E012",
                ),
            ],
        )

    def test_empty_default_permissions(self):
        class Checked(models.Model):
            class Meta:
                default_permissions = ()

        self.assertEqual(checks.run_checks(self.apps.get_app_configs()), [])


class LoginRequiredMiddlewareSubclass(LoginRequiredMiddleware):
    redirect_field_name = "redirect_to"


class AuthenticationMiddlewareSubclass(AuthenticationMiddleware):
    pass


class SessionMiddlewareSubclass(SessionMiddleware):
    pass


@override_system_checks([check_middleware])
class MiddlewareChecksTests(SimpleTestCase):
    @override_settings(
        MIDDLEWARE=[
            "auth_tests.test_checks.SessionMiddlewareSubclass",
            "auth_tests.test_checks.AuthenticationMiddlewareSubclass",
            "auth_tests.test_checks.LoginRequiredMiddlewareSubclass",
        ]
    )
    def test_middleware_subclasses(self):
        errors = checks.run_checks()
        self.assertEqual(errors, [])

    @override_settings(
        MIDDLEWARE=[
            "auth_tests.test_checks",
            "auth_tests.test_checks.NotExist",
        ]
    )
    def test_invalid_middleware_skipped(self):
        errors = checks.run_checks()
        self.assertEqual(errors, [])

    @override_settings(
        MIDDLEWARE=[
            "django.contrib.does.not.Exist",
            "django.contrib.sessions.middleware.SessionMiddleware",
            "django.contrib.auth.middleware.AuthenticationMiddleware",
            "django.contrib.auth.middleware.LoginRequiredMiddleware",
        ]
    )
    def test_check_ignores_import_error_in_middleware(self):
        errors = checks.run_checks()
        self.assertEqual(errors, [])

    @override_settings(
        MIDDLEWARE=[
            "django.contrib.sessions.middleware.SessionMiddleware",
            "django.contrib.auth.middleware.AuthenticationMiddleware",
            "django.contrib.auth.middleware.LoginRequiredMiddleware",
        ]
    )
    def test_correct_order_with_login_required_middleware(self):
        errors = checks.run_checks()
        self.assertEqual(errors, [])

    @override_settings(
        MIDDLEWARE=[
            "django.contrib.auth.middleware.LoginRequiredMiddleware",
            "django.contrib.auth.middleware.AuthenticationMiddleware",
            "django.contrib.sessions.middleware.SessionMiddleware",
        ]
    )
    def test_incorrect_order_with_login_required_middleware(self):
        errors = checks.run_checks()
        self.assertEqual(
            errors,
            [
                checks.Error(
                    "In order to use django.contrib.auth.middleware."
                    "LoginRequiredMiddleware, django.contrib.auth.middleware."
                    "AuthenticationMiddleware must be defined before it in MIDDLEWARE.",
                    id="auth.E013",
                )
            ],
        )

    @override_settings(
        MIDDLEWARE=[
            "django.contrib.auth.middleware.LoginRequiredMiddleware",
        ]
    )
    def test_missing_authentication_with_login_required_middleware(self):
        errors = checks.run_checks()
        self.assertEqual(
            errors,
            [
                checks.Error(
                    "In order to use django.contrib.auth.middleware."
                    "LoginRequiredMiddleware, django.contrib.auth.middleware."
                    "AuthenticationMiddleware must be defined before it in MIDDLEWARE.",
                    id="auth.E013",
                )
            ],
        )
