from django.core.exceptions import ImproperlyConfigured
from django.core.paginator import Page, Paginator
from django.db import models
from django.forms import fields, BaseForm, Form, ModelForm
from django.http import Http404
from django.test import RequestFactory, TestCase
from vanilla import *
import types
import warnings


class Example(models.Model):
    text = models.CharField(max_length=10)

    class Meta:
        ordering = ('id',)


class ExampleForm(Form):
    text = fields.CharField(max_length=10)


class InstanceOf(object):
    """
    We use this sentinal object together with our 'assertContext' helper method.

    Used to ensure that a particular context value is an object of the given
    type, without requiring a specific fixed value.  Useful for form context,
    and other complex instances.
    """
    def __init__(self, expected_type):
        self.expected_type = expected_type


def create_instance(text=None, quantity=1):
    for idx in range(quantity):
        text = text or ('example %d' % idx)
        Example.objects.create(text=text)


class BaseTestCase(TestCase):
    def setUp(self):
        self.factory = RequestFactory()
        super(BaseTestCase, self).setUp()

    def assertFormError(self, response, form, field, errors, msg_prefix=''):
        # Hack to get around the fact that we're using request factory,
        # instead of the full test client.
        response.context = response.context_data
        return super(BaseTestCase, self).assertFormError(response, form, field, errors, msg_prefix)

    def assertContext(self, response, expected):
        # Ensure the keys all match.
        # Note that this style ensures we get nice descriptive failures.
        for key in expected.keys():
            self.assertTrue(key in response.context_data,
                "context missing key '%s'" % key)
        for key in response.context_data.keys():
            self.assertTrue(key in expected,
                "context contains unexpected key '%s'" % key)

        # Ensure all the values match.
        for key, val in response.context_data.items():
            expected_val = expected[key]
            if isinstance(val, (models.query.QuerySet)):
                val = list(val)
            if isinstance(expected_val, models.query.QuerySet):
                expected_val = list(expected_val)

            if isinstance(expected_val, InstanceOf):
                self.assertTrue(isinstance(val, expected_val.expected_type),
                    "context['%s'] contained type '%s', but expected type '%s'"
                    % (key, type(val), expected_val.expected_type))
            else:
                self.assertEqual(val, expected_val,
                    "context['%s'] contained '%s', but expected '%s'" %
                    (key, val, expected_val))

    def get(self, view, *args, **kwargs):
        request = self.factory.get('/')
        return view(request, *args, **kwargs)

    def post(self, view, *args, **kwargs):
        data = kwargs.pop('data', {})
        request = self.factory.post('/', data=data)
        return view(request, *args, **kwargs)


class TestDetail(BaseTestCase):
    def test_detail(self):
        create_instance(quantity=3)
        pk = Example.objects.all()[0].pk
        view = DetailView.as_view(model=Example)
        response = self.get(view, pk=pk)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.template_name, ['vanilla/example_detail.html'])
        self.assertContext(response, {
            'object': Example.objects.get(pk=pk),
            'example': Example.objects.get(pk=pk),
            'view': InstanceOf(View)
        })

    def test_detail_not_found(self):
        create_instance(quantity=3)
        view = DetailView.as_view(model=Example)
        self.assertRaises(Http404, self.get, view, pk=999)

    def test_detail_misconfigured_urlconf(self):
        # If we don't provide 'pk' in the URL conf,
        # we should expect an ImproperlyConfigured exception.
        create_instance(quantity=3)
        view = DetailView.as_view(model=Example)
        self.assertRaises(ImproperlyConfigured, self.get, view, slug=999)

    def test_detail_misconfigured_template_name(self):
        # If don't provide 'model' or 'template_name',
        # we should expect an ImproperlyConfigured exception.
        create_instance(quantity=3)
        pk = Example.objects.all()[0].pk
        view = DetailView.as_view(queryset=Example.objects.all())
        self.assertRaises(ImproperlyConfigured, self.get, view, pk=pk)

    def test_detail_misconfigured_queryset(self):
        # If don't provide 'model' or 'queryset',
        # we should expect an ImproperlyConfigured exception.
        create_instance(quantity=3)
        pk = Example.objects.all()[0].pk
        view = DetailView.as_view(template_name='example.html')
        self.assertRaises(ImproperlyConfigured, self.get, view, pk=pk)

    def test_detail_missing_context_object_name(self):
        # If don't provide 'model' or 'context_object_name',
        # then the context will only contain the 'object' key.
        create_instance(quantity=3)
        pk = Example.objects.all()[0].pk
        view = DetailView.as_view(queryset=Example.objects.all(), template_name='example.html')
        response = self.get(view, pk=pk)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.template_name, ['example.html'])
        self.assertContext(response, {
            'object': Example.objects.get(pk=pk),
            'view': InstanceOf(View)
        })


