import sys
if sys.version_info < (2, 7):
    # In python < 2.7 unittest doesn't have expectedFailure
    from django.utils import unittest
else:
    # django.utils.unittest became deprecated in Django 1.7
    import unittest  # NOQA

import mock

import django
from django import forms
from django.core.exceptions import ImproperlyConfigured
from django.conf import settings
from django.db import models
from django.test import TestCase
from django.template.loader import render_to_string
from django.http import QueryDict

from betterforms.changelist import (
    BaseChangeListForm, SearchForm, SortForm, HeaderSet, Header, BoundHeader
)
from betterforms.forms import (
    BetterForm, BetterModelForm, Fieldset, BoundFieldset, flatten_to_tuple,
)


class TestUtils(TestCase):
    def test_flatten(self):
        fields1 = ('a', 'b', 'c')
        self.assertTupleEqual(flatten_to_tuple(fields1), fields1)

        fields2 = ('a', ('b', 'c'), 'd')
        self.assertTupleEqual(flatten_to_tuple(fields2), ('a', 'b', 'c', 'd'))

        fields3 = ('a', ('b', 'c'), 'd', ('e', ('f', 'g', ('h',)), 'i'))
        self.assertTupleEqual(flatten_to_tuple(fields3), ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'))


class TestFieldSets(TestCase):
    def test_basic_fieldset(self):
        fields = ('a', 'b', 'c')
        fieldset = Fieldset('the_name', fields=fields)
        self.assertEqual(fieldset.name, 'the_name')
        self.assertTupleEqual(fields, fieldset.fields)

    def test_nested_fieldset(self):
        fields = ('a', ('b', 'c'), 'd')
        fieldset = Fieldset('the_name', fields=fields)
        self.assertTupleEqual(flatten_to_tuple(fields), fieldset.fields)
        iterated = tuple(iter(fieldset))
        self.assertEqual(iterated[0], 'a')
        self.assertTupleEqual(iterated[1].fields, ('b', 'c'))
        self.assertEqual(iterated[2], 'd')

    def test_named_nested_fieldset(self):
        fields = ('a', ('sub_name', {'fields': ('b', 'c')}), 'd')
        fieldset = Fieldset('the_name', fields=fields)
        self.assertTupleEqual(fieldset.fields, ('a', 'b', 'c', 'd'))
        fieldsets = tuple(iter(fieldset))
        self.assertEqual(fieldsets[0], 'a')
        self.assertTupleEqual(fieldsets[1].fields, ('b', 'c'))
        self.assertEqual(fieldsets[1].name, 'sub_name')
        self.assertEqual(fieldsets[2], 'd')

    def test_deeply_nested_fieldsets(self):
        fields = ('a', ('b', 'c'), 'd', ('e', ('f', 'g', ('h',)), 'i'))
        fieldset = Fieldset('the_name', fields=fields)
        self.assertTupleEqual(flatten_to_tuple(fields), fieldset.fields)

    def test_fieldset_as_row_item(self):
        fields = ('a', Fieldset('sub_name', fields=['b', 'c']))
        fieldset = Fieldset('the_name', fields=fields)
        self.assertTupleEqual(fieldset.fields, ('a', 'b', 'c'))

    def test_nonzero_fieldset(self):
        fieldset1 = Fieldset('the_name', fields=[])
        self.assertFalse(fieldset1)

        fieldset2 = Fieldset('the_name', fields=['a'])
        self.assertTrue(fieldset2)

    def test_assigning_template_name(self):
        fieldset1 = Fieldset('the_name', fields=['a'])
        self.assertIsNone(fieldset1.template_name)
        fieldset2 = Fieldset('the_name', fields=['a'], template_name='some_custom_template.html')
        self.assertEqual(fieldset2.template_name, 'some_custom_template.html')


class TestFieldsetDeclarationSyntax(TestCase):
    def test_admin_style_declaration(self):
        class TestForm(BetterForm):
            a = forms.CharField()
            b = forms.CharField()
            c = forms.CharField()
            d = forms.CharField()

            class Meta:
                fieldsets = (
                    ('first', {'fields': ('a')}),
                    ('second', {'fields': ('b', 'c')}),
                    ('third', {'fields': ('d')}),
                )
        form = TestForm()
        fieldsets = [fieldset for fieldset in form.fieldsets]
        self.assertEqual(fieldsets[0].name, 'first')
        self.assertTupleEqual(fieldsets[0].fieldset.fields, ('a',))
        self.assertEqual(fieldsets[1].name, 'second')
        self.assertTupleEqual(fieldsets[1].fieldset.fields, ('b', 'c'))
        self.assertEqual(fieldsets[2].name, 'third')
        self.assertTupleEqual(fieldsets[2].fieldset.fields, ('d',))
        self.assertIsInstance(fieldsets[0], BoundFieldset)

    def test_bare_fields_style_declaration(self):
        class TestForm(BetterForm):
            a = forms.CharField()
            b = forms.CharField()
            c = forms.CharField()
            d = forms.CharField()

            class Meta:
                fieldsets = ('a', ('b', 'c'), 'd')
        form = TestForm()
        fieldsets = [fieldset for fieldset in form.fieldsets]
        self.assertEqual(fieldsets[0].field, form.fields['a'])
        self.assertEqual(fieldsets[1].name, '__base_fieldset___1')
        self.assertTupleEqual(fieldsets[1].fieldset.fields, ('b', 'c'))
        self.assertEqual(fieldsets[2].field, form.fields['d'])
        self.assertIsInstance(fieldsets[0], forms.forms.BoundField)
        self.assertIsInstance(fieldsets[1], BoundFieldset)
        self.assertIsInstance(fieldsets[2], forms.forms.BoundField)


class TestBetterForm(TestCase):
    def setUp(self):
        class TestForm(BetterForm):
            a = forms.CharField()
            b = forms.CharField()
            c = forms.CharField()

            class Meta:
                fieldsets = (
                    ('first', {'fields': ('a', 'b')}),
                    ('second', {'fields': ('c')}),
                )
        self.TestForm = TestForm

    def test_name_lookups(self):
        form = self.TestForm()
        fieldsets = [fieldset for fieldset in form.fieldsets]
        # field lookups
        self.assertEqual(form['a'].field, fieldsets[0]['a'].field)
        # fieldset lookups
        self.assertEqual(form['first'].fieldset, fieldsets[0].fieldset)
        self.assertEqual(form['second'].fieldset, fieldsets[1].fieldset)

    def test_index_lookups(self):
        form = self.TestForm()
        # field lookups
        self.assertEqual(form['a'].field, form.fieldsets[0][0].field)
        # fieldset lookups
        self.assertEqual(form['first'].fieldset, form.fieldsets[0].fieldset)
        self.assertEqual(form['second'].fieldset, form.fieldsets[1].fieldset)

    def test_field_to_fieldset_name_conflict(self):
        with self.assertRaises(AttributeError):
            class NameConflictForm(self.TestForm):
                class Meta:
                    fieldsets = (
                        ('first', {'fields': ('a', 'b')}),
                        ('first', {'fields': ('c')}),
                    )

    def test_duplicate_name_in_fieldset(self):
        with self.assertRaises(AttributeError):
            class NameConflictForm(self.TestForm):
                class Meta:
                    fieldsets = (
                        ('first', {'fields': ('a', 'a')}),
                        ('second', {'fields': ('c')}),
                    )

    def test_field_error(self):
        data = {'a': 'a', 'b': 'b', 'c': 'c'}
        form = self.TestForm(data)
        self.assertTrue(form.is_valid())

        form.field_error('a', 'test')
        self.assertFalse(form.is_valid())

    def test_form_error(self):
        data = {'a': 'a', 'b': 'b', 'c': 'c'}
        form = self.TestForm(data)
        self.assertTrue(form.is_valid())

        form.form_error('test')
        self.assertFalse(form.is_valid())
        self.assertDictEqual(form.errors, {'__all__': [u'test']})

    def test_fieldset_error(self):
        data = {'a': 'a', 'b': 'b', 'c': 'c'}
        form = self.TestForm(data)
        self.assertTrue(form.is_valid())

        self.assertNotIn(form.fieldsets[0].fieldset.error_css_class, form.fieldsets[0].css_classes)

        form.field_error('first', 'test')
        self.assertFalse(form.is_valid())
        fieldsets = [fieldset for fieldset in form.fieldsets]
        self.assertTrue(fieldsets[0].errors)
        self.assertIn(form.fieldsets[0].fieldset.error_css_class, form.fieldsets[0].css_classes)

    def test_fieldset_css_classes(self):
        class TestForm(BetterForm):
            a = forms.CharField()
            b = forms.CharField()
            c = forms.CharField()

            class Meta:
                fieldsets = (
                    ('first', {'fields': ('a', 'b')}),
                    ('second', {'fields': ('c'), 'css_classes': ['arst', 'tsra']}),
                )
        form = TestForm()
        self.assertIn('arst', form.fieldsets[1].css_classes)
        self.assertIn('tsra', form.fieldsets[1].css_classes)

    def test_fieldset_iteration(self):
        form = self.TestForm()
        self.assertTupleEqual(
            tuple(fieldset.fieldset for fieldset in form),
            tuple(fieldset.fieldset for fieldset in form.fieldsets),
        )

    def test_no_fieldsets(self):
        class TestForm(BetterForm):
            a = forms.CharField()
            b = forms.CharField()
            c = forms.CharField()

        form = TestForm()
        fields_iter = sorted((field.field for field in form), key=id)
        fields_values = sorted(form.fields.values(), key=id)
        self.assertSequenceEqual(fields_iter, fields_values)


class TestBetterModelForm(TestCase):
    def setUp(self):
        class TestModel(models.Model):
            class Meta:
                abstract = True
            a = models.CharField(max_length=255)
            b = models.CharField(max_length=255)
            c = models.CharField(max_length=255)
            d = models.CharField(max_length=255)
        self.TestModel = TestModel

    def test_basic_fieldsets(self):
        class TestModelForm(BetterModelForm):
            class Meta:
                model = self.TestModel
                fieldsets = (
                    ('first', {'fields': ('a',)}),
                    ('second', {'fields': ('b', 'c')}),
                    ('third', {'fields': ('d',)}),
                )
        form = TestModelForm()
        fieldsets = [fieldset for fieldset in form.fieldsets]
        self.assertEqual(fieldsets[0].name, 'first')
        self.assertEqual(fieldsets[1].name, 'second')
        self.assertEqual(fieldsets[2].name, 'third')
        self.assertTupleEqual(fieldsets[0].fieldset.fields, ('a',))
        self.assertTupleEqual(fieldsets[1].fieldset.fields, ('b', 'c'))
        self.assertTupleEqual(fieldsets[2].fieldset.fields, ('d',))

    def test_fields_meta_attribute(self):
        class TestModelForm1(BetterModelForm):
            class Meta:
                model = self.TestModel
                fieldsets = (
                    ('first', {'fields': ('a',)}),
                    ('second', {'fields': ('b', 'c')}),
                    ('third', {'fields': ('d',)}),
                )
        self.assertTrue(hasattr(TestModelForm1.Meta, 'fields'))
        self.assertTupleEqual(TestModelForm1.Meta.fields, ('a', 'b', 'c', 'd'))

        class TestModelForm2(BetterModelForm):
            class Meta:
                model = self.TestModel
                fieldsets = (
                    ('first', {'fields': ('a',)}),
                    ('second', {'fields': ('b', 'c')}),
                    ('third', {'fields': ('d',)}),
                )
                fields = ('a', 'b', 'd')

        self.assertTrue(hasattr(TestModelForm2.Meta, 'fields'))
        self.assertTupleEqual(TestModelForm2.Meta.fields, ('a', 'b', 'd'))

        class TestModelForm3(TestModelForm2):
            pass

        self.assertTrue(hasattr(TestModelForm3.Meta, 'fields'))
        self.assertTupleEqual(TestModelForm3.Meta.fields, ('a', 'b', 'd'))

        class TestModelForm4(TestModelForm2):
            class Meta(TestModelForm2.Meta):
                fieldsets = (
                    ('first', {'fields': ('a', 'c')}),
                    ('third', {'fields': ('d',)}),
                )

        self.assertTrue(hasattr(TestModelForm4.Meta, 'fields'))
        self.assertTupleEqual(TestModelForm4.Meta.fields, ('a', 'c', 'd'))


class TestFormRendering(TestCase):
    def setUp(self):
        class TestForm(BetterForm):
            # Set the label_suffix to an empty string for consistent results
            # across Django 1.5 and 1.6.
            label_suffix = ''

            a = forms.CharField()
            b = forms.CharField()
            c = forms.CharField()

            class Meta:
                fieldsets = (
                    ('first', {'fields': ('a', 'b')}),
                    ('second', {'fields': ('c')}),
                )
        self.TestForm = TestForm

    def test_non_fieldset_form_rendering(self):
        class TestForm(BetterForm):
            # Set the label_suffix to an empty string for consistent results
            # across Django 1.5 and 1.6.
            label_suffix = ''

            a = forms.CharField()
            b = forms.CharField(required=False)
            c = forms.CharField()

        form = TestForm()
        env = {
            'form': form,
            'no_head': True,
            'fieldset_template_name': 'betterforms/fieldset_as_div.html',
            'field_template_name': 'betterforms/field_as_div.html',
        }
        test = """
            <div class="required a formField">
                <label class="required" for="id_a">A</label>
                <input id="id_a" name="a" required type="text" />
            </div>
            <div class="b formField">
                <label for="id_b">B</label>
                <input id="id_b" name="b" type="text" />
            </div>
            <div class="required c formField">
                <label class="required" for="id_c">C</label>
                <input id="id_c" name="c" required type="text" />
            </div>
            """
        if django.VERSION < (1, 8):
            test = test.replace('label class="required"', 'label')
        if django.VERSION < (1, 10):
            test = test.replace(' required ', ' ')
        self.assertHTMLEqual(
            render_to_string('betterforms/form_as_fieldsets.html', env),
            test,
        )
        form.field_error('a', 'this is an error message')
        test = """
            <div class="required error a formField">
                <label class="required" for="id_a">A</label>
                <input id="id_a" name="a" required type="text" />
                <ul class="errorlist"><li>this is an error message</li></ul>
            </div>
            <div class="b formField">
                <label for="id_b">B</label>
                <input id="id_b" name="b" type="text" />
            </div>
            <div class="required c formField">
                <label class="required" for="id_c">C</label>
                <input id="id_c" name="c" required type="text" />
            </div>
            """
        if django.VERSION < (1, 8):
            test = test.replace('label class="required"', 'label')
        if django.VERSION < (1, 10):
            test = test.replace(' required ', ' ')
        self.assertHTMLEqual(
            render_to_string('betterforms/form_as_fieldsets.html', env),
            test,
        )

    def test_include_tag_rendering(self):
        form = self.TestForm()
        env = {
            'form': form,
            'no_head': True,
            'fieldset_template_name': 'betterforms/fieldset_as_div.html',
            'field_template_name': 'betterforms/field_as_div.html',
        }
        test = """
            <fieldset class="formFieldset first">
                <div class="required a formField">
                    <label class="required" for="id_a">A</label>
                    <input id="id_a" name="a" required type="text" />
                </div>
                <div class="required b formField">
                    <label class="required" for="id_b">B</label>
                    <input id="id_b" name="b" required type="text" />
                </div>
            </fieldset>
            <fieldset class="formFieldset second">
                <div class="required c formField">
                    <label class="required" for="id_c">C</label>
                    <input id="id_c" name="c" required type="text" />
                </div>
            </fieldset>
            """
        if django.VERSION < (1, 8):
            test = test.replace('label class="required"', 'label')
        if django.VERSION < (1, 10):
            test = test.replace(' required ', ' ')
        self.assertHTMLEqual(
            render_to_string('betterforms/form_as_fieldsets.html', env),
            test,
        )
        form.field_error('a', 'this is an error message')
        test = """
            <fieldset class="formFieldset first">
                <div class="required error a formField">
                    <label class="required" for="id_a">A</label>
                    <input id="id_a" name="a" required type="text" />
                    <ul class="errorlist"><li>this is an error message</li></ul>
                </div>
                <div class="required b formField">
                    <label class="required" for="id_b">B</label>
                    <input id="id_b" name="b" required type="text" />
                </div>
            </fieldset>
            <fieldset class="formFieldset second">
                <div class="required c formField">
                    <label class="required" for="id_c">C</label>
                    <input id="id_c" name="c" required type="text" />
                </div>
            </fieldset>
            """
        if django.VERSION < (1, 8):
            test = test.replace('label class="required"', 'label')
        if django.VERSION < (1, 10):
            test = test.replace(' required ', ' ')
        self.assertHTMLEqual(
            render_to_string('betterforms/form_as_fieldsets.html', env),
            test,
        )

    def test_fields_django_form_required(self):
        class TestForm(forms.Form):
            a = forms.CharField(label='A:')
            b = forms.CharField(label='B:', required=False)

        form = TestForm()

        env = {
            'form': form,
            'no_head': True,
            'fieldset_template_name': 'betterforms/fieldset_as_div.html',
            'field_template_name': 'betterforms/field_as_div.html',
        }
        test = """
            <div class="a formField required">
                <label for="id_a">A:</label>
                <input id="id_a" name="a" required type="text" />
            </div>
            <div class="b formField">
                <label for="id_b">B:</label>
                <input id="id_b" name="b" type="text" />
            </div>
            """
        if django.VERSION < (1, 8):
            test = test.replace('label class="required"', 'label')
        if django.VERSION < (1, 10):
            test = test.replace(' required ', ' ')
        self.assertHTMLEqual(
            render_to_string('betterforms/form_as_fieldsets.html', env),
            test,
        )

    @unittest.expectedFailure
    def test_form_to_str(self):
        # TODO: how do we test this
        assert False

    @unittest.expectedFailure
    def test_form_as_table(self):
        form = self.TestForm()
        form.as_table()

    @unittest.expectedFailure
    def test_form_as_ul(self):
        form = self.TestForm()
        form.as_ul()

    def test_form_as_p(self):
        form = self.TestForm()
        test = """
            <fieldset class="formFieldset first">
                <p class="required">
                    <label class="required" for="id_a">A</label>
                    <input id="id_a" name="a" required type="text" />
                </p>
                <p class="required">
                    <label class="required" for="id_b">B</label>
                    <input id="id_b" name="b" required type="text" />
                </p>
            </fieldset>
            <fieldset class="formFieldset second">
                <p class="required">
                    <label class="required" for="id_c">C</label>
                    <input id="id_c" name="c" required type="text" />
                </p>
            </fieldset>
            """
        if django.VERSION < (1, 8):
            test = test.replace('label class="required"', 'label')
        if django.VERSION < (1, 10):
            test = test.replace(' required ', ' ')
        self.assertHTMLEqual(
            form.as_p(),
            test,
        )

        form.field_error('a', 'this is an error')
        test = """
            <fieldset class="formFieldset first">
                <p class="required error">
                    <ul class="errorlist"><li>this is an error</li></ul>
                    <label class="required" for="id_a">A</label>
                    <input id="id_a" name="a" required type="text" />
                </p>
                <p class="required">
                    <label class="required" for="id_b">B</label>
                    <input id="id_b" name="b" required type="text" />
                </p>
            </fieldset>
            <fieldset class="formFieldset second">
                <p class="required">
                    <label class="required" for="id_c">C</label>
                    <input id="id_c" name="c" required type="text" />
                </p>
            </fieldset>
            """
        self.maxDiff=None
        if django.VERSION < (1, 8):
            test = test.replace('label class="required"', 'label')
        if django.VERSION < (1, 10):
            test = test.replace(' required ', ' ')
        self.assertHTMLEqual(
            form.as_p(),
            test,
        )

    def test_fieldset_legend(self):
        class TestForm(BetterForm):
            a = forms.CharField()
            b = forms.CharField()
            c = forms.CharField()

            label_suffix = ''

            class Meta:
                fieldsets = (
                    Fieldset('first', ('a', 'b'), legend='First Fieldset'),
                    Fieldset('second', ('c'), legend='Second Fieldset'),
                )

        form = TestForm()
        test = """
            <fieldset class="formFieldset first">
                <legend>First Fieldset</legend>
                <p class="required">
                    <label class="required" for="id_a">A</label>
                    <input id="id_a" name="a" required type="text" />
                </p>
                <p class="required">
                    <label class="required" for="id_b">B</label>
                    <input id="id_b" name="b" required type="text" />
                </p>
            </fieldset>
            <fieldset class="formFieldset second">
                <legend>Second Fieldset</legend>
                <p class="required">
                    <label class="required" for="id_c">C</label>
                    <input id="id_c" name="c" required type="text" />
                </p>
            </fieldset>
            """
        if django.VERSION < (1, 8):
            test = test.replace('label class="required"', 'label')
        if django.VERSION < (1, 10):
            test = test.replace(' required ', ' ')
        self.assertHTMLEqual(
            form.as_p(),
            test,
        )

    def test_css_classes_when_form_has_prefix(self):
        class TestForm(BetterForm):
            name = forms.CharField()
            label_suffix = ''

        form = TestForm(prefix="prefix")
        env = {'form': form, 'no_head': True}
        test = """
            <div class="required prefix-name name formField">
                <label class="required" for="id_prefix-name">Name</label>
                <input type="text" id="id_prefix-name" required name="prefix-name" />
            </div>
            """
        if django.VERSION < (1, 8):
            test = test.replace('label class="required"', 'label')
        if django.VERSION < (1, 10):
            test = test.replace(' required ', ' ')
        self.assertHTMLEqual(
            render_to_string('betterforms/form_as_fieldsets.html', env),
            test,
        )


class ChangeListModel(models.Model):
    field_a = models.CharField(max_length=255)
    field_b = models.CharField(max_length=255)
    field_c = models.TextField(max_length=255)


class TestChangleListQuerySetAPI(TestCase):
    def setUp(self):
        class TestChangeListForm(BaseChangeListForm):
            model = ChangeListModel
            foo = forms.CharField()
        self.TestChangeListForm = TestChangeListForm

        for i in range(5):
            ChangeListModel.objects.create(field_a=str(i))

    def test_with_model_declared(self):
        form = self.TestChangeListForm({})

        # base_queryset should default to Model.objects.all()
        self.assertTrue(form.base_queryset.count(), 5)

    def test_with_model_declaration_and_provided_queryset(self):
        form = self.TestChangeListForm({'foo': 'arst'}, queryset=ChangeListModel.objects.exclude(field_a='0').exclude(field_a='1'))

        self.assertTrue(form.base_queryset.count(), 3)
        form.full_clean()
        self.assertTrue(form.get_queryset().count(), 3)

    def test_missing_model_and_queryset(self):
        class TestChangeListForm(BaseChangeListForm):
            pass

        with self.assertRaises(AttributeError):
            TestChangeListForm()


class TestSearchFormAPI(TestCase):
    def setUp(self):
        ChangeListModel.objects.create(field_a='foo', field_b='bar', field_c='baz')
        ChangeListModel.objects.create(field_a='bar', field_b='baz')
        ChangeListModel.objects.create(field_a='baz')

    def test_requires_search_fields(self):
        class TheSearchForm(SearchForm):
            model = ChangeListModel

        with self.assertRaises(ImproperlyConfigured):
            TheSearchForm({})

    def test_passing_in_search_fields(self):
        class TheSearchForm(SearchForm):
            model = ChangeListModel

        form = TheSearchForm({}, search_fields=('field_a',))
        self.assertEqual(form.SEARCH_FIELDS, ('field_a',))

        form = TheSearchForm({}, search_fields=('field_a', 'field_b'))
        self.assertEqual(form.SEARCH_FIELDS, ('field_a', 'field_b'))

    def test_setting_search_fields_on_class(self):
        class TheSearchForm(SearchForm):
            SEARCH_FIELDS = ('field_a', 'field_b', 'field_c')
            model = ChangeListModel

        form = TheSearchForm({})
        self.assertEqual(form.SEARCH_FIELDS, ('field_a', 'field_b', 'field_c'))

    def test_overriding_search_fields_set_on_class(self):
        class TheSearchForm(SearchForm):
            SEARCH_FIELDS = ('field_a', 'field_b', 'field_c')
            model = ChangeListModel

        form = TheSearchForm({}, search_fields=('field_a', 'field_c'))
        self.assertEqual(form.SEARCH_FIELDS, ('field_a', 'field_c'))

    def test_searching(self):
        class TheSearchForm(SearchForm):
            SEARCH_FIELDS = ('field_a', 'field_b', 'field_c')
            model = ChangeListModel

        f = TheSearchForm({'q': 'foo'})
        f.full_clean()
        self.assertEqual(f.get_queryset().count(), 1)

        f = TheSearchForm({'q': 'bar'})
        f.full_clean()
        self.assertEqual(f.get_queryset().count(), 2)

        f = TheSearchForm({'q': 'baz'})
        f.full_clean()
        self.assertEqual(f.get_queryset().count(), 3)

    def test_searching_over_limited_fields(self):
        class TheSearchForm(SearchForm):
            SEARCH_FIELDS = ('field_a', 'field_c')
            model = ChangeListModel

        f = TheSearchForm({'q': 'foo'})
        f.full_clean()
        self.assertEqual(f.get_queryset().count(), 1)

        f = TheSearchForm({'q': 'bar'})
        f.full_clean()
        self.assertEqual(f.get_queryset().count(), 1)

        f = TheSearchForm({'q': 'baz'})
        f.full_clean()
        self.assertEqual(f.get_queryset().count(), 2)

    def test_case_insensitive(self):
        class TheSearchForm(SearchForm):
            SEARCH_FIELDS = ('field_a',)
            model = ChangeListModel

        self.assertFalse(TheSearchForm.CASE_SENSITIVE)

        upper_cased = ChangeListModel.objects.create(field_a='TeSt')
        lower_cased = ChangeListModel.objects.create(field_a='test')

        form = TheSearchForm({'q': 'Test'})
        form.full_clean()

        self.assertIn(upper_cased, form.get_queryset())
        self.assertIn(lower_cased, form.get_queryset())

    @unittest.skipIf(settings.DATABASES['default']['ENGINE'] == 'django.db.backends.sqlite3', 'Case Sensitive __contains queries are not supported on sqlite.')
    def test_case_sensitive(self):
        # TODO: make this test run on travis with postgres/mysql to be sure
        # this functionality actually works.
        class TheSearchForm(SearchForm):
            SEARCH_FIELDS = ('field_a', 'field_c')
            CASE_SENSITIVE = True
            model = ChangeListModel

        self.assertTrue(TheSearchForm.CASE_SENSITIVE)

        upper_cased = ChangeListModel.objects.create(field_a='TeSt')
        lower_cased = ChangeListModel.objects.create(field_a='test')

        form = TheSearchForm({'q': 'TeSt'})
        form.full_clean()

        self.assertIn(upper_cased, form.get_queryset())
        self.assertNotIn(lower_cased, form.get_queryset())


class TestHeaderAPI(TestCase):
    def test_header_bare_declaration(self):
        header = Header('field_a')

        self.assertTrue(header.is_sortable)
        self.assertEqual(header.name, 'field_a')
        self.assertEqual(header.column_name, 'field_a')
        self.assertEqual(header.label, 'Field a')

    def test_header_with_label_declaration(self):
        header = Header('field_a', 'Test Label')

        self.assertEqual(header.label, 'Test Label')

    def test_header_with_column_declared(self):
        header = Header('not_a_column', column_name='field_a')

        self.assertEqual(header.name, 'not_a_column')
        self.assertEqual(header.column_name, 'field_a')

    def test_non_sortable_header(self):
        header = Header('field_a', is_sortable=False)

        self.assertFalse(header.is_sortable)


class TestHeaderSetAPI(TestCase):
    def test_header_names_must_be_unique(self):
        HEADERS = (
            Header('field_a'),
            Header('field_a'),
        )
        with self.assertRaises(ImproperlyConfigured):
            HeaderSet(None, HEADERS)

    def test_header_set_declared_as_header_classes(self):
        HEADERS = (
            Header('field_a'),
            Header('field_b', 'Test Label'),
            Header('test_name', 'Test Name', column_name='field_c'),
            Header('created_at', is_sortable=False),
        )
        self.do_header_set_assertions(HEADERS)

    def test_header_set_declared_as_args(self):
        HEADERS = (
            ('field_a',),
            ('field_b', 'Test Label'),
            ('test_name', 'Test Name', 'field_c'),
            ('created_at', None, None, False),
        )
        self.do_header_set_assertions(HEADERS)

    def test_header_set_declared_as_name_and_kwargs(self):
        HEADERS = (
            ('field_a', {}),
            ('field_b', {'label': 'Test Label'}),
            ('test_name', {'label': 'Test Name', 'column_name': 'field_c'}),
            ('created_at', {'is_sortable': False}),
        )
        self.do_header_set_assertions(HEADERS)

    def test_header_set_mixed_declaration_styles(self):
        HEADERS = (
            'field_a',
            ('field_b', 'Test Label'),
            ('test_name', {'label': 'Test Name', 'column_name': 'field_c'}),
            Header('created_at', is_sortable=False),
        )
        self.do_header_set_assertions(HEADERS)

    def test_bad_header_declaration(self):
        HEADERS = (
            {'bad_declaration': 'test'},
            ('field_b', 'Test Label'),
            ('test_name', {'label': 'Test Name', 'column_name': 'field_c'}),
            Header('created_at', is_sortable=False),
        )
        with self.assertRaises(ImproperlyConfigured):
            self.do_header_set_assertions(HEADERS)

    def do_header_set_assertions(self, HEADERS):
        header_set = HeaderSet(None, HEADERS)

        self.assertTrue(len(header_set), 4)
        self.assertSequenceEqual(
            [header.name for header in header_set.headers.values()],
            ('field_a', 'field_b', 'test_name', 'created_at'),
        )
        self.assertSequenceEqual(
            [header.label for header in header_set.headers.values()],
            ('Field a', 'Test Label', 'Test Name', 'Created at'),
        )
        self.assertSequenceEqual(
            [header.column_name for header in header_set.headers.values()],
            ('field_a', 'field_b', 'field_c', None),
        )
        self.assertSequenceEqual(
            [header.is_sortable for header in header_set.headers.values()],
            (True, True, True, False),
        )

    def test_iteration_yields_bound_headers(self):
        HEADERS = (
            Header('field_a'),
            Header('field_b', 'Test Label'),
            Header('test_name', 'Test Name', column_name='field_c'),
            Header('created_at', is_sortable=False),
        )
        form = mock.NonCallableMagicMock(forms.Form)
        form.prefix = None
        header_set = HeaderSet(form, HEADERS)

        self.assertTrue(all((
            isinstance(header, BoundHeader) for header in header_set
        )))

    def test_index_and_key_lookups(self):
        HEADERS = (
            Header('field_a'),
            Header('field_b', 'Test Label'),
            Header('test_name', 'Test Name', column_name='field_c'),
            Header('created_at', is_sortable=False),
        )
        form = mock.NonCallableMagicMock(forms.Form)
        form.prefix = None
        header_set = HeaderSet(form, HEADERS)

        self.assertIsInstance(header_set[0], BoundHeader)
        self.assertEqual(header_set[0].header, HEADERS[0])

        self.assertIsInstance(header_set['field_b'], BoundHeader)
        self.assertEqual(header_set['field_b'].header, HEADERS[1])


class TestBoundHeaderAPI(TestCase):
    def setUp(self):
        self.HEADERS = (
            Header('test_name', 'Test Label', 'column_name', is_sortable=True),
        )
        self.form = mock.NonCallableMagicMock(forms.Form)
        self.form.prefix = None
        self.form.HEADERS = self.HEADERS

    def test_bound_header_pass_through_properties(self):
        header_set = HeaderSet(self.form, self.HEADERS)

        self.assertEqual(header_set[0].header, self.HEADERS[0])
        self.assertEqual(header_set[0].name, self.HEADERS[0].name)
        self.assertEqual(header_set[0].label, self.HEADERS[0].label)
        self.assertEqual(header_set[0].column_name, self.HEADERS[0].column_name)
        self.assertEqual(header_set[0].is_sortable, self.HEADERS[0].is_sortable)

    def test_sort_parameter_no_prefix(self):
        self.assertIsNone(self.form.prefix)
        header = HeaderSet(self.form, self.HEADERS)[0]

        self.assertEqual(header.param, 'sorts')

    def test_sort_parameter_with_prefix(self):
        self.form.prefix = 'test'
        header = HeaderSet(self.form, self.HEADERS)[0]

        self.assertEqual(header.param, 'test-sorts')

    def test_is_active_property_while_not_active(self):
        header = HeaderSet(self.form, self.HEADERS)[0]

        self.assertFalse(header.is_active)
        self.assertFalse(header.is_ascending)
        self.assertFalse(header.is_descending)

    def test_is_active_property_while_active_and_ascending(self):
        self.form.data = {'sorts': '1'}
        self.form.cleaned_data = {'sorts': [1]}
        header = HeaderSet(self.form, self.HEADERS)[0]

        self.assertTrue(header.is_active)
        self.assertTrue(header.is_ascending)
        self.assertFalse(header.is_descending)

    def test_is_active_property_while_active_and_descending(self):
        self.form.data = {'sorts': '-1'}
        self.form.cleaned_data = {'sorts': [-1]}
        header = HeaderSet(self.form, self.HEADERS)[0]

        self.assertTrue(header.is_active)
        self.assertFalse(header.is_ascending)
        self.assertTrue(header.is_descending)

    def test_add_to_sorts_with_no_sorts(self):
        HEADERS = (
            Header('field_a'),
            Header('field_b'),
            Header('field_c'),
        )
        self.form.data = {}
        self.form.cleaned_data = {'sorts': []}
        self.form.HEADERS = HEADERS
        header_set = HeaderSet(self.form, HEADERS)
        self.assertEqual(header_set['field_a'].add_to_sorts(), [1])
        self.assertEqual(header_set['field_b'].add_to_sorts(), [2])
        self.assertEqual(header_set['field_c'].add_to_sorts(), [3])

    def test_add_to_sorts_with_active_sorts(self):
        HEADERS = (
            Header('field_a'),
            Header('field_b'),
            Header('field_c'),
        )
        self.form.data = {'sorts': '1.-2'}
        self.form.cleaned_data = {'sorts': [1, -2]}
        self.form.HEADERS = HEADERS
        header_set = HeaderSet(self.form, HEADERS)
        self.assertEqual(header_set['field_a'].add_to_sorts(), [-1, -2])
        self.assertEqual(header_set['field_b'].add_to_sorts(), [2, 1])
        self.assertEqual(header_set['field_c'].add_to_sorts(), [3, 1, -2])

    def test_sort_priority_display(self):
        HEADERS = (
            Header('field_a'),
            Header('field_b'),
            Header('field_c'),
        )
        self.form.data = {'sorts': '-2.1'}
        self.form.cleaned_data = {'sorts': [-2, 1]}
        self.form.HEADERS = HEADERS
        header_set = HeaderSet(self.form, HEADERS)
        self.assertEqual(header_set['field_a'].priority, 2)
        self.assertEqual(header_set['field_b'].priority, 1)
        self.assertEqual(header_set['field_c'].priority, None)

    def test_css_classes(self):
        HEADERS = (
            Header('field_a'),
            Header('field_b'),
            Header('field_c'),
        )
        self.form.data = {'sorts': '1.-2'}
        self.form.cleaned_data = {'sorts': [1, -2]}
        self.form.HEADERS = HEADERS
        header_set = HeaderSet(self.form, HEADERS)
        self.assertEqual(header_set['field_a'].css_classes, 'active ascending')
        self.assertEqual(header_set['field_b'].css_classes, 'active descending')
        self.assertEqual(header_set['field_c'].css_classes, '')

    def test_bound_header_querystring_properties(self):
        HEADERS = (
            Header('field_a'),
            Header('field_b'),
            Header('field_c'),
        )
        self.form.data = {'sorts': '1.-2'}
        self.form.cleaned_data = {'sorts': [1, -2]}
        self.form.HEADERS = HEADERS
        header_set = HeaderSet(self.form, HEADERS)

        self.assertEqual(header_set['field_a'].querystring, 'sorts=-1.-2')
        self.assertEqual(header_set['field_a'].singular_querystring, 'sorts=-1')
        self.assertEqual(header_set['field_a'].remove_querystring, 'sorts=-2')

        self.assertEqual(header_set['field_b'].querystring, 'sorts=2.1')
        self.assertEqual(header_set['field_b'].singular_querystring, 'sorts=2')
        self.assertEqual(header_set['field_b'].remove_querystring, 'sorts=1')

        self.assertEqual(header_set['field_c'].querystring, 'sorts=3.1.-2')
        self.assertEqual(header_set['field_c'].singular_querystring, 'sorts=3')
        self.assertEqual(header_set['field_c'].remove_querystring, 'sorts=1.-2')

    def assertQueryStringEqual(self, a, b, *args, **kwargs):
        """
        We need this because QueryDicts are dicts and key-ordering is not
        guaranteed on Python3. So we just convert query_strings back into
        QueryDicts to compare them.
        """
        self.assertEqual(QueryDict(a), QueryDict(b), *args, **kwargs)

    def test_bound_header_querystring_with_querydict(self):
        HEADERS = (
            Header('field_a'),
        )
        self.form.data = QueryDict('field=value')
        self.form.cleaned_data = {'field': 'value'}
        self.form.HEADERS = HEADERS
        header_set = HeaderSet(self.form, HEADERS)

        self.assertQueryStringEqual(header_set['field_a'].querystring, 'field=value&sorts=1')
        self.assertQueryStringEqual(header_set['field_a'].singular_querystring, 'field=value&sorts=1')
        self.assertQueryStringEqual(header_set['field_a'].remove_querystring, 'field=value&sorts=')

    def test_bound_header_querystring_with_querydict_overwrites_instead_of_appending(self):
        HEADERS = (
            Header('field_a'),
        )
        self.form.data = QueryDict('field=value&sorts=1')
        self.form.cleaned_data = {'field': 'value', 'sorts': [1]}
        self.form.HEADERS = HEADERS
        header_set = HeaderSet(self.form, HEADERS)

        # It used to output 'field=value&sorts=1&sorts=-1'
        self.assertQueryStringEqual(header_set['field_a'].querystring, 'field=value&sorts=-1')
        self.assertQueryStringEqual(header_set['field_a'].singular_querystring, 'field=value&sorts=-1')
        self.assertQueryStringEqual(header_set['field_a'].remove_querystring, 'field=value&sorts=')


class TestSortFormAPI(TestCase):
    def setUp(self):
        self.abc = ChangeListModel.objects.create(field_a='a', field_b='b', field_c='c')
        self.cab = ChangeListModel.objects.create(field_a='c', field_b='a', field_c='b')
        self.bca = ChangeListModel.objects.create(field_a='b', field_b='c', field_c='a')

        class TestSortForm(SortForm):
            model = ChangeListModel
            HEADERS = (
                Header('field_a'),
                Header('field_b'),
                Header('named_header', column_name='field_c'),
                Header('not_sortable', is_sortable=False),
            )
        self.TestSortForm = TestSortForm

    def test_valid_with_no_sorts(self):
        form = self.TestSortForm({})
        self.assertTrue(form.is_valid())
        self.assertEqual(form.get_queryset().count(), 3)

    def test_sort_field_cleaning(self):
        self.assertTrue(self.TestSortForm({'sorts': '1.2.3'}).is_valid())
        self.assertTrue(self.TestSortForm({'sorts': '2.3.1'}).is_valid())
        self.assertTrue(self.TestSortForm({'sorts': '3.1.2'}).is_valid())

        # Unsortable Header
        unsortable = self.TestSortForm({'sorts': '1.2.3.4'})
        self.assertFalse(unsortable.is_valid())
        self.assertIn('sorts', unsortable.errors)
        self.assertIn(self.TestSortForm.error_messages['unsortable_header'], unsortable.errors['sorts'])

        # Unknown Header
        unknown = self.TestSortForm({'sorts': '1.2.3.5'})
        self.assertFalse(unknown.is_valid())
        self.assertIn('sorts', unsortable.errors)
        self.assertIn(self.TestSortForm.error_messages['unknown_header'], unsortable.errors['sorts'])

        # Invalid Header
        unknown = self.TestSortForm({'sorts': '1.2.X'})
        self.assertFalse(unknown.is_valid())
        self.assertIn('sorts', unsortable.errors)
        self.assertIn(self.TestSortForm.error_messages['unknown_header'], unsortable.errors['sorts'])

    def test_single_field_sorting(self):
        f = self.TestSortForm({'sorts': '1'})
        f.full_clean()
        self.assertSequenceEqual(
            f.get_queryset(),
            (self.abc, self.bca, self.cab),
        )

        f = self.TestSortForm({'sorts': '-1'})
        f.full_clean()
        self.assertSequenceEqual(
            f.get_queryset(),
            (self.cab, self.bca, self.abc),
        )

        f = self.TestSortForm({'sorts': '2'})
        f.full_clean()
        self.assertSequenceEqual(
            f.get_queryset(),
            (self.cab, self.abc, self.bca),
        )

        f = self.TestSortForm({'sorts': '-2'})
        f.full_clean()
        self.assertSequenceEqual(
            f.get_queryset(),
            (self.bca, self.abc, self.cab),
        )

        f = self.TestSortForm({'sorts': '3'})
        f.full_clean()
        self.assertSequenceEqual(
            f.get_queryset(),
            (self.bca, self.cab, self.abc),
        )

        f = self.TestSortForm({'sorts': '-3'})
        f.full_clean()
        self.assertSequenceEqual(
            f.get_queryset(),
            (self.abc, self.cab, self.bca),
        )

    def test_multi_field_sorting(self):
        self.aac = ChangeListModel.objects.create(field_a='a', field_b='a', field_c='c')

        f = self.TestSortForm({'sorts': '1.2'})
        f.full_clean()
        self.assertSequenceEqual(
            f.get_queryset(),
            (self.aac, self.abc, self.bca, self.cab),
        )

        f = self.TestSortForm({'sorts': '1.-2'})
        f.full_clean()
        self.assertSequenceEqual(
            f.get_queryset(),
            (self.abc, self.aac, self.bca, self.cab),
        )

    def test_order_by_override(self):
        self.aac = ChangeListModel.objects.create(field_a='a', field_b='a', field_c='c')
        self.aab = ChangeListModel.objects.create(field_a='a', field_b='a', field_c='b')

        class OverriddenOrderForm(self.TestSortForm):
            def get_order_by(self):
                order_by = super(OverriddenOrderForm, self).get_order_by()
                return ['field_a'] + order_by

        f = OverriddenOrderForm({'sorts': '-3.2'})
        f.full_clean()
        self.assertSequenceEqual(
            f.get_queryset(),
            (self.aac, self.abc, self.aab, self.bca, self.cab),
        )

        f = OverriddenOrderForm({'sorts': '3.2'})
        f.full_clean()
        self.assertSequenceEqual(
            f.get_queryset(),
            (self.aab, self.aac, self.abc, self.bca, self.cab),
        )
