# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.utils.six import StringIO
import sys

from django.apps import apps
from django.conf import settings
from django.core import checks
from django.core.checks import Error, Warning
from django.core.checks.model_checks import check_all_models
from django.core.checks.registry import CheckRegistry
from django.core.checks.compatibility.django_1_6_0 import check_1_6_compatibility
from django.core.checks.compatibility.django_1_7_0 import check_1_7_compatibility
from django.core.management.base import CommandError
from django.core.management import call_command
from django.db import models
from django.db.models.fields import NOT_PROVIDED
from django.test import TestCase
from django.test.utils import override_settings, override_system_checks
from django.utils.encoding import force_text

from .models import SimpleModel, Book


class DummyObj(object):
    def __repr__(self):
        return "obj"


class SystemCheckFrameworkTests(TestCase):

    def test_register_and_run_checks(self):
        calls = [0]

        registry = CheckRegistry()

        @registry.register()
        def f(**kwargs):
            calls[0] += 1
            return [1, 2, 3]
        errors = registry.run_checks()
        self.assertEqual(errors, [1, 2, 3])
        self.assertEqual(calls[0], 1)


class MessageTests(TestCase):

    def test_printing(self):
        e = Error("Message", hint="Hint", obj=DummyObj())
        expected = "obj: Message\n\tHINT: Hint"
        self.assertEqual(force_text(e), expected)

    def test_printing_no_hint(self):
        e = Error("Message", hint=None, obj=DummyObj())
        expected = "obj: Message"
        self.assertEqual(force_text(e), expected)

    def test_printing_no_object(self):
        e = Error("Message", hint="Hint", obj=None)
        expected = "?: Message\n\tHINT: Hint"
        self.assertEqual(force_text(e), expected)

    def test_printing_with_given_id(self):
        e = Error("Message", hint="Hint", obj=DummyObj(), id="ID")
        expected = "obj: (ID) Message\n\tHINT: Hint"
        self.assertEqual(force_text(e), expected)

    def test_printing_field_error(self):
        field = SimpleModel._meta.get_field('field')
        e = Error("Error", hint=None, obj=field)
        expected = "check_framework.SimpleModel.field: Error"
        self.assertEqual(force_text(e), expected)

    def test_printing_model_error(self):
        e = Error("Error", hint=None, obj=SimpleModel)
        expected = "check_framework.SimpleModel: Error"
        self.assertEqual(force_text(e), expected)

    def test_printing_manager_error(self):
        manager = SimpleModel.manager
        e = Error("Error", hint=None, obj=manager)
        expected = "check_framework.SimpleModel.manager: Error"
        self.assertEqual(force_text(e), expected)


class Django_1_6_0_CompatibilityChecks(TestCase):

    @override_settings(TEST_RUNNER='django.test.runner.DiscoverRunner')
    def test_test_runner_new_default(self):
        errors = check_1_6_compatibility()
        self.assertEqual(errors, [])

    @override_settings(TEST_RUNNER='myapp.test.CustomRunner')
    def test_test_runner_overriden(self):
        errors = check_1_6_compatibility()
        self.assertEqual(errors, [])

    def test_test_runner_not_set_explicitly(self):
        # If TEST_RUNNER was set explicitly, temporarily pretend it wasn't
        test_runner_overridden = False
        if 'TEST_RUNNER' in settings._wrapped._explicit_settings:
            test_runner_overridden = True
            settings._wrapped._explicit_settings.remove('TEST_RUNNER')
        # We remove some settings to make this look like a project generated under Django 1.5.
        settings._wrapped._explicit_settings.add('MANAGERS')
        settings._wrapped._explicit_settings.add('ADMINS')
        try:
            errors = check_1_6_compatibility()
            expected = [
                checks.Warning(
                    "Some project unittests may not execute as expected.",
                    hint=("Django 1.6 introduced a new default test runner. It looks like "
                          "this project was generated using Django 1.5 or earlier. You should "
                          "ensure your tests are all running & behaving as expected. See "
                          "https://docs.djangoproject.com/en/dev/releases/1.6/#new-test-runner "
                          "for more information."),
                    obj=None,
                    id='1_6.W001',
                )
            ]
            self.assertEqual(errors, expected)
        finally:
            # Restore settings value
            if test_runner_overridden:
                settings._wrapped._explicit_settings.add('TEST_RUNNER')
            settings._wrapped._explicit_settings.remove('MANAGERS')
            settings._wrapped._explicit_settings.remove('ADMINS')

    def test_boolean_field_default_value(self):
        with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'):
            # We patch the field's default value to trigger the warning
            boolean_field = Book._meta.get_field('is_published')
            old_default = boolean_field.default
            try:
                boolean_field.default = NOT_PROVIDED
                errors = check_1_6_compatibility()
                expected = [
                    checks.Warning(
                        'BooleanField does not have a default value.',
                        hint=('Django 1.6 changed the default value of BooleanField from False to None. '
                              'See https://docs.djangoproject.com/en/1.6/ref/models/fields/#booleanfield '
                              'for more information.'),
                        obj=boolean_field,
                        id='1_6.W002',
                    )
                ]
                self.assertEqual(errors, expected)
            finally:
                # Restore the ``default``
                boolean_field.default = old_default