class TestList(BaseTestCase):
    def test_list(self):
        create_instance(quantity=3)
        view = ListView.as_view(model=Example)
        response = self.get(view)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.template_name, ['vanilla/example_list.html'])
        self.assertContext(response, {
            'object_list': Example.objects.all(),
            'example_list': Example.objects.all(),
            'view': InstanceOf(View),
            'page_obj': None,
            'paginator': None,
            'is_paginated': False
        })

    def test_empty_list(self):
        view = ListView.as_view(model=Example)
        response = self.get(view)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.template_name, ['vanilla/example_list.html'])
        self.assertContext(response, {
            'object_list': [],
            'example_list': [],
            'view': InstanceOf(View),
            'page_obj': None,
            'paginator': None,
            'is_paginated': False
        })

    def test_empty_list_not_found(self):
        view = ListView.as_view(model=Example, allow_empty=False)
        self.assertRaises(Http404, self.get, view, pk=999)

    def test_paginated_list(self):
        create_instance(quantity=30)
        view = ListView.as_view(model=Example, paginate_by=10)
        response = self.get(view)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.template_name, ['vanilla/example_list.html'])
        self.assertContext(response, {
            'object_list': Example.objects.all()[:10],
            'example_list': Example.objects.all()[:10],
            'view': InstanceOf(View),
            'page_obj': InstanceOf(Page),
            'paginator': InstanceOf(Paginator),
            'is_paginated': True
        })

    def test_paginated_list_valid_page_specified(self):
        create_instance(quantity=30)
        view = ListView.as_view(model=Example, paginate_by=10)
        response = self.get(view, page=2)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.template_name, ['vanilla/example_list.html'])
        self.assertContext(response, {
            'object_list': Example.objects.all()[10:20],
            'example_list': Example.objects.all()[10:20],
            'view': InstanceOf(View),
            'page_obj': InstanceOf(Page),
            'paginator': InstanceOf(Paginator),
            'is_paginated': True
        })

    def test_paginated_list_last_page_specified(self):
        create_instance(quantity=30)
        view = ListView.as_view(model=Example, paginate_by=10)
        response = self.get(view, page='last')

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.template_name, ['vanilla/example_list.html'])
        self.assertContext(response, {
            'object_list': Example.objects.all()[20:],
            'example_list': Example.objects.all()[20:],
            'view': InstanceOf(View),
            'page_obj': InstanceOf(Page),
            'paginator': InstanceOf(Paginator),
            'is_paginated': True
        })

    def test_paginated_list_invalid_page_specified(self):
        create_instance(quantity=30)
        view = ListView.as_view(model=Example, paginate_by=10)
        self.assertRaises(Http404, self.get, view, page=999)

    def test_paginated_list_non_integer_page_specified(self):
        create_instance(quantity=30)
        view = ListView.as_view(model=Example, paginate_by=10)
        self.assertRaises(Http404, self.get, view, page='null')


class TestCreate(BaseTestCase):
    def test_create(self):
        view = CreateView.as_view(model=Example, fields=('text',), success_url='/success/')
        response = self.post(view, data={'text': 'example'})

        self.assertEqual(response.status_code, 302)
        self.assertEqual(response['location'], '/success/')
        self.assertEqual(Example.objects.count(), 1)
        self.assertEqual(Example.objects.get().text, 'example')

    def test_create_failed(self):
        view = CreateView.as_view(model=Example, fields=('text',), success_url='/success/')
        response = self.post(view, data={'text': 'example' * 100})

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.template_name, ['vanilla/example_form.html'])
        self.assertFormError(response, 'form', 'text', ['Ensure this value has at most 10 characters (it has 700).'])
        self.assertContext(response, {
            'form': InstanceOf(BaseForm),
            'view': InstanceOf(View)
        })
        self.assertFalse(Example.objects.exists())

    def test_create_preview(self):
        view = CreateView.as_view(model=Example, fields=('text',), success_url='/success/')
        response = self.get(view)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.template_name, ['vanilla/example_form.html'])
        self.assertContext(response, {
            'form': InstanceOf(BaseForm),
            'view': InstanceOf(View)
        })
        self.assertFalse(Example.objects.exists())

    def test_create_no_success_url(self):
        view = CreateView.as_view(model=Example, fields=('text',))
        self.assertRaises(ImproperlyConfigured, self.post, view, data={'text': 'example'})

    def test_create_misconfigured_form_class(self):
        # If don't provide 'model' or 'form_class',
        # we should expect an ImproperlyConfigured exception.
        view = CreateView.as_view(
            queryset=Example.objects.all(),
            template_name='example.html',
            success_url='/success/'
        )
        self.assertRaises(ImproperlyConfigured, self.post, view, data={'text': 'example'})

    def test_create_create_no_fields(self):
        # If we don't provide `.fields` then expect a `PendingDeprecation` warning.
        view = CreateView.as_view(model=Example, success_url='/success/')
        with warnings.catch_warnings(record=True) as warned:
            warnings.simplefilter("always")
            self.post(view, data={'text': 'example'})
            self.assertTrue(bool(warned))

