import datetime
from collections import Counter
from unittest import mock

from django.core.exceptions import ValidationError
from django.forms import (
    BaseForm, CharField, DateField, FileField, Form, IntegerField,
    SplitDateTimeField, formsets,
)
from django.forms.formsets import BaseFormSet, all_valid, formset_factory
from django.forms.utils import ErrorList
from django.forms.widgets import HiddenInput
from django.test import SimpleTestCase


class Choice(Form):
    choice = CharField()
    votes = IntegerField()


ChoiceFormSet = formset_factory(Choice)


class FavoriteDrinkForm(Form):
    name = CharField()


class BaseFavoriteDrinksFormSet(BaseFormSet):
    def clean(self):
        seen_drinks = []

        for drink in self.cleaned_data:
            if drink['name'] in seen_drinks:
                raise ValidationError('You may only specify a drink once.')

            seen_drinks.append(drink['name'])


# A FormSet that takes a list of favorite drinks and raises an error if
# there are any duplicates.
FavoriteDrinksFormSet = formset_factory(FavoriteDrinkForm, formset=BaseFavoriteDrinksFormSet, extra=3)


class CustomKwargForm(Form):
    def __init__(self, *args, custom_kwarg, **kwargs):
        self.custom_kwarg = custom_kwarg
        super().__init__(*args, **kwargs)


