from django import forms
from django.db import models
from django.db.models import functions
from polymorphic.models import PolymorphicModel, PolymorphicTypeInvalid
from polymorphic.tests.models import (
    Bottom,
    Middle,
    Top,
    Team,
    UserProfile,
    Model2A,
    Model2B,
    Regression295Parent,
    Regression295Related,
    RelationBase,
    RelationA,
    RelationB,
    SpecialBook,
    Book,
)
from django.test import TestCase
from django.contrib.contenttypes.models import ContentType
from polymorphic.formsets import polymorphic_modelformset_factory, PolymorphicFormSetChild


class RegressionTests(TestCase):
    def test_for_query_result_incomplete_with_inheritance(self):
        """https://github.com/bconstantin/django_polymorphic/issues/15"""

        top = Top()
        top.save()
        middle = Middle()
        middle.save()
        bottom = Bottom()
        bottom.save()

        expected_queryset = [top, middle, bottom]
        self.assertQuerySetEqual(
            Top.objects.order_by("pk"),
            [repr(r) for r in expected_queryset],
            transform=repr,
        )

        expected_queryset = [middle, bottom]
        self.assertQuerySetEqual(
            Middle.objects.order_by("pk"),
            [repr(r) for r in expected_queryset],
            transform=repr,
        )

        expected_queryset = [bottom]
        self.assertQuerySetEqual(
            Bottom.objects.order_by("pk"),
            [repr(r) for r in expected_queryset],
            transform=repr,
        )

    def test_pr_254(self):
        user_a = UserProfile.objects.create(name="a")
        user_b = UserProfile.objects.create(name="b")
        user_c = UserProfile.objects.create(name="c")

        team1 = Team.objects.create(team_name="team1")
        team1.user_profiles.add(user_a, user_b, user_c)
        team1.save()

        team2 = Team.objects.create(team_name="team2")
        team2.user_profiles.add(user_c)
        team2.save()

        # without prefetch_related, the test passes
        my_teams = (
            Team.objects.filter(user_profiles=user_c)
            .order_by("team_name")
            .prefetch_related("user_profiles")
            .distinct()
        )

        self.assertEqual(len(my_teams[0].user_profiles.all()), 3)

        self.assertEqual(len(my_teams[1].user_profiles.all()), 1)

        self.assertEqual(len(my_teams[0].user_profiles.all()), 3)
        self.assertEqual(len(my_teams[1].user_profiles.all()), 1)

        # without this "for" loop, the test passes
        for _ in my_teams:
            pass

        # This time, test fails.  PR 254 claim
        # with sqlite:      4 != 3
        # with postgresql:  2 != 3
        self.assertEqual(len(my_teams[0].user_profiles.all()), 3)
        self.assertEqual(len(my_teams[1].user_profiles.all()), 1)

    def test_alias_queryset(self):
        """
        Test that .alias() works works correctly with polymorphic querysets.
        It should not raise AttributeError, and the aliased field should NOT be present on the instance.
        """
        Model2B.objects.create(field1="val1", field2="val2")

        # Scenario 1: .alias() only
        # Should not crash, and 'lower_field1' should NOT be an attribute
        qs = Model2A.objects.alias(lower_field1=functions.Lower("field1"))
        results = list(qs)
        self.assertEqual(len(results), 1)
        self.assertIsInstance(results[0], Model2B)
        self.assertFalse(hasattr(results[0], "lower_field1"))

        # Scenario 2: .annotate()
        # Should work, and 'upper_field1' SHOULD be an attribute
        qs = Model2A.objects.annotate(upper_field1=functions.Upper("field1"))
        results = list(qs)
        self.assertEqual(len(results), 1)
        self.assertTrue(hasattr(results[0], "upper_field1"))
        self.assertEqual(results[0].upper_field1, "VAL1")

        # Scenario 3: Mixed alias() and annotate()
        qs = Model2A.objects.alias(alias_val=functions.Lower("field1")).annotate(
            anno_val=functions.Upper("field1")
        )
        results = list(qs)
        self.assertEqual(len(results), 1)
        self.assertFalse(hasattr(results[0], "alias_val"))
        self.assertTrue(hasattr(results[0], "anno_val"))
        self.assertEqual(results[0].anno_val, "VAL1")

    def test_alias_advanced(self):
        """
        Test .alias() interactions with filter, order_by, only, and defer.
        """
        obj1 = Model2B.objects.create(field1="Alpha", field2="One")
        obj2 = Model2B.objects.create(field1="Beta", field2="Two")
        obj3 = Model2B.objects.create(field1="Gamma", field2="Three")

        # 1. Filter by alias
        qs = Model2A.objects.alias(lower_f1=functions.Lower("field1")).filter(lower_f1="beta")
        self.assertEqual(qs.count(), 1)
        self.assertEqual(qs[0], obj2)
        self.assertFalse(hasattr(qs[0], "lower_f1"))

        # 2. Order by alias
        qs = Model2A.objects.alias(len_f2=functions.Length("model2b__field2")).order_by("len_f2")
        # Lengths: One=3, Two=3, Three=5. (Ordering of equal values is DB dep, but logic holds)
        results = list(qs)
        self.assertEqual(len(results), 3)
        self.assertFalse(hasattr(results[0], "len_f2"))

        # 3. Alias + Only
        qs = Model2A.objects.alias(lower_f1=functions.Lower("field1")).only("field1")
        # Should not crash
        results = list(qs)
        self.assertEqual(len(results), 3)
        # Verify deferral logic didn't break
        # accessing field1 should not trigger refresh (hard to test without internals, but basic access works)
        self.assertEqual(results[0].field1, "Alpha")

        # 4. Alias + Defer
        qs = Model2A.objects.alias(lower_f1=functions.Lower("field1")).defer("field1")
        results = list(qs)
        self.assertEqual(len(results), 3)
        # accessing field1 should trigger refresh
        self.assertEqual(results[0].field1, "Alpha")

    def test_upcasting_to_sibling_class(self):
        """
        Test that querying a model that has been upcasted to a sibling
        polymorphic class does not raise a TypeError.
        Reproduces issue #280.
        """
        # Create a Model2A instance with a specific pk
        Model2A.objects.create(pk=1, field1="original")

        # "Upcast" it to a Model2B by creating an object with the same pk.
        # The polymorphic_ctype will now point to Model2B.
        Model2B.objects.create(pk=1, field1="updated", field2="new")

        # The original bug raised TypeError. We expect that accessing the
        # queryset should not raise TypeError. It should either be empty,
        # or raise a clean PolymorphicTypeInvalid error.
        try:
            list(Model2A.objects.all())
        except PolymorphicTypeInvalid:
            pass  # This is an acceptable outcome.
        except TypeError as e:
            self.fail(f"Querying for upcasted sibling raised TypeError: {e}")

    def test_mixed_inheritance_save_issue_495(self):
        """
        Test that saving models with mixed polymorphic and non-polymorphic
        inheritance works correctly. This addresses issue #495.
        """
        from polymorphic.tests.models import NormalExtension, PolyExtension, PolyExtChild

        # Create and save NormalExtension
        normal_ext = NormalExtension.objects.create(nb_field=1, ne_field="normal")
        normal_ext.add_to_ne(" extended")
        normal_ext.refresh_from_db()
        self.assertEqual(normal_ext.ne_field, "normal extended")
        normal_ext.add_to_nb(5)
        normal_ext.refresh_from_db()
        self.assertEqual(normal_ext.nb_field, 6)

        # Create and save PolyExtension
        poly_ext = PolyExtension.objects.create(nb_field=1, ne_field="normal", poly_ext_field=10)
        poly_ext.add_to_ne(" extended")
        poly_ext.refresh_from_db()
        self.assertEqual(poly_ext.ne_field, "normal extended")
        poly_ext.add_to_ext(5)
        poly_ext.refresh_from_db()
        self.assertEqual(poly_ext.poly_ext_field, 15)
        poly_ext.add_to_nb(5)
        poly_ext.refresh_from_db()
        self.assertEqual(poly_ext.nb_field, 6)

        # Create and save PolyExtChild
        poly_child = PolyExtChild.objects.create(
            nb_field=1, ne_field="normal", poly_ext_field=20, poly_child_field="child"
        )
        poly_child.add_to_ne(" extended")
        poly_child.add_to_nb(5)
        poly_child.add_to_ext(10)
        poly_child.add_to_child(" added")
        poly_child.refresh_from_db()
        self.assertEqual(poly_child.nb_field, 6)
        self.assertEqual(poly_child.ne_field, "normal extended")
        self.assertEqual(poly_child.poly_ext_field, 30)
        self.assertEqual(poly_child.poly_child_field, "child added")

        poly_child.override_add_to_ne(" overridden")
        poly_child.override_add_to_ext(5)
        poly_child.refresh_from_db()
        self.assertEqual(poly_child.ne_field, "normal extended OVERRIDDEN")
        self.assertEqual(poly_child.poly_ext_field, 40)

    def test_create_or_update(self):
        """
        https://github.com/jazzband/django-polymorphic/issues/494
        """
        from polymorphic.tests.models import Model2B, Model2C

        obj, created = Model2B.objects.update_or_create(
            field1="value1", defaults={"field2": "value2"}
        )
        self.assertTrue(created)
        self.assertEqual(obj.field1, "value1")
        self.assertEqual(obj.field2, "value2")

        obj2, created = Model2B.objects.update_or_create(
            field1="value1", defaults={"field2": "new_value2"}
        )
        self.assertFalse(created)
        self.assertEqual(obj2.pk, obj.pk)
        self.assertEqual(obj2.field1, "value1")
        self.assertEqual(obj2.field2, "new_value2")

        self.assertEqual(Model2B.objects.count(), 1)

        obj3, created = Model2C.objects.update_or_create(
            field1="value1", defaults={"field2": "new_value3", "field3": "value3"}
        )

        self.assertTrue(created)
        self.assertEqual(Model2B.objects.count(), 2)
        self.assertEqual(Model2C.objects.count(), 1)
        self.assertEqual(obj3, Model2B.objects.order_by("pk").last())

    def test_double_underscore_in_related_name(self):
        """
        Test filtering on a related field when the relation name itself contains '__'.
        This reproduces the issue in #295, where 'my__relation___real_field' was
        being incorrectly parsed as a polymorphic lookup.
        """

        related = Regression295Related.objects.create(_real_field="test_value")
        Regression295Parent.objects.create(related_object=related)

        # The following filter would be translated to 'related_object___real_field'
        # by Django's query machinery.
        qs = Regression295Parent.objects.filter(related_object___real_field="test_value")
        self.assertEqual(qs.count(), 1)

    def test_issue_252_abstract_base_class(self):
        """
        Test that polymorphic models inheriting from both an abstract model
        and PolymorphicModel can be created and saved without IndexError.

        This reproduces issue #252:
        https://github.com/jazzband/django-polymorphic/issues/252

        The issue reported an IndexError when trying to create an Event object
        that inherited from both an abstract model (AbstractDateInfo) and
        PolymorphicModel. The error occurred in Django's query_utils.py when
        accessing polymorphic_ctype_id.

        We use RelationBase which inherits from RelationAbstractModel (abstract)
        and PolymorphicModel, demonstrating the same pattern.
        """
        # Test creating base polymorphic model with abstract parent
        # RelationBase inherits from RelationAbstractModel (abstract) and PolymorphicModel
        base = RelationBase(field_base="test_base")
        # This should not raise IndexError
        base.save()

        # Verify the object was saved correctly
        self.assertIsNotNone(base.pk)
        self.assertEqual(base.field_base, "test_base")
        self.assertIsNotNone(base.polymorphic_ctype_id)

        # Test creating child models
        relation_a = RelationA.objects.create(field_base="base_a", field_a="field_a_value")
        self.assertIsNotNone(relation_a.pk)
        self.assertEqual(relation_a.field_a, "field_a_value")

        relation_b = RelationB.objects.create(field_base="base_b", field_b="field_b_value")
        self.assertIsNotNone(relation_b.pk)
        self.assertEqual(relation_b.field_b, "field_b_value")

        # Test querying polymorphic objects
        relations = RelationBase.objects.all().order_by("pk")
        self.assertEqual(relations.count(), 3)

        # Verify polymorphic behavior - objects should be returned as their actual types
        self.assertIsInstance(relations[0], RelationBase)
        self.assertIsInstance(relations[1], RelationA)
        self.assertIsInstance(relations[2], RelationB)


