import django
import pytest
from crispy_forms import __version__
from crispy_forms.bootstrap import Field, InlineCheckboxes, UneditableField
from crispy_forms.helper import FormHelper
from crispy_forms.layout import HTML, Column, Fieldset, Layout, Row, Submit
from crispy_forms.utils import render_crispy_form
from django import forms
from django.forms.models import formset_factory, modelformset_factory
from django.middleware.csrf import _get_new_csrf_string
from django.shortcuts import render
from django.template import Context, Template
from django.test.html import parse_html
from django.test.utils import override_settings
from django.urls import reverse
from django.utils.translation import gettext_lazy as _

from .forms import (
    AdvancedFileForm,
    CheckboxesSampleForm,
    CrispyEmptyChoiceTestModel,
    CrispyTestModel,
    FileForm,
    SampleForm,
    SampleForm2,
    SampleForm3,
    SampleForm4,
    SampleForm5,
    SampleForm6,
    SelectSampleForm,
)
from .test_settings import TEMPLATE_DIRS
from .utils import contains_partial, parse_expected, parse_form

CONVERTERS = {
    "textinput": "textinput textInput",
    "fileinput": "fileinput fileUpload",
    "passwordinput": "textinput textInput",
}


def test_invalid_unicode_characters(settings):
    # Adds a BooleanField that uses non valid unicode characters "ñ"
    form_helper = FormHelper()
    form_helper.add_layout(Layout("españa"))

    template = Template(
        """
        {% load crispy_forms_tags %}
        {% crispy form form_helper %}
    """
    )
    c = Context({"form": SampleForm(), "form_helper": form_helper})
    settings.CRISPY_FAIL_SILENTLY = False
    with pytest.raises(Exception):
        template.render(c)


def test_unicode_form_field():
    class UnicodeForm(forms.Form):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.fields["contraseña"] = forms.CharField()

        helper = FormHelper()
        helper.layout = Layout("contraseña")

    html = render_crispy_form(UnicodeForm())
    assert 'id="id_contraseña"' in html


def test_meta_extra_fields_with_missing_fields():
    class FormWithMeta(SampleForm):
        class Meta:
            fields = ("email", "first_name", "last_name")

    form = FormWithMeta()
    # We remove email field on the go
    del form.fields["email"]

    form_helper = FormHelper()
    form_helper.layout = Layout("first_name")

    template = Template(
        """
        {% load crispy_forms_tags %}
        {% crispy form form_helper %}
    """
    )
    c = Context({"form": form, "form_helper": form_helper})
    html = template.render(c)
    assert "email" not in html


def test_layout_unresolved_field(settings):
    form_helper = FormHelper()
    form_helper.add_layout(Layout("typo"))

    template = Template(
        """
        {% load crispy_forms_tags %}
        {% crispy form form_helper %}
    """
    )
    c = Context({"form": SampleForm(), "form_helper": form_helper})
    settings.CRISPY_FAIL_SILENTLY = False
    with pytest.raises(Exception):
        template.render(c)


def test_double_rendered_field(settings):
    form_helper = FormHelper()
    form_helper.add_layout(Layout("is_company", "is_company"))

    template = Template(
        """
        {% load crispy_forms_tags %}
        {% crispy form form_helper %}
    """
    )
    c = Context({"form": SampleForm(), "form_helper": form_helper})
    settings.CRISPY_FAIL_SILENTLY = False
    with pytest.raises(Exception):
        template.render(c)


def test_context_pollution():
    class ExampleForm(forms.Form):
        comment = forms.CharField()

    form = ExampleForm()
    form2 = SampleForm()

    template = Template(
        """
        {% load crispy_forms_tags %}
        {{ form.as_ul }}
        {% crispy form2 %}
        {{ form.as_ul }}
    """
    )
    c = Context({"form": form, "form2": form2})
    html = template.render(c)

    assert html.count('name="comment"') == 2
    assert html.count('name="is_company"') == 1