class FormsFormsetTestCase(SimpleTestCase):

    def make_choiceformset(
            self, formset_data=None, formset_class=ChoiceFormSet,
            total_forms=None, initial_forms=0, max_num_forms=0, min_num_forms=0, **kwargs):
        """
        Make a ChoiceFormset from the given formset_data.
        The data should be given as a list of (choice, votes) tuples.
        """
        kwargs.setdefault('prefix', 'choices')
        kwargs.setdefault('auto_id', False)

        if formset_data is None:
            return formset_class(**kwargs)

        if total_forms is None:
            total_forms = len(formset_data)

        def prefixed(*args):
            args = (kwargs['prefix'],) + args
            return '-'.join(args)

        data = {
            prefixed('TOTAL_FORMS'): str(total_forms),
            prefixed('INITIAL_FORMS'): str(initial_forms),
            prefixed('MAX_NUM_FORMS'): str(max_num_forms),
            prefixed('MIN_NUM_FORMS'): str(min_num_forms),
        }
        for i, (choice, votes) in enumerate(formset_data):
            data[prefixed(str(i), 'choice')] = choice
            data[prefixed(str(i), 'votes')] = votes

        return formset_class(data, **kwargs)

    def test_basic_formset(self):
        """
        A FormSet constructor takes the same arguments as Form. Create a
        FormSet for adding data. By default, it displays 1 blank form.
        """
        formset = self.make_choiceformset()
        self.assertHTMLEqual(
            str(formset),
            """<input type="hidden" name="choices-TOTAL_FORMS" value="1">
<input type="hidden" name="choices-INITIAL_FORMS" value="0">
<input type="hidden" name="choices-MIN_NUM_FORMS" value="0">
<input type="hidden" name="choices-MAX_NUM_FORMS" value="1000">
<tr><th>Choice:</th><td><input type="text" name="choices-0-choice"></td></tr>
<tr><th>Votes:</th><td><input type="number" name="choices-0-votes"></td></tr>"""
        )
        # FormSet are treated similarly to Forms. FormSet has an is_valid()
        # method, and a cleaned_data or errors attribute depending on whether
        # all the forms passed validation. However, unlike a Form, cleaned_data
        # and errors will be a list of dicts rather than a single dict.
        formset = self.make_choiceformset([('Calexico', '100')])
        self.assertTrue(formset.is_valid())
        self.assertEqual([form.cleaned_data for form in formset.forms], [{'votes': 100, 'choice': 'Calexico'}])

        # If a FormSet wasn't passed any data, is_valid() and has_changed()
        # return False.
        formset = self.make_choiceformset()
        self.assertFalse(formset.is_valid())
        self.assertFalse(formset.has_changed())

    def test_form_kwargs_formset(self):
        """
        Custom kwargs set on the formset instance are passed to the
        underlying forms.
        """
        FormSet = formset_factory(CustomKwargForm, extra=2)
        formset = FormSet(form_kwargs={'custom_kwarg': 1})
        for form in formset:
            self.assertTrue(hasattr(form, 'custom_kwarg'))
            self.assertEqual(form.custom_kwarg, 1)

    def test_form_kwargs_formset_dynamic(self):
        """Form kwargs can be passed dynamically in a formset."""
        class DynamicBaseFormSet(BaseFormSet):
            def get_form_kwargs(self, index):
                return {'custom_kwarg': index}

        DynamicFormSet = formset_factory(CustomKwargForm, formset=DynamicBaseFormSet, extra=2)
        formset = DynamicFormSet(form_kwargs={'custom_kwarg': 'ignored'})
        for i, form in enumerate(formset):
            self.assertTrue(hasattr(form, 'custom_kwarg'))
            self.assertEqual(form.custom_kwarg, i)

    def test_form_kwargs_empty_form(self):
        FormSet = formset_factory(CustomKwargForm)
        formset = FormSet(form_kwargs={'custom_kwarg': 1})
        self.assertTrue(hasattr(formset.empty_form, 'custom_kwarg'))
        self.assertEqual(formset.empty_form.custom_kwarg, 1)

    def test_formset_validation(self):
        # FormSet instances can also have an error attribute if validation failed for
        # any of the forms.
        formset = self.make_choiceformset([('Calexico', '')])
        self.assertFalse(formset.is_valid())
        self.assertEqual(formset.errors, [{'votes': ['This field is required.']}])

    def test_formset_validation_count(self):
        """
        A formset's ManagementForm is validated once per FormSet.is_valid()
        call and each form of the formset is cleaned once.
        """
        def make_method_counter(func):
            """Add a counter to func for the number of times it's called."""
            counter = Counter()
            counter.call_count = 0

            def mocked_func(*args, **kwargs):
                counter.call_count += 1
                return func(*args, **kwargs)

            return mocked_func, counter

        mocked_is_valid, is_valid_counter = make_method_counter(formsets.ManagementForm.is_valid)
        mocked_full_clean, full_clean_counter = make_method_counter(BaseForm.full_clean)
        formset = self.make_choiceformset([('Calexico', '100'), ('Any1', '42'), ('Any2', '101')])

        with mock.patch('django.forms.formsets.ManagementForm.is_valid', mocked_is_valid), \
                mock.patch('django.forms.forms.BaseForm.full_clean', mocked_full_clean):
            self.assertTrue(formset.is_valid())
        self.assertEqual(is_valid_counter.call_count, 1)
        self.assertEqual(full_clean_counter.call_count, 4)

    def test_formset_has_changed(self):
        """
        FormSet.has_changed() is True if any data is passed to its forms, even
        if the formset didn't validate.
        """
        blank_formset = self.make_choiceformset([('', '')])
        self.assertFalse(blank_formset.has_changed())
        # invalid formset
        invalid_formset = self.make_choiceformset([('Calexico', '')])
        self.assertFalse(invalid_formset.is_valid())
        self.assertTrue(invalid_formset.has_changed())
        # valid formset
        valid_formset = self.make_choiceformset([('Calexico', '100')])
        self.assertTrue(valid_formset.is_valid())
        self.assertTrue(valid_formset.has_changed())

    def test_formset_initial_data(self):
        """
        A FormSet can be prefilled with existing data by providing a list of
        dicts to the `initial` argument. By default, an extra blank form is
        included.
        """
        formset = self.make_choiceformset(initial=[{'choice': 'Calexico', 'votes': 100}])
        self.assertHTMLEqual(
            '\n'.join(form.as_ul() for form in formset.forms),
            """<li>Choice: <input type="text" name="choices-0-choice" value="Calexico"></li>
<li>Votes: <input type="number" name="choices-0-votes" value="100"></li>
<li>Choice: <input type="text" name="choices-1-choice"></li>
<li>Votes: <input type="number" name="choices-1-votes"></li>"""
        )

    def test_blank_form_unfilled(self):
        """A form that's displayed as blank may be submitted as blank."""
        formset = self.make_choiceformset([('Calexico', '100'), ('', '')], initial_forms=1)
        self.assertTrue(formset.is_valid())
        self.assertEqual([form.cleaned_data for form in formset.forms], [{'votes': 100, 'choice': 'Calexico'}, {}])

    def test_second_form_partially_filled(self):
        """
        If at least one field is filled out on a blank form, it will be
        validated.
        """
        formset = self.make_choiceformset([('Calexico', '100'), ('The Decemberists', '')], initial_forms=1)
        self.assertFalse(formset.is_valid())
        self.assertEqual(formset.errors, [{}, {'votes': ['This field is required.']}])

    def test_delete_prefilled_data(self):
        """
        Deleting prefilled data is an error. Removing data from form fields
        isn't the proper way to delete it.
        """
        formset = self.make_choiceformset([('', ''), ('', '')], initial_forms=1)
        self.assertFalse(formset.is_valid())
        self.assertEqual(
            formset.errors,
            [{'votes': ['This field is required.'], 'choice': ['This field is required.']}, {}]
        )

    def test_displaying_more_than_one_blank_form(self):
        """
        More than 1 empty form can be displayed using formset_factory's
        `extra` argument.
        """
        ChoiceFormSet = formset_factory(Choice, extra=3)
        formset = ChoiceFormSet(auto_id=False, prefix='choices')
        self.assertHTMLEqual(
            '\n'.join(form.as_ul() for form in formset.forms),
            """<li>Choice: <input type="text" name="choices-0-choice"></li>
<li>Votes: <input type="number" name="choices-0-votes"></li>
<li>Choice: <input type="text" name="choices-1-choice"></li>
<li>Votes: <input type="number" name="choices-1-votes"></li>
<li>Choice: <input type="text" name="choices-2-choice"></li>
<li>Votes: <input type="number" name="choices-2-votes"></li>"""
        )
        # Since every form was displayed as blank, they are also accepted as
        # blank. This may seem a little strange, but min_num is used to require
        # a minimum number of forms to be completed.
        data = {
            'choices-TOTAL_FORMS': '3',  # the number of forms rendered
            'choices-INITIAL_FORMS': '0',  # the number of forms with initial data
            'choices-MIN_NUM_FORMS': '0',  # min number of forms
            'choices-MAX_NUM_FORMS': '0',  # max number of forms
            'choices-0-choice': '',
            'choices-0-votes': '',
            'choices-1-choice': '',
            'choices-1-votes': '',
            'choices-2-choice': '',
            'choices-2-votes': '',
        }
        formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
        self.assertTrue(formset.is_valid())
        self.assertEqual([form.cleaned_data for form in formset.forms], [{}, {}, {}])

    def test_min_num_displaying_more_than_one_blank_form(self):
        """
        More than 1 empty form can also be displayed using formset_factory's
        min_num argument. It will (essentially) increment the extra argument.
        """
        ChoiceFormSet = formset_factory(Choice, extra=1, min_num=1)
        formset = ChoiceFormSet(auto_id=False, prefix='choices')
        # Min_num forms are required; extra forms can be empty.
        self.assertFalse(formset.forms[0].empty_permitted)
        self.assertTrue(formset.forms[1].empty_permitted)
        self.assertHTMLEqual(
            '\n'.join(form.as_ul() for form in formset.forms),
            """<li>Choice: <input type="text" name="choices-0-choice"></li>
<li>Votes: <input type="number" name="choices-0-votes"></li>
<li>Choice: <input type="text" name="choices-1-choice"></li>
<li>Votes: <input type="number" name="choices-1-votes"></li>"""
        )

    def test_min_num_displaying_more_than_one_blank_form_with_zero_extra(self):
        """More than 1 empty form can be displayed using min_num."""
        ChoiceFormSet = formset_factory(Choice, extra=0, min_num=3)
        formset = ChoiceFormSet(auto_id=False, prefix='choices')
        self.assertHTMLEqual(
            '\n'.join(form.as_ul() for form in formset.forms),
            """<li>Choice: <input type="text" name="choices-0-choice"></li>
<li>Votes: <input type="number" name="choices-0-votes"></li>
<li>Choice: <input type="text" name="choices-1-choice"></li>
<li>Votes: <input type="number" name="choices-1-votes"></li>
<li>Choice: <input type="text" name="choices-2-choice"></li>
<li>Votes: <input type="number" name="choices-2-votes"></li>"""
        )

    def test_single_form_completed(self):
        """Just one form may be completed."""
        data = {
            'choices-TOTAL_FORMS': '3',  # the number of forms rendered
            'choices-INITIAL_FORMS': '0',  # the number of forms with initial data
            'choices-MIN_NUM_FORMS': '0',  # min number of forms
            'choices-MAX_NUM_FORMS': '0',  # max number of forms
            'choices-0-choice': 'Calexico',
            'choices-0-votes': '100',
            'choices-1-choice': '',
            'choices-1-votes': '',
            'choices-2-choice': '',
            'choices-2-votes': '',
        }
        ChoiceFormSet = formset_factory(Choice, extra=3)
        formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
        self.assertTrue(formset.is_valid())
        self.assertEqual([form.cleaned_data for form in formset.forms], [{'votes': 100, 'choice': 'Calexico'}, {}, {}])

    def test_formset_validate_max_flag(self):
        """
        If validate_max is set and max_num is less than TOTAL_FORMS in the
        data, a ValidationError is raised. MAX_NUM_FORMS in the data is
        irrelevant here (it's output as a hint for the client but its value
        in the returned data is not checked).
        """
        data = {
            'choices-TOTAL_FORMS': '2',  # the number of forms rendered
            'choices-INITIAL_FORMS': '0',  # the number of forms with initial data
            'choices-MIN_NUM_FORMS': '0',  # min number of forms
            'choices-MAX_NUM_FORMS': '2',  # max number of forms - should be ignored
            'choices-0-choice': 'Zero',
            'choices-0-votes': '0',