class TestUpdate(BaseTestCase):
    def test_update(self):
        create_instance(quantity=3)
        pk = Example.objects.all()[0].pk
        view = UpdateView.as_view(model=Example, fields=('text',), success_url='/success/')
        response = self.post(view, pk=pk, data={'text': 'example'})

        self.assertEqual(response.status_code, 302)
        self.assertEqual(response['location'], '/success/')
        self.assertEqual(Example.objects.count(), 3)
        self.assertEqual(Example.objects.get(pk=pk).text, 'example')

    def test_update_failed(self):
        create_instance(quantity=3)
        pk = Example.objects.all()[0].pk
        original_text = Example.objects.all()[0].text
        view = UpdateView.as_view(model=Example, fields=('text',), success_url='/success/')
        response = self.post(view, pk=pk, data={'text': 'example' * 100})

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.template_name, ['vanilla/example_form.html'])
        self.assertFormError(response, 'form', 'text', ['Ensure this value has at most 10 characters (it has 700).'])
        self.assertContext(response, {
            'object': Example.objects.get(pk=pk),
            'example': Example.objects.get(pk=pk),
            'form': InstanceOf(BaseForm),
            'view': InstanceOf(View)
        })
        self.assertEqual(Example.objects.count(), 3)
        self.assertEqual(Example.objects.get(pk=pk).text, original_text)

    def test_update_preview(self):
        create_instance(quantity=3)
        pk = Example.objects.all()[0].pk
        view = UpdateView.as_view(model=Example, fields=('text',), success_url='/success/')
        response = self.get(view, pk=pk)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.template_name, ['vanilla/example_form.html'])
        self.assertContext(response, {
            'object': Example.objects.get(pk=pk),
            'example': Example.objects.get(pk=pk),
            'form': InstanceOf(BaseForm),
            'view': InstanceOf(View)
        })
        self.assertEqual(Example.objects.count(), 3)

    def test_update_no_success_url(self):
        create_instance(quantity=3)
        pk = Example.objects.all()[0].pk
        view = UpdateView.as_view(model=Example, fields=('text',))
        self.assertRaises(ImproperlyConfigured, self.post, view, pk=pk, data={'text': 'example'})

    def test_update_no_fields(self):
        # If we don't provide `.fields` then expect a `PendingDeprecation` warning.
        create_instance(quantity=3)
        pk = Example.objects.all()[0].pk
        view = UpdateView.as_view(model=Example, success_url='/success/')
        with warnings.catch_warnings(record=True) as warned:
            warnings.simplefilter("always")
            self.post(view, pk=pk, data={'text': 'example'})
            self.assertTrue(bool(warned))

class TestDelete(BaseTestCase):
    def test_delete(self):
        create_instance(quantity=3)
        pk = Example.objects.all()[0].pk
        view = DeleteView.as_view(model=Example, success_url='/success/')
        response = self.post(view, pk=pk)

        self.assertEqual(response.status_code, 302)
        self.assertEqual(response['location'], '/success/')
        self.assertEqual(Example.objects.count(), 2)
        self.assertFalse(Example.objects.filter(pk=pk).exists())

    def test_delete_not_found(self):
        create_instance(quantity=3)
        view = DeleteView.as_view(model=Example, success_url='/success/')
        self.assertRaises(Http404, self.get, view, pk=999)

    def test_delete_preview(self):
        create_instance(quantity=3)
        pk = Example.objects.all()[0].pk
        view = DeleteView.as_view(model=Example, success_url='/success/')
        response = self.get(view, pk=pk)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.template_name, ['vanilla/example_confirm_delete.html'])
        self.assertContext(response, {
            'object': Example.objects.get(pk=pk),
            'example': Example.objects.get(pk=pk),
            'view': InstanceOf(View)
        })
        self.assertEqual(Example.objects.count(), 3)

    def test_delete_no_success_url(self):
        create_instance(quantity=3)
        pk = Example.objects.all()[0].pk
        view = DeleteView.as_view(model=Example)
        self.assertRaises(ImproperlyConfigured, self.post, view, pk=pk)