class SpecialBookForm(forms.ModelForm):
    class Meta:
        model = SpecialBook
        exclude = ("author",)


class TestFormsetExclude(TestCase):
    def test_formset_child_respects_exclude(self):
        SpecialBookFormSet = polymorphic_modelformset_factory(
            Book,
            fields=[],
            formset_children=(PolymorphicFormSetChild(SpecialBook, form=SpecialBookForm),),
        )
        formset = SpecialBookFormSet(queryset=SpecialBook.objects.none())
        self.assertNotIn("author", formset.forms[0].fields)

    def test_formset_initial_with_contenttype_instance(self):
        """Test that polymorphic_ctype can be set as ContentType instance in initial data (issue #549)"""
        from django.contrib.contenttypes.models import ContentType

        ct = ContentType.objects.get_for_model(SpecialBook, for_concrete_model=False)

        SpecialBookFormSet = polymorphic_modelformset_factory(
            Book,
            fields="__all__",
            formset_children=(PolymorphicFormSetChild(SpecialBook, form=SpecialBookForm),),
        )

        # Set initial data with ContentType instance (as users do in issue #549)
        formset = SpecialBookFormSet(
            queryset=SpecialBook.objects.none(),
            initial=[{"polymorphic_ctype": ct}],
        )

        # Should not raise an error when creating the formset
        form = formset.forms[0]

        # Verify the polymorphic_ctype field is properly set up with the ID
        self.assertIn("polymorphic_ctype", form.fields)

        # The critical assertion: the field's initial value should be the ID (int),
        # not the ContentType instance. This proves the normalization worked.
        self.assertEqual(form.fields["polymorphic_ctype"].initial, ct.pk)
        self.assertIsInstance(form.fields["polymorphic_ctype"].initial, int)

    def test_formset_with_none_instance(self):
        """Test that formset handles None instance without AttributeError (issue #363).

        This occurs when a bound formset has a pk that doesn't exist in the queryset,
        causing Django's _existing_object to return None. The polymorphic formset
        must handle this gracefully instead of calling get_real_instance_class() on None.
        """
        from django.contrib.contenttypes.models import ContentType

        ct = ContentType.objects.get_for_model(SpecialBook, for_concrete_model=False)

        SpecialBookFormSet = polymorphic_modelformset_factory(
            Book,
            fields="__all__",
            formset_children=(PolymorphicFormSetChild(SpecialBook, form=SpecialBookForm),),
        )

        # Simulate the scenario where _existing_object returns None:
        # - Bound formset with data
        # - Claims to have an initial form (INITIAL_FORMS > 0)
        # - But the pk doesn't exist in queryset, so _existing_object returns None
        data = {
            "form-TOTAL_FORMS": "1",
            "form-INITIAL_FORMS": "1",
            "form-MIN_NUM_FORMS": "0",
            "form-MAX_NUM_FORMS": "1000",
            "form-0-id": "99999",  # Non-existent pk - _existing_object will return None
            "form-0-polymorphic_ctype": str(ct.pk),
        }

        formset = SpecialBookFormSet(data=data, queryset=SpecialBook.objects.none())

        # This should not raise AttributeError when instance is None
        forms = formset.forms
        self.assertEqual(len(forms), 1)
        self.assertIn("polymorphic_ctype", forms[0].fields)

    def test_combined_formset_behaviors(self):
        # 1. __init__ exclude handling
        child_none = PolymorphicFormSetChild(Book, form=SpecialBookForm, exclude=None)
        self.assertEqual(child_none.exclude, ())

        child_list = PolymorphicFormSetChild(Book, form=SpecialBookForm, exclude=["author"])
        self.assertIn("author", child_list.exclude)

        # 2. get_form exclude merging
        form = child_list.get_form(extra_exclude=["field1"])
        self.assertIn("author", form._meta.exclude)
        self.assertIn("field1", form._meta.exclude)

        form_meta_default = child_none.get_form()
        self.assertIn("author", form_meta_default._meta.exclude)

        # 3. polymorphic_ctype normalization
        ct = ContentType.objects.get_for_model(SpecialBook, for_concrete_model=False)
        SpecialBookFormSet = polymorphic_modelformset_factory(
            Book,
            fields="__all__",
            formset_children=(PolymorphicFormSetChild(SpecialBook, form=SpecialBookForm),),
        )
        formset = SpecialBookFormSet(
            queryset=SpecialBook.objects.none(),
            initial=[{"polymorphic_ctype": ct}],
        )
        # The formset should normalize the ContentType instance to its ID
        form_ct = formset.forms[0]
        self.assertIsInstance(form_ct.initial["polymorphic_ctype"], int)
        self.assertEqual(form_ct.initial["polymorphic_ctype"], ct.pk)