def test_layout_fieldset_row_html_with_unicode_fieldnames():
    form_helper = FormHelper()
    form_helper.add_layout(
        Layout(
            Fieldset(
                "Company Data",
                "is_company",
                css_id="fieldset_company_data",
                css_class="fieldsets",
                title="fieldset_title",
                test_fieldset="123",
            ),
            Fieldset(
                "User Data",
                "email",
                Row(
                    "password1",
                    "password2",
                    css_id="row_passwords",
                    css_class="rows",
                ),
                HTML('<a href="#" id="testLink">test link</a>'),
                HTML(
                    """
                    {% if flag %}{{ message }}{% endif %}
                """
                ),
                "first_name",
                "last_name",
            ),
        )
    )

    template = Template(
        """
        {% load crispy_forms_tags %}
        {% crispy form form_helper %}
    """
    )
    c = Context(
        {
            "form": SampleForm(),
            "form_helper": form_helper,
            "flag": True,
            "message": "Hello!",
        }
    )
    html = template.render(c)

    assert 'id="fieldset_company_data"' in html
    assert 'class="fieldsets' in html
    assert 'title="fieldset_title"' in html
    assert 'test-fieldset="123"' in html
    assert 'id="row_passwords"' in html
    assert html.count("<label") == 6

    assert 'class="form-row rows"' in html
    assert "Hello!" in html
    assert "testLink" in html


def test_change_layout_dynamically_delete_field():
    template = Template(
        """
        {% load crispy_forms_tags %}
        {% crispy form form_helper %}
    """
    )

    form = SampleForm()
    form_helper = FormHelper()
    form_helper.add_layout(
        Layout(
            Fieldset(
                "Company Data",
                "is_company",
                "email",
                "password1",
                "password2",
                css_id="multifield_info",
            ),
            Column(
                "first_name",
                "last_name",
                css_id="column_name",
            ),
        )
    )

    # We remove email field on the go
    # Layout needs to be adapted for the new form fields
    del form.fields["email"]
    del form_helper.layout.fields[0].fields[1]

    c = Context({"form": form, "form_helper": form_helper})
    html = template.render(c)
    assert "email" not in html


def test_column_has_css_classes():
    template = Template(
        """
        {% load crispy_forms_tags %}
        {% crispy form form_helper %}
    """
    )

    form = SampleForm()
    form_helper = FormHelper()
    form_helper.add_layout(
        Layout(
            Fieldset(
                "Company Data",
                "is_company",
                "email",
                "password1",
                "password2",
                css_id="multifield_info",
            ),
            Column("first_name", "last_name"),
        )
    )

    c = Context({"form": form, "form_helper": form_helper})
    html = template.render(c)
    assert html.count("col-md") == 1


def test_bs4_column_css_classes():
    template = Template(
        """
        {% load crispy_forms_tags %}
        {% crispy form form_helper %}
    """
    )

    form = SampleForm()
    form_helper = FormHelper()
    form_helper.add_layout(
        Layout(
            Column("first_name", "last_name"),
            Column("first_name", "last_name", css_class="col-sm"),
            Column("first_name", "last_name", css_class="mb-4"),
        )
    )

    c = Context({"form": form, "form_helper": form_helper})
    html = template.render(c)

    assert html.count("col-md") == 2
    assert html.count("col-sm") == 1