class TestAttributeOverrides(BaseTestCase):
    def test_template_name_override(self):
        create_instance(quantity=3)
        pk = Example.objects.all()[0].pk
        view = DetailView.as_view(model=Example, template_name='example.html')
        response = self.get(view, pk=pk)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.template_name, ['example.html'])
        self.assertContext(response, {
            'object': Example.objects.get(pk=pk),
            'example': Example.objects.get(pk=pk),
            'view': InstanceOf(View)
        })

    def test_template_name_suffix_override(self):
        create_instance(quantity=3)
        pk = Example.objects.all()[0].pk
        view = DetailView.as_view(model=Example, template_name_suffix='_suffix')
        response = self.get(view, pk=pk)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.template_name, ['vanilla/example_suffix.html'])
        self.assertContext(response, {
            'object': Example.objects.get(pk=pk),
            'example': Example.objects.get(pk=pk),
            'view': InstanceOf(View)
        })

    def test_context_object_name_override(self):
        create_instance(quantity=3)
        pk = Example.objects.all()[0].pk
        view = DetailView.as_view(model=Example, context_object_name='current')
        response = self.get(view, pk=pk)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.template_name, ['vanilla/example_detail.html'])
        self.assertContext(response, {
            'object': Example.objects.get(pk=pk),
            'current': Example.objects.get(pk=pk),
            'view': InstanceOf(View)
        })

    def test_form_class_override(self):
        class CustomForm(ModelForm):
            class Meta:
                model = Example
        view = CreateView.as_view(model=Example, success_url='/success/', form_class=CustomForm)
        response = self.get(view)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.template_name, ['vanilla/example_form.html'])
        self.assertContext(response, {
            'form': InstanceOf(CustomForm),
            'view': InstanceOf(View)
        })
        self.assertFalse(Example.objects.exists())

    def test_queryset_override(self):
        create_instance(text='abc', quantity=3)
        create_instance(text='def', quantity=3)
        view = ListView.as_view(model=Example, queryset=Example.objects.filter(text='abc'))
        response = self.get(view)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.template_name, ['vanilla/example_list.html'])
        self.assertContext(response, {
            'object_list': Example.objects.filter(text='abc'),
            'example_list': Example.objects.filter(text='abc'),
            'view': InstanceOf(View),
            'page_obj': None,
            'paginator': None,
            'is_paginated': False
        })


class TestTemplateView(BaseTestCase):
    def test_template_view(self):
        view = TemplateView.as_view(template_name='example.html')
        response = self.get(view)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.template_name, ['example.html'])
        self.assertContext(response, {
            'view': InstanceOf(View)
        })

    def test_misconfigured_template_view(self):
        # A template view with no `template_name` is improperly configured.
        view = TemplateView.as_view()
        self.assertRaises(ImproperlyConfigured, self.get, view)


class TestFormView(BaseTestCase):
    def test_form_success(self):
        view = FormView.as_view(
            form_class=ExampleForm,
            success_url='/success/',
            template_name='example.html'
        )
        response = self.post(view, data={'text': 'example'})

        self.assertEqual(response.status_code, 302)
        self.assertEqual(response['location'], '/success/')

    def test_form_failure(self):
        view = FormView.as_view(
            form_class=ExampleForm,
            success_url='/success/',
            template_name='example.html'
        )
        response = self.post(view, data={'text': 'example' * 100})

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.template_name, ['example.html'])
        self.assertFormError(response, 'form', 'text', ['Ensure this value has at most 10 characters (it has 700).'])
        self.assertContext(response, {
            'form': InstanceOf(BaseForm),
            'view': InstanceOf(View)
        })

    def test_form_preview(self):
        view = FormView.as_view(
            form_class=ExampleForm,
            success_url='/success/',
            template_name='example.html'
        )
        response = self.get(view)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.template_name, ['example.html'])
        self.assertContext(response, {
            'form': InstanceOf(BaseForm),
            'view': InstanceOf(View)
        })

    def test_misconfigured_form_view_no_form_class(self):
        # A template view with no `form_class` is improperly configured.
        view = FormView.as_view(
            success_url='/success/',
            template_name='example.html'
        )
        self.assertRaises(ImproperlyConfigured, self.get, view)

    def test_misconfigured_form_view_no_success_url(self):
        # A template view with no `success_url` is improperly configured.
        view = FormView.as_view(
            form_class=ExampleForm,
            template_name='example.html'
        )
        self.assertRaises(ImproperlyConfigured, self.post, view, data={'text': 'example'})