class Django_1_7_0_CompatibilityChecks(TestCase):

    @override_settings(MIDDLEWARE_CLASSES=('django.contrib.sessions.middleware.SessionMiddleware',))
    def test_middleware_classes_overridden(self):
        errors = check_1_7_compatibility()
        self.assertEqual(errors, [])

    def test_middleware_classes_not_set_explicitly(self):
        # If MIDDLEWARE_CLASSES was set explicitly, temporarily pretend it wasn't
        middleware_classes_overridden = False
        if 'MIDDLEWARE_CLASSES' in settings._wrapped._explicit_settings:
            middleware_classes_overridden = True
            settings._wrapped._explicit_settings.remove('MIDDLEWARE_CLASSES')
        try:
            errors = check_1_7_compatibility()
            expected = [
                checks.Warning(
                    "MIDDLEWARE_CLASSES is not set.",
                    hint=("Django 1.7 changed the global defaults for the MIDDLEWARE_CLASSES. "
                          "django.contrib.sessions.middleware.SessionMiddleware, "
                          "django.contrib.auth.middleware.AuthenticationMiddleware, and "
                          "django.contrib.messages.middleware.MessageMiddleware were removed from the defaults. "
                          "If your project needs these middleware then you should configure this setting."),
                    obj=None,
                    id='1_7.W001',
                )
            ]
            self.assertEqual(errors, expected)
        finally:
            # Restore settings value
            if middleware_classes_overridden:
                settings._wrapped._explicit_settings.add('MIDDLEWARE_CLASSES')


def simple_system_check(**kwargs):
    simple_system_check.kwargs = kwargs
    return []


def tagged_system_check(**kwargs):
    tagged_system_check.kwargs = kwargs
    return []
tagged_system_check.tags = ['simpletag']


class CheckCommandTests(TestCase):

    def setUp(self):
        simple_system_check.kwargs = None
        tagged_system_check.kwargs = None
        self.old_stdout, self.old_stderr = sys.stdout, sys.stderr
        sys.stdout, sys.stderr = StringIO(), StringIO()

    def tearDown(self):
        sys.stdout, sys.stderr = self.old_stdout, self.old_stderr

    @override_system_checks([simple_system_check, tagged_system_check])
    def test_simple_call(self):
        call_command('check')
        self.assertEqual(simple_system_check.kwargs, {'app_configs': None})
        self.assertEqual(tagged_system_check.kwargs, {'app_configs': None})

    @override_system_checks([simple_system_check, tagged_system_check])
    def test_given_app(self):
        call_command('check', 'auth', 'admin')
        auth_config = apps.get_app_config('auth')
        admin_config = apps.get_app_config('admin')
        self.assertEqual(simple_system_check.kwargs, {'app_configs': [auth_config, admin_config]})
        self.assertEqual(tagged_system_check.kwargs, {'app_configs': [auth_config, admin_config]})

    @override_system_checks([simple_system_check, tagged_system_check])
    def test_given_tag(self):
        call_command('check', tags=['simpletag'])
        self.assertEqual(simple_system_check.kwargs, None)
        self.assertEqual(tagged_system_check.kwargs, {'app_configs': None})

    @override_system_checks([simple_system_check, tagged_system_check])
    def test_invalid_tag(self):
        self.assertRaises(CommandError, call_command, 'check', tags=['missingtag'])

    @override_system_checks([simple_system_check])
    def test_list_tags_empty(self):
        call_command('check', list_tags=True)
        self.assertEqual('\n', sys.stdout.getvalue())

    @override_system_checks([tagged_system_check])
    def test_list_tags(self):
        call_command('check', list_tags=True)
        self.assertEqual('simpletag\n', sys.stdout.getvalue())