def test_formset_layout():
    SampleFormSet = formset_factory(SampleForm, extra=3)
    formset = SampleFormSet()
    helper = FormHelper()
    helper.form_id = "thisFormsetRocks"
    helper.form_class = "formsets-that-rock"
    helper.form_method = "POST"
    helper.form_action = "simpleAction"
    helper.layout = Layout(
        Fieldset(
            "Item {{ forloop.counter }}",
            "is_company",
            "email",
        ),
        HTML("{% if forloop.first %}Note for first form only{% endif %}"),
        Row("password1", "password2"),
        Fieldset("", "first_name", "last_name"),
    )

    html = render_crispy_form(
        form=formset, helper=helper, context={"csrf_token": _get_new_csrf_string()}
    )

    # Check formset fields
    assert contains_partial(
        html,
        '<input id="id_form-TOTAL_FORMS" name="form-TOTAL_FORMS" '
        'type="hidden" value="3"/>',
    )
    assert contains_partial(
        html,
        '<input id="id_form-INITIAL_FORMS" name="form-INITIAL_FORMS" '
        'type="hidden" value="0"/>',
    )
    assert contains_partial(
        html,
        '<input id="id_form-MAX_NUM_FORMS" name="form-MAX_NUM_FORMS" '
        'type="hidden" value="1000"/>',
    )
    assert contains_partial(
        html,
        '<input id="id_form-MIN_NUM_FORMS" name="form-MIN_NUM_FORMS" '
        'type="hidden" value="0"/>',
    )
    assert html.count("hidden") == 5

    # Check form structure
    assert html.count("<form") == 1
    assert html.count("csrfmiddlewaretoken") == 1
    assert "formsets-that-rock" in html
    assert 'method="post"' in html
    assert 'id="thisFormsetRocks"' in html
    assert 'action="%s"' % reverse("simpleAction") in html

    # Check form layout
    assert "Item 1" in html
    assert "Item 2" in html
    assert "Item 3" in html
    assert html.count("Note for first form only") == 1
    assert html.count("row") == 3

    assert html.count("form-group") == 18


def test_modelformset_layout():
    CrispyModelFormSet = modelformset_factory(
        CrispyTestModel, form=SampleForm4, extra=3
    )
    formset = CrispyModelFormSet(queryset=CrispyTestModel.objects.none())
    helper = FormHelper()
    helper.layout = Layout("email")

    html = render_crispy_form(form=formset, helper=helper)

    assert html.count("id_form-0-id") == 1
    assert html.count("id_form-1-id") == 1
    assert html.count("id_form-2-id") == 1

    assert contains_partial(
        html,
        '<input id="id_form-TOTAL_FORMS" name="form-TOTAL_FORMS" '
        'type="hidden" value="3"/>',
    )
    assert contains_partial(
        html,
        '<input id="id_form-INITIAL_FORMS" name="form-INITIAL_FORMS" '
        'type="hidden" value="0"/>',
    )
    assert contains_partial(
        html,
        '<input id="id_form-MAX_NUM_FORMS" name="form-MAX_NUM_FORMS" '
        'type="hidden" value="1000"/>',
    )

    assert html.count('name="form-0-email"') == 1
    assert html.count('name="form-1-email"') == 1
    assert html.count('name="form-2-email"') == 1
    assert html.count('name="form-3-email"') == 0
    assert html.count("password") == 0


def test_i18n():
    template = Template(
        """
        {% load crispy_forms_tags %}
        {% crispy form form.helper %}
    """
    )
    form = SampleForm()
    form_helper = FormHelper()
    form_helper.layout = Layout(
        HTML(_("i18n text")),
        Fieldset(
            _("i18n legend"),
            "first_name",
            "last_name",
        ),
    )
    form.helper = form_helper

    html = template.render(Context({"form": form}))
    assert html.count("i18n legend") == 1


def test_default_layout():
    test_form = SampleForm2()
    assert test_form.helper.layout.fields == [
        "is_company",
        "email",
        "password1",
        "password2",
        "first_name",
        "last_name",
        "datetime_field",
    ]


def test_default_layout_two():
    test_form = SampleForm3()
    assert test_form.helper.layout.fields == ["email"]


def test_modelform_layout_without_meta():
    test_form = SampleForm4()
    test_form.helper = FormHelper()
    test_form.helper.layout = Layout("email")
    html = render_crispy_form(test_form)

    assert "email" in html
    assert "password" not in html


def test_specialspaceless_not_screwing_intended_spaces():
    # see issue #250
    test_form = SampleForm()
    test_form.fields["email"].widget = forms.Textarea()
    test_form.helper = FormHelper()
    test_form.helper.layout = Layout(
        "email", HTML("<span>first span</span> <span>second span</span>")
    )
    html = render_crispy_form(test_form)
    assert "<span>first span</span> <span>second span</span>" in html


def test_choice_with_none_is_selected():
    # see issue #701
    model_instance = CrispyEmptyChoiceTestModel()
    model_instance.fruit = None
    test_form = SampleForm6(instance=model_instance)
    html = render_crispy_form(test_form)
    assert "checked" in html


@override_settings(
    TEMPLATES=[
        {
            "BACKEND": "django.template.backends.django.DjangoTemplates",
            "DIRS": TEMPLATE_DIRS,
            "OPTIONS": {
                "loaders": [
                    "django.template.loaders.filesystem.Loader",
                    "django.template.loaders.app_directories.Loader",
                ],
            },
        }
    ]
)
def test_keepcontext_context_manager():
    # Test case for issue #180
    # Apparently it only manifest when using render_to_response this exact way
    form = CheckboxesSampleForm()
    form.helper = FormHelper()
    # We use here InlineCheckboxes as it updates context in an unsafe way
    form.helper.layout = Layout(
        "checkboxes", InlineCheckboxes("alphacheckboxes"), "numeric_multiple_checkboxes"
    )
    context = {"form": form}

    response = render(
        request=None, template_name="crispy_render_template.html", context=context
    )

    assert response.content.count(b"custom-control-inline") == 3
    assert response.content.count(b"custom-checkbox") > 0


def test_use_custom_control_is_used_in_checkboxes():
    form = CheckboxesSampleForm()
    form.helper = FormHelper()
    form.helper.layout = Layout(
        "checkboxes",
        InlineCheckboxes("alphacheckboxes"),
        "numeric_multiple_checkboxes",
    )
    # form.helper.use_custom_control take default value which is True
    assert parse_form(form) == parse_expected(
        "bootstrap4/test_layout/test_use_custom_control_is_used_in_checkboxes_true.html"
    )

    form.helper.use_custom_control = True
    assert parse_form(form) == parse_expected(
        "bootstrap4/test_layout/test_use_custom_control_is_used_in_checkboxes_true.html"
    )

    form.helper.use_custom_control = False
    assert parse_form(form) == parse_expected(
        "bootstrap4/test_layout/"
        "test_use_custom_control_is_used_in_checkboxes_false.html"
    )

    form = CheckboxesSampleForm({})
    form.helper = FormHelper()
    form.helper.layout = Layout(
        "checkboxes",
        InlineCheckboxes("alphacheckboxes"),
        "numeric_multiple_checkboxes",
    )
    if django.VERSION < (5, 0):
        expected = (
            "bootstrap4/test_layout/"
            "test_use_custom_control_is_used_in_checkboxes_true_failing_lt50.html"
        )
    else:
        expected = (
            "bootstrap4/test_layout/"
            "test_use_custom_control_is_used_in_checkboxes_true_failing.html"
        )
    assert parse_form(form) == parse_expected(expected)


def test_use_custom_control_is_used_in_radio():
    form = SampleForm5()
    form.helper = FormHelper()
    form.helper.layout = Layout(
        "radio_select",
    )
    # form.helper.use_custom_control take default value which is True
    assert parse_form(form) == parse_expected(
        "bootstrap4/test_layout/test_use_custom_control_is_used_in_radio_true.html"
    )

    form.helper.use_custom_control = True
    assert parse_form(form) == parse_expected(
        "bootstrap4/test_layout/test_use_custom_control_is_used_in_radio_true.html"
    )

    form.helper.use_custom_control = False
    assert parse_form(form) == parse_expected(
        "bootstrap4/test_layout/test_use_custom_control_is_used_in_radio_false.html"
    )

    form = SampleForm5({})
    form.helper = FormHelper()
    form.helper.layout = Layout(
        "radio_select",
    )
    if django.VERSION < (5, 0):
        expected = (
            "bootstrap4/test_layout/"
            "test_use_custom_control_is_used_in_radio_true_failing_lt50.html"
        )
    else:
        expected = (
            "bootstrap4/test_layout/"
            "test_use_custom_control_is_used_in_radio_true_failing.html"
        )
    assert parse_form(form) == parse_expected(expected)