def custom_error_system_check(app_configs, **kwargs):
    return [
        Error(
            'Error',
            hint=None,
            id='myerrorcheck.E001',
        )
    ]


def custom_warning_system_check(app_configs, **kwargs):
    return [
        Warning(
            'Warning',
            hint=None,
            id='mywarningcheck.E001',
        )
    ]


class SilencingCheckTests(TestCase):

    def setUp(self):
        self.old_stdout, self.old_stderr = sys.stdout, sys.stderr
        self.stdout, self.stderr = StringIO(), StringIO()
        sys.stdout, sys.stderr = self.stdout, self.stderr

    def tearDown(self):
        sys.stdout, sys.stderr = self.old_stdout, self.old_stderr

    @override_settings(SILENCED_SYSTEM_CHECKS=['myerrorcheck.E001'])
    @override_system_checks([custom_error_system_check])
    def test_silenced_error(self):
        out = StringIO()
        err = StringIO()
        try:
            call_command('check', stdout=out, stderr=err)
        except CommandError:
            self.fail("The mycheck.E001 check should be silenced.")
        self.assertEqual(out.getvalue(), '')
        self.assertEqual(
            err.getvalue(),
            'System check identified some issues:\n\n'
            'ERRORS:\n'
            '?: (myerrorcheck.E001) Error\n\n'
            'System check identified 1 issue (0 silenced).\n'
        )

    @override_settings(SILENCED_SYSTEM_CHECKS=['mywarningcheck.E001'])
    @override_system_checks([custom_warning_system_check])
    def test_silenced_warning(self):
        out = StringIO()
        err = StringIO()
        try:
            call_command('check', stdout=out, stderr=err)
        except CommandError:
            self.fail("The mycheck.E001 check should be silenced.")

        self.assertEqual(out.getvalue(), 'System check identified no issues (1 silenced).\n')
        self.assertEqual(err.getvalue(), '')


class CheckFrameworkReservedNamesTests(TestCase):

    def setUp(self):
        self.current_models = apps.all_models[__package__]
        self.saved_models = set(self.current_models)

    def tearDown(self):
        for model in (set(self.current_models) - self.saved_models):
            del self.current_models[model]
        apps.clear_cache()

    @override_settings(SILENCED_SYSTEM_CHECKS=['models.E020'])
    def test_model_check_method_not_shadowed(self):
        class ModelWithAttributeCalledCheck(models.Model):
            check = 42

        class ModelWithFieldCalledCheck(models.Model):
            check = models.IntegerField()

        class ModelWithRelatedManagerCalledCheck(models.Model):
            pass

        class ModelWithDescriptorCalledCheck(models.Model):
            check = models.ForeignKey(ModelWithRelatedManagerCalledCheck)
            article = models.ForeignKey(ModelWithRelatedManagerCalledCheck, related_name='check')

        expected = [
            Error(
                "The 'ModelWithAttributeCalledCheck.check()' class method is "
                "currently overridden by 42.",
                hint=None,
                obj=ModelWithAttributeCalledCheck,
                id='models.E020'
            ),
            Error(
                "The 'ModelWithRelatedManagerCalledCheck.check()' class method is "
                "currently overridden by %r." % ModelWithRelatedManagerCalledCheck.check,
                hint=None,
                obj=ModelWithRelatedManagerCalledCheck,
                id='models.E020'
            ),
            Error(
                "The 'ModelWithDescriptorCalledCheck.check()' class method is "
                "currently overridden by %r." % ModelWithDescriptorCalledCheck.check,
                hint=None,
                obj=ModelWithDescriptorCalledCheck,
                id='models.E020'
            ),
        ]

        self.assertEqual(check_all_models(), expected)