@pytest.mark.parametrize(
    "use_custom_control, expected_html",
    [
        (True, "bootstrap4/test_layout/test_use_custom_control_in_select_true.html"),
        (False, "bootstrap4/test_layout/test_use_custom_control_in_select_false.html"),
    ],
)
def test_use_custom_control_in_select(use_custom_control, expected_html):
    form = SelectSampleForm()

    form.helper = FormHelper()
    form.helper.template_pack = "bootstrap4"
    form.helper.layout = Layout("select")
    form.helper.use_custom_control = use_custom_control

    assert parse_form(form) == parse_expected(expected_html)


def test_bootstrap4_form_inline():
    form = SampleForm()
    form.helper = FormHelper()
    form.helper.form_class = "form-inline"
    form.helper.field_template = "bootstrap4/layout/inline_field.html"
    form.helper.layout = Layout("email", "password1", "last_name")

    html = render_crispy_form(form)
    assert html.count('class="form-inline"') == 1
    assert html.count('class="input-group"') == 3
    assert html.count('<label for="id_email" class="sr-only') == 1
    assert html.count('id="div_id_email" class="input-group"') == 1
    assert html.count('placeholder="email"') == 1
    assert html.count("</label> <input") == 3


def test_update_attributes_class():
    form = SampleForm()
    form.helper = FormHelper()
    form.helper.layout = Layout("email", Field("password1"), "password2")
    form.helper["password1"].update_attributes(css_class="hello")
    html = render_crispy_form(form)
    assert html.count(' class="hello') == 1
    form.helper = FormHelper()
    form.helper.layout = Layout(
        "email",
        Field("password1", css_class="hello"),
        "password2",
    )
    form.helper["password1"].update_attributes(css_class="hello2")
    html = render_crispy_form(form)
    assert html.count(' class="hello hello2') == 1


@override_settings(CRISPY_CLASS_CONVERTERS=CONVERTERS)
def test_file_field():
    form = FileForm()
    form.helper = FormHelper()
    form.helper.field_class = "col-lg-9 mb-2"
    form.helper.layout = Layout("clearable_file")
    assert parse_form(form) == parse_expected(
        "bootstrap4/test_layout/test_file_field_clearable_custom_control.html"
    )

    form.helper.use_custom_control = False
    assert parse_form(form) == parse_expected(
        "bootstrap4/test_layout/test_file_field_clearable.html"
    )

    form.helper.use_custom_control = True
    form.helper.layout = Layout("file_field")
    assert parse_form(form) == parse_expected(
        "bootstrap4/test_layout/test_file_field_custom_control.html"
    )

    form.helper.use_custom_control = False
    assert parse_form(form) == parse_expected(
        "bootstrap4/test_layout/test_file_field_default.html"
    )


def test_file_field_with_custom_class():
    form = AdvancedFileForm()
    form.helper = FormHelper()
    form.helper.layout = Layout("clearable_file")
    assert parse_form(form) == parse_expected(
        "bootstrap4/test_layout/test_file_field_with_custom_class_clearable.html"
    )

    form.helper.layout = Layout("file_field")
    assert parse_form(form) == parse_expected(
        "bootstrap4/test_layout/test_file_field_with_custom_class.html"
    )


def test_form_control_size():
    "CSS classes form-control and form-control-lg are both required"
    form = SampleForm()
    form.helper = FormHelper()
    form.helper.layout = Layout(Field("first_name", css_class="form-control-lg"))
    assert parse_form(form) == parse_expected(
        "bootstrap4/test_layout/test_form_control_size.html"
    )


@pytest.mark.skipif(
    __version__[0] == "1",
    reason="#1224 changed editable field behaviour post crispy forms 1.x",
)
def test_uneditable_field():
    form = SampleForm()
    form.helper = FormHelper()
    form.helper.layout = Layout(
        UneditableField("first_name"),
    )
    assert parse_form(form) == parse_expected(
        "bootstrap4/test_layout/test_uneditable_field.html"
    )


@pytest.mark.parametrize(
    "use_custom_control, expected_html",
    [
        (
            True,
            "bootstrap4/test_layout/"
            "test_use_custom_control_in_uneditable_select_true.html",
        ),
        (
            False,
            "bootstrap4/test_layout/"
            "test_use_custom_control_in_uneditable_select_false.html",
        ),
    ],
)
@pytest.mark.skipif(
    __version__[0] == "1",
    reason="#1224 changed editable field behaviour post crispy forms 1.x",
)
def test_use_custom_control_in_uneditable_select(use_custom_control, expected_html):
    form = SelectSampleForm()
    form.helper = FormHelper()
    form.helper.template_pack = "bootstrap4"
    form.helper.layout = Layout(UneditableField("select"))
    form.helper.use_custom_control = use_custom_control
    assert parse_form(form) == parse_expected(expected_html)


def test_multiple_fields(settings):
    "Field can accept any number of fields and apply the kwargs to all fields"
    form = SampleForm()
    form.helper = FormHelper()
    form.helper.layout = Layout(
        Field("first_name", "last_name", css_class="form-control-lg")
    )
    template_pack = settings.CRISPY_TEMPLATE_PACK
    assert parse_form(form) == parse_expected(
        f"{template_pack}/test_layout/test_multiple_fields.html"
    )


def test_fundamentals():
    """
    This is the example that is in the `crispy_tag_forms` docs.
    """

    class ExampleForm(forms.Form):
        like_website = forms.TypedChoiceField(
            label="Do you like this website?",
            choices=((1, "Yes"), (0, "No")),
            coerce=lambda x: bool(int(x)),
            widget=forms.RadioSelect,
            initial="1",
            required=True,
        )

        favorite_food = forms.CharField(
            label="What is your favorite food?",
            max_length=80,
            required=True,
        )

        favorite_color = forms.CharField(
            label="What is your favorite color?",
            max_length=80,
            required=True,
        )

        favorite_number = forms.IntegerField(
            label="Favorite number",
            required=False,
        )

        notes = forms.CharField(
            label="Additional notes or feedback",
            required=False,
        )

        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.helper = FormHelper()
            self.helper.form_id = "id-exampleForm"
            self.helper.form_class = "blueForms"
            self.helper.form_method = "post"
            self.helper.form_action = "submit_survey"

            self.helper.add_input(Submit("submit", "Submit"))

    form = ExampleForm()
    context = {"csrf_token": "NotARealToken"}
    html = render_crispy_form(form, context=context)
    assert parse_html(html) == parse_expected(
        "bootstrap4/test_layout/test_fundamentals_example.html"
    )


@pytest.mark.skipif(
    __version__[0] == "1", reason="#1250 removed custom-control post crispy forms 1.x"
)
def test_table_inline_formset_checkbox():
    class TestForm(forms.Form):
        box_one = forms.CharField(label="box one", widget=forms.CheckboxInput())
        box_two = forms.CharField(label="box two", widget=forms.CheckboxInput())

    formset = formset_factory(TestForm)
    formset.helper = FormHelper()
    formset.helper.template = "bootstrap4/table_inline_formset.html"
    assert parse_form(formset) == parse_expected(
        "bootstrap4/test_layout/test_inline_formset_checkbox.html"
    )


def test_radio_attrs():
    class TestForm(forms.Form):
        radios = forms.ChoiceField(
            choices=(
                ("option_one", "Option one"),
                ("option_two", "Option two"),
            ),
            widget=forms.RadioSelect(
                attrs={
                    "class": "sr-only sr-only-focusable",
                }
            ),
        )

    form = TestForm()
    form.helper = FormHelper()
    assert parse_form(form) == parse_expected(
        "bootstrap4/test_layout/test_radio_attrs.html"
    )
