"""
Tests specifically targeting uncovered code paths to achieve 100% coverage.
"""

import warnings

import pytest
from django.contrib.contenttypes.models import ContentType
from django.db.models.query import ModelIterable
from django.test import TestCase, TransactionTestCase

from polymorphic.formsets.models import (
    BasePolymorphicModelFormSet,
    PolymorphicFormSetChild,
    polymorphic_modelformset_factory,
)
from polymorphic.models import PolymorphicModel
from polymorphic.query import PolymorphicModelIterable, PolymorphicQuerySet
from polymorphic.tests.models import (
    Model2A,
    Model2B,
    Model2C,
    Model2D,
    ModelShow1,
    ModelShow2,
    ModelShow3,
    RelationBase,
    SubclassSelectorProxyBaseModel,
    SubclassSelectorProxyModel,
)
from polymorphic.utils import reset_polymorphic_ctype


# ===========================================================================
# base.py - deprecated base_objects property (lines 157-163)
# ===========================================================================


class TestBaseObjectsDeprecated(TestCase):
    """Test the deprecated base_objects property on PolymorphicModelBase metaclass."""

    def test_base_objects_deprecated_warning(self):
        """Accessing Model.base_objects raises DeprecationWarning."""
        with warnings.catch_warnings(record=True) as caught:
            warnings.simplefilter("always")
            mgr = Model2A.base_objects
            assert len(caught) == 1
            w = caught[0]
            assert issubclass(w.category, DeprecationWarning)
            assert "base_objects" in str(w.message)
            assert "non_polymorphic()" in str(w.message)
            # The manager returned should be a django Manager
            from django.db import models as djmodels

            assert isinstance(mgr, djmodels.Manager)

    def test_base_objects_returns_manager(self):
        """base_objects manager can filter/query normally."""
        Model2A.objects.create(field1="test_deprecated")
        with warnings.catch_warnings(record=True):
            warnings.simplefilter("always")
            mgr = Model2A.base_objects
            # should return base (non-polymorphic) instances
            obj = mgr.filter(field1="test_deprecated").first()
            assert obj is not None


# ===========================================================================
# managers.py - PolymorphicManager.__str__ (line 106)
# ===========================================================================


class TestPolymorphicManagerStr(TestCase):
    """Test PolymorphicManager.__str__ method."""

    def test_manager_str(self):
        """PolymorphicManager.__str__ includes class name and queryset class name."""
        # Use the actual manager from the model rather than constructing one manually
        result = str(Model2A.objects)
        assert "PolymorphicManager" in result
        assert "PolymorphicQuerySet" in result

    def test_custom_manager_str(self):
        """Custom PolymorphicManager subclass has correct __str__."""
        from polymorphic.tests.models import ModelWithMyManager

        # Use the actual manager from the model (ModelWithMyManager uses MyManager)
        result = str(ModelWithMyManager.objects)
        assert "MyManager" in result
        assert "MyManagerQuerySet" in result


# ===========================================================================
# deletion.py - PolymorphicGuard.__eq__ with tuple (lines 97-103)
# ===========================================================================


class TestPolymorphicGuardEqWithTuple(TestCase):
    """Test PolymorphicGuard.__eq__ when compared with a 3-tuple (migration autodetector case)."""

    def test_guard_eq_tuple_with_deconstructible_action(self):
        """When compared to a 3-tuple and action has deconstruct(), compare via deconstruct."""
        from django.db import models as dj_models

        from polymorphic.deletion import PolymorphicGuard

        # models.SET(value) returns an object with a deconstruct() method
        action = dj_models.SET(42)
        guard = PolymorphicGuard(action)

        # deconstruct() returns ('django.db.models.SET', (42,), {})
        path, args, kwargs = action.deconstruct()

        # Compare guard to the equivalent 3-tuple
        assert guard == (path, args, kwargs)

    def test_guard_eq_tuple_with_list_args(self):
        """When compared to a 3-tuple with list args instead of tuple, still works."""
        from django.db import models as dj_models

        from polymorphic.deletion import PolymorphicGuard

        action = dj_models.SET(99)
        guard = PolymorphicGuard(action)
        path, args, kwargs = action.deconstruct()

        # Use list instead of tuple for args element (list [99] instead of tuple (99,))
        assert guard == (path, list(args), kwargs)

    def test_guard_eq_tuple_wrong_length(self):
        """Tuple with wrong length is not compared via deconstruct."""
        from django.db import models as dj_models

        from polymorphic.deletion import PolymorphicGuard

        action = dj_models.SET_NULL
        guard = PolymorphicGuard(action)

        # 2-tuple does NOT trigger the deconstruct path
        result = guard.__eq__(("some_path", []))
        # falls through to migration_key comparison - should return False
        assert result is False

    def test_guard_eq_tuple_action_without_deconstruct(self):
        """If action lacks deconstruct(), tuple comparison falls through."""
        from polymorphic.deletion import PolymorphicGuard

        # A plain callable without deconstruct()
        def my_handler(collector, field, sub_objs, using):
            pass

        guard = PolymorphicGuard(my_handler)
        # 3-tuple, but my_handler has no deconstruct() -> condition False -> migration_key path
        result = guard.__eq__(("some.path", (), {}))
        # my_handler != SET_NULL fingerprint, so False
        assert result is False


# ===========================================================================
# utils.py - reset_polymorphic_ctype with ignore_existing + filters (lines 41, 50, 52)
# utils.py - line 70 (unreachable - pragma: no cover is appropriate)
# ===========================================================================


class TestResetPolymorphicCtypeExtended(TransactionTestCase):
    """Extended tests for reset_polymorphic_ctype covering ignore_existing and filters."""

    def test_reset_with_ignore_existing(self):
        """
        reset_polymorphic_ctype with ignore_existing=True reverses the order (line 41)
        and filters for objects with null ctype (line 50).
        """
        # Create objects and null out their ctypes
        obj_a = Model2A.objects.create(field1="A_ignore")
        obj_b = Model2B.objects.create(field1="B_ignore", field2="B2")

        # Null out ctypes
        Model2A.objects.all().update(polymorphic_ctype_id=None)

        # Call with ignore_existing=True
        reset_polymorphic_ctype(Model2A, Model2B, ignore_existing=True)

        # Ctypes should now be set
        obj_a.refresh_from_db()
        obj_b.refresh_from_db()
        ct_a = ContentType.objects.get_for_model(Model2A, for_concrete_model=False)
        ct_b = ContentType.objects.get_for_model(Model2B, for_concrete_model=False)
        assert obj_a.polymorphic_ctype_id == ct_a.pk
        assert obj_b.polymorphic_ctype_id == ct_b.pk

    def test_reset_with_ignore_existing_skips_already_set(self):
        """
        With ignore_existing=True, already-set ctypes are NOT overwritten (line 50).
        """
        # Create objects - ctypes are set by default
        obj_a = Model2A.objects.create(field1="A_skip")
        original_ct_id = obj_a.polymorphic_ctype_id

        # Do NOT null out ctype - it should be skipped
        reset_polymorphic_ctype(Model2A, ignore_existing=True)

        obj_a.refresh_from_db()
        # Should still be the same ctype (not changed)
        assert obj_a.polymorphic_ctype_id == original_ct_id

    def test_reset_with_extra_filters(self):
        """
        reset_polymorphic_ctype with extra **filters applies them (line 52).
        """
        # Create objects
        Model2A.objects.create(field1="filter_me")
        Model2A.objects.create(field1="not_me")

        # Null out all ctypes
        Model2A.objects.all().update(polymorphic_ctype_id=None)

        # Reset only for objects matching the filter
        reset_polymorphic_ctype(Model2A, field1="filter_me")

        ct_a = ContentType.objects.get_for_model(Model2A, for_concrete_model=False)
        # filter_me should be set
        assert Model2A.objects.filter(field1="filter_me", polymorphic_ctype_id=ct_a.pk).exists()
        # not_me should still be null
        assert Model2A.objects.filter(field1="not_me", polymorphic_ctype_id__isnull=True).exists()

    def test_reset_with_ignore_existing_and_filters(self):
        """
        Combined test: ignore_existing=True AND extra filters (lines 41, 50, 52 together).
        """
        # Create objects with null ctypes
        obj1 = Model2A.objects.create(field1="combo_reset")
        obj2 = Model2A.objects.create(field1="combo_skip")

        Model2A.objects.all().update(polymorphic_ctype_id=None)

        # Reset only field1="combo_reset" with ignore_existing=True
        reset_polymorphic_ctype(Model2A, ignore_existing=True, field1="combo_reset")

        ct_a = ContentType.objects.get_for_model(Model2A, for_concrete_model=False)
        # Use non_polymorphic() to bypass polymorphic ctype checks when ct is still null
        obj1_data = (
            Model2A.objects.non_polymorphic()
            .filter(pk=obj1.pk)
            .values("polymorphic_ctype_id")
            .first()
        )
        obj2_data = (
            Model2A.objects.non_polymorphic()
            .filter(pk=obj2.pk)
            .values("polymorphic_ctype_id")
            .first()
        )
        assert obj1_data["polymorphic_ctype_id"] == ct_a.pk
        assert obj2_data["polymorphic_ctype_id"] is None


# ===========================================================================
# query.py - PolymorphicModelIterable.__iter__ when polymorphic_disabled (line 78)
# ===========================================================================


class TestPolymorphicModelIterableDisabled(TransactionTestCase):
    """Test PolymorphicModelIterable when polymorphic_disabled=True (line 78)."""

    def test_polymorphic_iterable_returns_base_iter_when_disabled(self):
        """
        When qs.polymorphic_disabled=True and _iterable_class is still
        PolymorphicModelIterable, __iter__ returns base_iter directly.
        """
        Model2A.objects.create(field1="disabled_test")

        qs = Model2A.objects.all()
        # The _iterable_class is PolymorphicModelIterable by default
        assert issubclass(qs._iterable_class, PolymorphicModelIterable)

        # Set polymorphic_disabled=True without calling non_polymorphic()
        # (which would also replace _iterable_class with ModelIterable)
        qs2 = qs._clone()
        qs2.polymorphic_disabled = True
        # _iterable_class is still PolymorphicModelIterable

        results = list(qs2)
        # Should return base class (Model2A) objects
        assert len(results) >= 1
        for obj in results:
            assert type(obj) is Model2A


# ===========================================================================
# query.py - max_chunk=0 (falsy) branch (lines 97->104)
# ===========================================================================


class TestPolymorphicIteratorMaxChunk(TransactionTestCase):
    """Test _polymorphic_iterator when max_chunk is 0/falsy."""

    def test_iterator_with_zero_max_query_params(self):
        """
        Lines 97->104: When max_query_params is 0 (falsy), the if max_chunk branch
        is skipped and sql_chunk falls back to Polymorphic_QuerySet_objects_per_request.
        """
        from unittest.mock import MagicMock, patch

        Model2A.objects.create(field1="chunk_test")
        Model2B.objects.create(field1="chunk_b", field2="B2")

        qs = Model2A.objects.all()

        class FakeFeatures:
            max_query_params = 0

        class FakeConnection:
            features = FakeFeatures()

        # Patch connections to return 0 for max_query_params
        # Then actually iterate the queryset to exercise _polymorphic_iterator
        with patch("polymorphic.query.connections") as mock_connections:
            mock_connections.__getitem__ = MagicMock(return_value=FakeConnection())
            # Iterate the queryset - this calls _polymorphic_iterator which checks max_chunk
            # With max_chunk=0, the if branch is skipped (97->104)
            results = list(qs)
            assert len(results) >= 2


# ===========================================================================
# query.py - non_polymorphic() False branch of issubclass check (lines 216->218)
# ===========================================================================


class TestNonPolymorphicIterable(TestCase):
    """Test non_polymorphic() when _iterable_class is NOT PolymorphicModelIterable."""

    def test_non_polymorphic_with_non_polymorphic_iterable(self):
        """
        non_polymorphic() when _iterable_class is already ModelIterable (not a subclass
        of PolymorphicModelIterable) - the False branch of the issubclass check (line 216->218).

        Calling non_polymorphic() twice hits the False branch naturally: the first call
        switches _iterable_class to ModelIterable; the second call finds it is already
        ModelIterable (not a subclass of PolymorphicModelIterable) and skips the swap.
        """
        qs2 = Model2A.objects.non_polymorphic().non_polymorphic()
        assert qs2.polymorphic_disabled is True
        # _iterable_class should be ModelIterable after the second call
        assert qs2._iterable_class is ModelIterable


# ===========================================================================
# query.py - defer(None) case (line 295)
# ===========================================================================


class TestDeferNone(TransactionTestCase):
    """Test the defer(None) case in PolymorphicQuerySet."""

    def test_defer_none(self):
        """defer(None) should reset deferred loading."""
        Model2B.objects.create(field1="defer_none_b", field2="B2")

        # First defer a field
        qs = Model2A.objects.defer("field1")
        # Then call defer(None) to reset
        qs2 = qs.defer(None)
        # Should work without error - returns all fields
        results = list(qs2)
        assert len(results) >= 1


# ===========================================================================
# query.py - _polymorphic_add_deferred_loading False branch (line 323)
# ===========================================================================


class TestPolymorphicDeferredLoading(TransactionTestCase):
    """Test the _polymorphic_add_deferred_loading branches."""

    def test_add_deferred_loading_remove_from_immediate(self):
        """
        Test line 323: when defer=False, removing field names from immediate load set.
        Verify the code path is traversed by ensuring the query works without error.
        """
        Model2B.objects.create(field1="defer_load_b", field2="B2")

        # First use only() to set up 'immediate load' mode (defer=False)
        # Then call defer() on this qs - this triggers the False branch
        # (removes from immediate load set)
        qs2 = Model2A.objects.only("field1").defer("field1")
        # Verify the query executes without error (code path covered by execution)
        results = list(qs2)
        assert len(results) >= 1

    def test_add_immediate_loading_when_defer_is_false(self):
        """
        Test line 342: when defer=False (already in only() mode), replaces immediate fields.
        Verify the code path is traversed by ensuring the query works without error.
        """
        Model2B.objects.create(field1="only_load_b", field2="B2")
        # Call only() then only() again - this triggers line 342 (defer=False → replace fields)
        qs2 = Model2A.objects.only("field1").only("polymorphic_ctype_id")
        # Verify the query executes without error (code path covered by execution)
        results = list(qs2)
        assert len(results) >= 1


# ===========================================================================
# query.py - patch_lookup else branch (line 366)
# ===========================================================================


class TestAnnotateAggregatePatchLookup(TransactionTestCase):
    """Test the else branch in patch_lookup (line 366) - direct .name attribute."""

    def test_annotate_with_direct_name_aggregate(self):
        """
        Test annotate with an aggregate that has a .name attribute directly
        (not get_source_expressions, not Q, not FilteredRelation, not F).
        This tests the else branch in patch_lookup.
        """
        from django.db.models import Count

        Model2A.objects.create(field1="patch_lookup_test")

        # Test that annotate works normally - exercises patch_lookup via kwargs
        qs = Model2A.objects.annotate(count=Count("id"))
        results = list(qs)
        assert len(results) >= 1

    def test_patch_lookup_else_branch_direct(self):
        """
        Line 366: The else branch in patch_lookup for objects with .name but
        no get_source_expressions (not Q, not FilteredRelation, not F).

        NOTE: There is no clean public API way to reach this branch in modern Django,
        because all standard Django aggregate/expression classes have get_source_expressions().
        The else branch is only reachable with a custom expression that has .name but
        lacks get_source_expressions. We call _process_aggregate_args directly here
        because this branch cannot be triggered through qs.annotate() or qs.aggregate()
        in practice with any built-in Django expression type.
        """
        Model2A.objects.create(field1="patch_lookup_else")

        # Create a fake expression with .name but no get_source_expressions
        class FakeExpr:
            def __init__(self, name):
                self.name = name

        fake = FakeExpr("field1")
        qs = Model2A.objects.all()

        # Call _process_aggregate_args with fake as a kwarg value
        # This triggers patch_lookup(fake) → not Q, not FilteredRelation,
        # not F, no get_source_expressions → else: a.name = translate(...)
        kwargs = {"test_fake": fake}
        qs._process_aggregate_args([], kwargs)
        # field1 has no ___  so translate returns it unchanged
        assert fake.name == "field1"


class TestAnnotateWithQObject(TransactionTestCase):
    """Test tree_node_test___lookup when 'a' is a Q object (lines 373-385)."""

    def test_aggregate_with_q_positional_args(self):
        """
        Lines 373-385: aggregate() with Q as positional arg triggers tree_node_test___lookup.
        When a Q object is passed as a positional arg to annotate/aggregate,
        test___lookup is called with it, which calls tree_node_test___lookup.

        NOTE: Passing Q as a positional argument to qs.aggregate() is not valid in the
        public Django API (aggregate() requires keyword arguments for named results).
        This code path in _process_aggregate_args handles Q positional args that come
        from internal callers. We call _process_aggregate_args directly because there
        is no public API way to pass a Q as a positional arg to aggregate/annotate.
        """
        from django.db.models import Count, Q

        Model2A.objects.create(field1="q_agg_test")
        Model2B.objects.create(field1="q_agg_b", field2="B2")

        # _process_aggregate_args processes positional args with test___lookup
        q = Q(field1="q_agg_test")
        args = [q]
        kwargs = {}
        # Directly call _process_aggregate_args to test the Q object path
        qs = Model2A.objects.all()
        qs._process_aggregate_args(args, kwargs)  # triggers tree_node_test___lookup

    def test_aggregate_with_nested_q_objects_positional(self):
        """
        Nested Q objects in positional args test the recursive tree_node_test___lookup.

        NOTE: Same rationale as test_aggregate_with_q_positional_args - no public API
        accepts Q as positional arg to aggregate/annotate.
        """
        from django.db.models import Q

        Model2A.objects.create(field1="nested_q")

        qs = Model2A.objects.all()
        # Nested Q objects - tests recursion in tree_node_test___lookup
        nested_q = Q(Q(field1="nested_q") | Q(field1="other"))
        qs._process_aggregate_args([nested_q], {})

    def test_aggregate_with_q_object_no_triple_underscore(self):
        """
        aggregate() with Q object in kwargs filter exercises patch_lookup with Q.
        """
        from django.db.models import Count, Q

        Model2A.objects.create(field1="q_agg_test2")

        qs = Model2A.objects.all()
        result = qs.aggregate(count=Count("id", filter=Q(field1="q_agg_test2")))
        assert "count" in result

    def test_test___lookup_with_expression_having_source_expressions(self):
        """
        Lines 386-389: test___lookup for expressions with get_source_expressions.
        """
        from django.db.models import Count, F

        qs = Model2A.objects.all()
        # F expression has no get_source_expressions but Count does
        # This tests the elif hasattr(a, 'get_source_expressions') branch in test___lookup
        count_expr = Count("id")
        qs._process_aggregate_args([count_expr], {})


# ===========================================================================
# query.py - stale content type (line 497) + real_concrete_class is None (lines 513->527)
# ===========================================================================


class TestGetRealInstancesEdgeCases(TransactionTestCase):
    """Test _get_real_instances with stale/missing content types."""

    def test_stale_content_type_skipped(self):
        """
        Line 497: When real_concrete_class_id is None (stale content type), skip the object.

        We iterate the full queryset using the public list(qs) API, patching
        get_real_concrete_instance_class_id at the class level so that all instances
        appear to have a stale (None) content type. The objects should be skipped.
        """
        from unittest.mock import patch

        Model2B.objects.create(field1="stale_ct", field2="B2")

        qs = Model2A.objects.all()

        # Patch at the class level so all base objects appear to have a stale CT
        with patch.object(
            Model2A,
            "get_real_concrete_instance_class_id",
            return_value=None,
        ):
            # list(qs) triggers _polymorphic_iterator → _get_real_instances
            # Each object's get_real_concrete_instance_class_id() returns None → skipped
            results = list(qs)
            assert len(results) == 0


# ===========================================================================
# query.py - AssertionError else branch (line 569)
# ===========================================================================


class TestDeferLoadingAssertionElseBranch(TransactionTestCase):
    """Test the else branch when AssertionError is raised without '___' in field (line 569)."""

    def test_field_without_triple_underscore_reraises(self):
        """
        When translating deferred fields in _get_real_instances, if AssertionError raised
        and '___' not in field name, it should re-raise. This tests line 569.

        We use the public qs.defer("field1") API to set up deferred loading, then iterate
        with list(qs) (also public) while patching translate_polymorphic_field_path to raise
        AssertionError for the plain field name (no '___'). The patch is needed because
        there is no standard way to trigger this specific code path through purely unpatched
        public API - the else-raise branch requires an AssertionError from the translator
        for a field name that contains no triple-underscore.
        """
        from unittest.mock import patch

        from polymorphic.query_translate import translate_polymorphic_field_path as orig_translate

        Model2B.objects.create(field1="assert_test", field2="B2")

        def patched_translate(model, field_path):
            if field_path == "field1":
                raise AssertionError("simulated assertion for plain field")
            return orig_translate(model, field_path)

        # Use public API: defer("field1") sets polymorphic_deferred_loading for "field1"
        qs = Model2A.objects.defer("field1")

        with patch(
            "polymorphic.query.translate_polymorphic_field_path", side_effect=patched_translate
        ):
            with pytest.raises(AssertionError, match="simulated assertion"):
                # list(qs) iterates via _polymorphic_iterator → _get_real_instances
                # which translates deferred fields → patched_translate raises for "field1"
                # "field1" has no "___" → else: raise
                list(qs)


# ===========================================================================
# query.py - annotation not on base object (lines 625->624)
# ===========================================================================


class TestAnnotationNotOnBaseObject(TransactionTestCase):
    """Test hasattr(base_object, anno_field_name) is False (line 625->624)."""

    def test_annotation_missing_from_base_object(self):
        """
        When a real object is fetched via _get_real_instances and an annotation
        is NOT on the base object (hasattr returns False), the annotation is skipped.
        """
        from django.db.models import Value

        Model2B.objects.create(field1="ann_missing", field2="B2")

        # Annotate the queryset
        qs = Model2A.objects.annotate(my_test_annotation=Value(42))
        results = list(qs)

        # At least the annotation is present on results
        for obj in results:
            if hasattr(obj, "my_test_annotation"):
                assert obj.my_test_annotation == 42


# ===========================================================================
# query.py - __repr__ for non-multiline queryset (line 660)
# ===========================================================================


class TestQuerySetRepr(TransactionTestCase):
    """Test PolymorphicQuerySet.__repr__ for non-multiline output."""

    def test_repr_multiline(self):
        """Models with polymorphic_query_multiline_output=True use multiline repr."""
        # Model2A uses ShowFieldType which sets polymorphic_query_multiline_output=True
        Model2A.objects.create(field1="repr_test")
        qs = Model2A.objects.all()
        assert qs.model.polymorphic_query_multiline_output is True
        r = repr(qs)
        assert "[" in r or "repr_test" in r

    def test_repr_non_multiline(self):
        """
        Line 660: When polymorphic_query_multiline_output=False, use super().__repr__().
        """
        from polymorphic.tests.models import Duck

        Duck.objects.create(name="repr_duck")
        qs = Duck.objects.all()
        # Duck uses PolymorphicModel without ShowField mixin
        assert qs.model.polymorphic_query_multiline_output is False
        r = repr(qs)
        # Django's default repr includes QuerySet
        assert "QuerySet" in r or "repr_duck" in r or "[" in r


# ===========================================================================
# query.py - _p_list_class.__repr__ (lines 664-665)
# ===========================================================================


class TestPListClassRepr(TransactionTestCase):
    """Test _p_list_class.__repr__ method."""

    def test_p_list_class_repr(self):
        """_p_list_class wraps items with multiline repr."""
        Model2A.objects.create(field1="plist_test")

        qs = Model2A.objects.all()
        # get_real_instances with multiline output returns _p_list_class
        result = qs.get_real_instances()
        # Since Model2A has ShowFieldType (multiline_output=True), result is _p_list_class
        assert isinstance(result, PolymorphicQuerySet._p_list_class)
        r = repr(result)
        assert "plist_test" in r or "[" in r


# ===========================================================================
# query.py - get_real_instances when polymorphic_query_multiline_output=False (line 690)
# ===========================================================================


class TestGetRealInstancesNonMultiline(TransactionTestCase):
    """Test get_real_instances returns plain list for non-multiline models."""

    def test_get_real_instances_no_multiline(self):
        """
        Line 690: When polymorphic_query_multiline_output=False,
        get_real_instances() returns plain olist.
        """
        from polymorphic.tests.models import Duck

        Duck.objects.create(name="duck_real")
        qs = Duck.objects.all()
        assert qs.model.polymorphic_query_multiline_output is False

        result = qs.get_real_instances()
        assert isinstance(result, list)
        # Should NOT be _p_list_class
        assert type(result) is not PolymorphicQuerySet._p_list_class


# ===========================================================================
# query_translate.py - RelatedField path return (line 181->188)
# ===========================================================================


class TestTranslatePolymorphicFieldPath(TestCase):
    """Test translate_polymorphic_field_path for RelatedField detection."""

    def test_related_field_path_passed_through_to_django(self):
        """
        The True branch at line 181 (isinstance is True → return field_path).
        When a path starts with a RelatedField name (FK/M2M), polymorphic returns it
        unchanged and passes it to Django. Django then handles it (possibly raising
        FieldError for unusual lookup syntax), confirming polymorphic did NOT try to
        interpret 'fk' as a model class name (which would raise AssertionError).

        RelationBase.fk is a ForeignKey to self (a RelatedField).
        """
        from django.core.exceptions import FieldError

        # fk is a ForeignKey on RelationBase → polymorphic returns "fk___field_base" unchanged
        # Django receives the path and raises FieldError (not AssertionError from polymorphic),
        # confirming that line 184 (return field_path) was hit.
        with pytest.raises(FieldError):
            RelationBase.objects.filter(fk___field_base="x").count()

    def test_m2m_field_path_passed_through_to_django(self):
        """M2M field name at the start of a path is returned unchanged by polymorphic."""
        from django.core.exceptions import FieldError

        # m2m is a ManyToManyField on RelationBase → passed through unchanged
        # Django raises FieldError (not AssertionError from polymorphic)
        with pytest.raises(FieldError):
            RelationBase.objects.filter(m2m___something="x").count()

    def test_plain_field_name_as_classname_falls_through(self):
        """
        Line 181->188: The False branch - field IS found in _meta but is NOT a RelatedField.
        E.g., field1 is a CharField. It's found by get_field('field1') but isinstance
        check fails, so falls through to _map_queryname_to_class.
        """
        # field1 is a CharField on Model2A - not a RelatedField
        # Falls through from 181 False branch to line 188 (_map_queryname_to_class)
        # which raises AssertionError since 'field1' is not a model class name
        with pytest.raises(AssertionError, match="is not a subclass"):
            Model2A.objects.filter(field1___something="x").count()

    def test_get_query_related_name_iterates_fields(self):
        """
        Line 223->222: The loop in _get_query_related_name iterates over local_fields.
        When a field is not a OneToOneField parent_link, the loop continues (223->222).

        Model2A.objects.filter(Model2B___field2="x") calls translate_polymorphic_field_path
        → _create_base_path → _get_query_related_name(Model2B), covering the True branch
        (parent_link O2O found, loop returns).

        For the False/continue branch (223->222): One2OneRelatingModel is a ROOT concrete
        polymorphic model (PolymorphicModel is abstract → no parent_ptr O2O). Its local_fields
        are [id, polymorphic_ctype, one2one, field1] — none are O2O parent_links — so the
        loop iterates all 4 fields hitting 223->222 each time, then falls through to line 228.
        There is no public API path that calls _get_query_related_name on a ROOT model
        (it's only called for child models during path building), so a direct call is needed.
        """
        from polymorphic.query_translate import _get_query_related_name
        from polymorphic.tests.models import One2OneRelatingModel

        # filter covers True branch of 223 (parent_link found, returns "model2b")
        count = Model2A.objects.filter(Model2B___field2="x").count()
        assert count == 0  # no matching objects, but translation succeeded

        # Direct call covers False branch of 223 (223->222, loop continues for non-parent-link fields)
        # and line 228 fallback (no parent_link found in any local field)
        result = _get_query_related_name(One2OneRelatingModel)
        assert result == "one2onerelatingmodel"

    def test_get_query_related_name_fallback_for_proxy_model(self):
        """
        Line 228: Fallback return (class name lower) when no OneToOneField parent_link
        is found in local_fields. This happens for proxy models, which have empty
        local_fields (no concrete fields added).

        SubclassSelectorProxyBaseModel.objects.filter(SubclassSelectorProxyModel___base_field=...)
        calls _get_query_related_name(SubclassSelectorProxyModel) during translation.
        SubclassSelectorProxyModel is a proxy with no local_fields, so the for loop
        doesn't execute → line 228 fallback is hit. The translation completes successfully,
        and then Django raises FieldError since proxy models share their parent's table
        (no join path exists). The FieldError from Django (not AssertionError from polymorphic)
        confirms the fallback path was traversed.
        """
        from django.core.exceptions import FieldError

        # The translate call hits line 228 (fallback), then Django raises FieldError
        # because proxy models share the base table (no separate DB join path).
        with pytest.raises(FieldError):
            SubclassSelectorProxyBaseModel.objects.filter(
                SubclassSelectorProxyModel___base_field="x"
            ).count()


# ===========================================================================
# query_translate.py - empty path from _create_base_path (line 223->222)
# ===========================================================================


class TestCreateBasePath(TestCase):
    """Test _create_base_path returns empty string for same base class."""

    def test_create_base_path_empty(self):
        """
        Line 198->201: When _create_base_path returns '' (basepath is falsy), newpath
        is just the pure_field_path without a '__' prefix.

        Model2A___field1 where Model2A IS the queryset model → _create_base_path returns ''
        → newpath = '' + 'field1' = 'field1' (the if basepath branch is skipped).
        Verified via queryset filter: Model2A___field1 translates to plain 'field1'.
        """
        # Model2A___field1 where Model2A IS the queryset model → basepath = '' → newpath = 'field1'
        # The filter works since 'field1' is a valid field on Model2A
        Model2A.objects.create(field1="base_path_test")
        count = Model2A.objects.filter(Model2A___field1="base_path_test").count()
        assert count == 1


# ===========================================================================
# query_translate.py - modellist is a single PolymorphicModel (line 248)
# ===========================================================================


class TestCreateInstanceofQ(TestCase):
    """Test create_instanceof_q with various inputs."""

    def test_empty_modellist_returns_none(self):
        """
        Line 248: When modellist is empty/falsy, return None.
        """
        from polymorphic.query_translate import create_instanceof_q

        result = create_instanceof_q([])
        assert result is None

    def test_single_polymorphic_model(self):
        """
        Lines 250-254: When modellist is a single PolymorphicModel (not list/tuple),
        it's converted to [modellist]. Triggered via filter(instance_of=Model2A) where
        the value is passed as a single class (not a tuple), going through
        _translate_polymorphic_filter_definition → create_instanceof_q(Model2A).
        """
        # filter(instance_of=Model2A) passes Model2A (not a tuple) to create_instanceof_q
        # → hits the "if not isinstance(modellist, (list, tuple))" branch (lines 250-254)
        count = Model2A.objects.filter(instance_of=Model2A).count()
        assert count >= 0  # filter created successfully, query executes without error

    def test_non_polymorphic_model_raises(self):
        """
        Lines 255-258: TypeError raised when single non-PolymorphicModel passed.
        """
        from polymorphic.query_translate import create_instanceof_q
        from polymorphic.tests.models import PlainA  # PlainA is a plain Django model

        with pytest.raises(TypeError, match="instance_of expects"):
            create_instanceof_q(PlainA)


# ===========================================================================
# showfields.py - polymorphic_showfield_old_format (line 38)
# ===========================================================================


class TestShowFieldsOldFormat(TransactionTestCase):
    """Test showfields with old format separator."""

    def test_old_format_colon_separator(self):
        """
        Line 38: polymorphic_showfield_old_format=True uses ': ' as separator.
        """
        from polymorphic.tests.models import ModelExtraA

        # ModelExtraA has ShowFieldTypeAndContent but no m2m, so str() is safe to call
        obj = ModelExtraA(field1="test_old_format")
        obj.__class__.polymorphic_showfield_old_format = True
        try:
            result = str(obj)
            assert ": " in result
        finally:
            obj.__class__.polymorphic_showfield_old_format = False


# ===========================================================================
# showfields.py - FK field content is None (line 51)
# ===========================================================================


class TestShowFieldsNoneContent(TransactionTestCase):
    """Test showfields when FK content is None."""

    def test_fk_field_none_content(self):
        """
        Line 42-43: FK is None shows 'None'.

        ModelExtraA uses ShowFieldTypeAndContent. An unsaved instance has no
        polymorphic_ctype set (it's a ForeignKey). str() calls _showfields_get_content
        internally, hitting the FK-is-None branch (lines 42-43).
        """
        from polymorphic.tests.models import ModelExtraA

        # Unsaved object: polymorphic_ctype is None (ForeignKey not yet set)
        obj = ModelExtraA(field1="fk_none_test")
        result = str(obj)
        assert "None" in result

    def test_none_content_for_regular_field(self):
        """
        Line 51: content is None for a regular (non-FK) field shows 'None'.

        ModelExtraA uses ShowFieldTypeAndContent. Setting field1=None and calling
        str() exercises _showfields_get_content for a CharField with None value,
        hitting the elif content is None branch (line 51).
        """
        from polymorphic.tests.models import ModelExtraA

        obj = ModelExtraA(field1=None)
        result = str(obj)
        assert "None" in result


# ===========================================================================
# showfields.py - text truncation (line 56)
# ===========================================================================


class TestShowFieldsTruncation(TransactionTestCase):
    """Test showfields truncation of long field values."""

    def test_long_field_truncated(self):
        """
        Line 56: When field content is longer than max_field_width, truncate it.
        """
        from polymorphic.tests.models import ModelExtraA

        # ModelExtraA has ShowFieldTypeAndContent, no m2m
        # Default max_field_width is 20
        obj = ModelExtraA(field1="x" * 30)  # 30 chars > 20 default max
        result = str(obj)
        # Should contain truncated version with ".."
        assert ".." in result


# ===========================================================================
# showfields.py - diamond inheritance deduplication (line 67)
# ===========================================================================


class TestShowFieldsDiamondInheritance(TransactionTestCase):
    """Test showfields deduplication in diamond inheritance (line 67)."""

    def test_diamond_inheritance_no_duplicate_fields(self):
        """
        Line 67: In diamond inheritance, same field name not shown twice.
        Enhance_Inherit inherits from both Enhance_Base and Enhance_Plain.
        """
        from polymorphic.tests.models import Enhance_Inherit

        # Enhance_Inherit has multiple inheritance
        # showfields should not duplicate fields
        obj = Enhance_Inherit(field_b="base", field_i="inherit", field_p="plain")
        result = str(obj)
        # field_p should appear at most once
        assert result.count("field_p") <= 1


# ===========================================================================
# showfields.py - deferred fields display (lines 127-128)
# ===========================================================================


class TestShowFieldsDeferred(TransactionTestCase):
    """Test showfields for deferred fields display."""

    def test_deferred_fields_shown(self):
        """
        Lines 127-128: When polymorphic_showfield_deferred=True and there are
        deferred fields, they are shown in the __str__ output.
        """
        # Model2A has polymorphic_showfield_deferred = True
        obj = Model2A.objects.create(field1="deferred_test")

        # Fetch with defer to actually have deferred fields
        obj_deferred = Model2A.objects.defer("field1").get(pk=obj.pk)
        result = str(obj_deferred)
        assert "deferred" in result or "field1" in result


# ===========================================================================
# showfields.py - line breaking (lines 149-152)
# ===========================================================================


class TestShowFieldsLineBreaking(TransactionTestCase):
    """Test showfields line breaking when max_line_width is set."""

    def test_line_breaking_with_max_line_width(self):
        """
        Lines 149-152: When polymorphic_showfield_max_line_width is set and
        content exceeds it, a newline+indent is inserted.
        """
        from polymorphic.showfields import ShowFieldTypeAndContent

        # Create object with many/wide fields to trigger line breaking
        obj = Model2C.objects.create(
            field1="short",
            field2="short2",
            field3="short3",
        )

        # Temporarily set max_line_width to trigger line breaking
        original = Model2C.polymorphic_showfield_max_line_width
        try:
            # Monkey-patch to set small max line width
            Model2C.polymorphic_showfield_max_line_width = 20
            result = str(obj)
            # Should contain newlines due to line breaking
            assert "\n" in result
        finally:
            Model2C.polymorphic_showfield_max_line_width = original


# ===========================================================================
# formsets/models.py - IndexError in initial_extra (lines 192-193)
# ===========================================================================


class TestFormsetInitialExtra(TestCase):
    """Test IndexError when initial_extra has fewer items than extra forms."""

    def test_initial_extra_index_error_handled(self):
        """
        Lines 192-193: IndexError is caught silently when initial_extra
        has fewer items than the extra form index.
        """
        FormSet = polymorphic_modelformset_factory(
            model=Model2A,
            fields="__all__",
            extra=3,
            formset_children=[PolymorphicFormSetChild(model=Model2A)],
        )

        # Create formset with initial_extra having only 1 item but extra=3
        formset = FormSet(queryset=Model2A.objects.none())
        # Set initial_extra with only 1 item for 3 extra forms
        formset.initial_extra = [{"field1": "initial_extra_0"}]

        # Accessing forms should not raise; forms 1 and 2 have no initial
        forms = formset.forms
        assert len(forms) == 3
        # First extra form has initial value set
        assert forms[0].initial.get("field1") == "initial_extra_0"
        # Second and third extra forms had IndexError → no initial
        assert "field1" not in forms[1].initial
        assert "field1" not in forms[2].initial


# ===========================================================================
# formsets/models.py - self.initial for regular forms (lines 205-208)
# ===========================================================================


class TestFormsetSelfInitial(TestCase):
    """Test self.initial for regular (non-extra) forms."""

    def test_formset_with_initial_data_for_extra_forms(self):
        """
        Lines 205-208: When self.initial is provided to the FormSet constructor,
        it can be used for extra forms (when initial_extra is not set).
        """
        FormSet = polymorphic_modelformset_factory(
            model=Model2A,
            fields="__all__",
            extra=2,
            formset_children=[PolymorphicFormSetChild(model=Model2A)],
        )

        initial = [{"field1": "initial_val_0"}, {"field1": "initial_val_1"}]
        formset = FormSet(queryset=Model2A.objects.none(), initial=initial)

        forms = formset.forms
        # Forms were created - verify they exist
        assert len(forms) == 2

    def test_formset_self_initial_for_existing_objects_with_extra_initial(self):
        """
        Lines 205-208: self.initial is used when i < initial_form_count()
        AND "initial" is not already in kwargs (e.g., not set by initial_extra).

        For an unbound formset with a queryset that has existing objects AND
        self.initial is provided, the self.initial[i] provides initial data
        for those existing-object forms.
        """
        obj_a = Model2A.objects.create(field1="existing_A")

        FormSet = polymorphic_modelformset_factory(
            model=Model2A,
            fields="__all__",
            extra=0,
            formset_children=[PolymorphicFormSetChild(model=Model2A)],
        )

        # Provide self.initial for the queryset forms
        # This is an unusual but valid usage: override initial data for bound-query forms
        initial = [{"field1": "override_initial"}]
        formset = FormSet(queryset=Model2A.objects.filter(pk=obj_a.pk), initial=initial)

        forms = formset.forms
        assert len(forms) >= 1
        # The instance is set from queryset, but self.initial may provide extra initial

    def test_formset_initial_index_error_handled(self):
        """
        Lines 205-208: IndexError when self.initial has fewer items than forms.
        In this case, formset.initial has items but some indices don't exist.
        """
        FormSet = polymorphic_modelformset_factory(
            model=Model2A,
            fields="__all__",
            extra=3,
            formset_children=[PolymorphicFormSetChild(model=Model2A)],
        )

        # Manually set self.initial after creation to test lines 205-208
        formset = FormSet(queryset=Model2A.objects.none())
        formset.initial = [{"field1": "only_one"}]
        # Reset initial_extra to None so the initial_extra branch is skipped
        formset.initial_extra = None

        forms = formset.forms
        # form[0] uses initial[0], forms[1] and [2] get IndexError → pass
        assert len(forms) == 3


# ===========================================================================
# formsets/models.py - ContentType ID (int) in initial (lines 253, 271->277)
# ===========================================================================


class TestFormsetCTypeAsInt(TestCase):
    """Test unbound formset with polymorphic_ctype as int (not ContentType instance)."""

    def test_unbound_formset_ct_as_int(self):
        """
        Line 253: ct_value is an int (not ContentType instance) in unbound formset.
        Line 271->277: The False branch - ct_value is NOT a ContentType instance.
        """
        FormSet = polymorphic_modelformset_factory(
            model=Model2A,
            fields="__all__",
            extra=1,
            formset_children=[
                PolymorphicFormSetChild(model=Model2A),
                PolymorphicFormSetChild(model=Model2B),
            ],
        )

        ct_b = ContentType.objects.get_for_model(Model2B, for_concrete_model=False)
        # Pass the CT as an integer ID (not a ContentType instance)
        initial = [{"polymorphic_ctype": ct_b.pk}]
        formset = FormSet(queryset=Model2A.objects.none(), initial=initial)

        # Form should be for Model2B (looked up via int ID)
        forms = formset.forms
        assert len(forms) == 1
        assert "field2" in forms[0].fields


# ===========================================================================
# formsets/models.py - per-row model selection for an unbound formset
# ===========================================================================


class TestFormsetModelFromQuerysetData(TestCase):
    """Test that an unbound formset built from a mixed-type queryset selects the
    correct child model for each row."""

    def test_model_from_queryset_data(self):
        """
        Each initial form is bound to its queryset object's real concrete class
        (the ``defaults["instance"]`` branch in ``_construct_form``), so a formset
        over a mixed Model2A/Model2B queryset produces one correctly-typed form
        per row.
        """
        obj_a = Model2A.objects.create(field1="qs_data_a")
        obj_b = Model2B.objects.create(field1="qs_data_b", field2="B2")

        FormSet = polymorphic_modelformset_factory(
            model=Model2A,
            fields="__all__",
            extra=0,
            formset_children=[
                PolymorphicFormSetChild(model=Model2A),
                PolymorphicFormSetChild(model=Model2B),
            ],
        )

        # Provide existing queryset with real objects - forms use queryset_data
        queryset = Model2A.objects.filter(pk__in=[obj_a.pk, obj_b.pk]).order_by("pk")
        formset = FormSet(queryset=queryset)

        forms = formset.forms
        # The form types come from the queryset_data items
        assert len(forms) == 2
        # queryset_data drives the model for each form: one Model2A and one Model2B
        form_models = {form.instance.__class__ for form in forms if form.instance.pk}
        assert form_models == {Model2A, Model2B}


# ===========================================================================
# DRF serializers (contrib/drf/serializers.py)
# ===========================================================================


class TestPolymorphicSerializerBase(TestCase):
    """Tests for PolymorphicSerializer."""

    pytestmark = pytest.mark.integration

    def setUp(self):
        from rest_framework import serializers

        from polymorphic.contrib.drf.serializers import PolymorphicSerializer

        class ModelASerializer(serializers.Serializer):
            field1 = serializers.CharField()

            def create(self, validated_data):
                return Model2A(**validated_data)

            def update(self, instance, validated_data):
                instance.field1 = validated_data.get("field1", instance.field1)
                return instance

        class ModelBSerializer(serializers.Serializer):
            field1 = serializers.CharField()
            field2 = serializers.CharField()

            def create(self, validated_data):
                return Model2B(**validated_data)

            def update(self, instance, validated_data):
                instance.field1 = validated_data.get("field1", instance.field1)
                instance.field2 = validated_data.get("field2", instance.field2)
                return instance

        class TestPolymorphicSerializer(PolymorphicSerializer):
            model_serializer_mapping = {
                Model2A: ModelASerializer,
                Model2B: ModelBSerializer,
            }

        self.PolymorphicSerializer = TestPolymorphicSerializer
        self.ModelASerializer = ModelASerializer
        self.ModelBSerializer = ModelBSerializer

    def test_new_requires_model_serializer_mapping(self):
        """__new__ raises ImproperlyConfigured when model_serializer_mapping is missing."""
        from django.core.exceptions import ImproperlyConfigured

        from polymorphic.contrib.drf.serializers import PolymorphicSerializer

        class MissingMapping(PolymorphicSerializer):
            pass

        with pytest.raises(ImproperlyConfigured, match="model_serializer_mapping"):
            MissingMapping()

    def test_new_requires_string_resource_type_field_name(self):
        """__new__ raises ImproperlyConfigured when resource_type_field_name is not a string."""
        from django.core.exceptions import ImproperlyConfigured

        from polymorphic.contrib.drf.serializers import PolymorphicSerializer

        class BadResourceType(PolymorphicSerializer):
            model_serializer_mapping = {Model2A: None}
            resource_type_field_name = 123  # not a string

        with pytest.raises(ImproperlyConfigured, match="resource_type_field_name"):
            BadResourceType()

    def test_init_sets_up_mappings(self):
        """__init__ sets up model_serializer_mapping and resource_type_model_mapping."""
        serializer = self.PolymorphicSerializer()
        assert Model2A in serializer.model_serializer_mapping
        assert Model2B in serializer.model_serializer_mapping
        assert "Model2A" in serializer.resource_type_model_mapping
        assert "Model2B" in serializer.resource_type_model_mapping

    def test_to_resource_type(self):
        """to_resource_type returns the model's object_name."""
        serializer = self.PolymorphicSerializer()
        assert serializer.to_resource_type(Model2A) == "Model2A"
        assert serializer.to_resource_type(Model2B) == "Model2B"

    def test_to_representation_with_instance(self):
        """to_representation with a model instance."""
        serializer = self.PolymorphicSerializer()
        obj = Model2A(field1="test_rep")
        result = serializer.to_representation(obj)
        assert result["field1"] == "test_rep"
        assert result["resourcetype"] == "Model2A"

    def test_to_representation_with_mapping(self):
        """to_representation with a mapping (dict)."""
        serializer = self.PolymorphicSerializer()
        data = {"resourcetype": "Model2A", "field1": "map_test"}
        result = serializer.to_representation(data)
        assert result["field1"] == "map_test"
        assert result["resourcetype"] == "Model2A"

    def test_to_internal_value_regular(self):
        """to_internal_value for regular (non-partial) case."""
        serializer = self.PolymorphicSerializer()
        data = {"resourcetype": "Model2A", "field1": "internal_val"}
        result = serializer.to_internal_value(data)
        assert result["field1"] == "internal_val"
        assert result["resourcetype"] == "Model2A"

    def test_to_internal_value_partial_with_instance(self):
        """to_internal_value for partial update uses instance's resource type."""
        obj = Model2A(field1="partial_obj")
        serializer = self.PolymorphicSerializer(instance=obj, partial=True)
        data = {"field1": "updated"}
        result = serializer.to_internal_value(data)
        assert result["field1"] == "updated"
        assert result["resourcetype"] == "Model2A"

    def test_create(self):
        """create() delegates to the child serializer."""
        serializer = self.PolymorphicSerializer()
        validated_data = {"resourcetype": "Model2A", "field1": "created"}
        result = serializer.create(validated_data)
        assert result.field1 == "created"

    def test_update(self):
        """update() delegates to the child serializer."""
        obj = Model2A(field1="original")
        serializer = self.PolymorphicSerializer()
        validated_data = {"resourcetype": "Model2A", "field1": "updated"}
        result = serializer.update(obj, validated_data)
        assert result.field1 == "updated"

    def test_is_valid_success(self):
        """is_valid() returns True for valid data."""
        data = {"resourcetype": "Model2A", "field1": "valid_data"}
        serializer = self.PolymorphicSerializer(data=data)
        assert serializer.is_valid()

    def test_is_valid_partial_with_instance(self):
        """is_valid() with partial=True and instance uses instance's type."""
        obj = Model2A(field1="existing")
        data = {"field1": "patched"}
        serializer = self.PolymorphicSerializer(instance=obj, data=data, partial=True)
        assert serializer.is_valid()

    def test_is_valid_missing_resource_type(self):
        """is_valid() returns False when resourcetype is missing."""
        data = {"field1": "no_type"}
        serializer = self.PolymorphicSerializer(data=data)
        result = serializer.is_valid()
        assert result is False
        assert "resourcetype" in serializer.errors

    def test_is_valid_updates_validated_data_from_child(self):
        """is_valid() updates validated_data from child serializer."""
        data = {"resourcetype": "Model2A", "field1": "child_valid"}
        serializer = self.PolymorphicSerializer(data=data)
        valid = serializer.is_valid()
        assert valid
        assert "field1" in serializer.validated_data

    def test_run_validation_regular(self):
        """run_validation for regular (non-partial) case."""
        data = {"resourcetype": "Model2A", "field1": "run_val"}
        serializer = self.PolymorphicSerializer()
        result = serializer.run_validation(data)
        assert result["field1"] == "run_val"
        assert result["resourcetype"] == "Model2A"

    def test_run_validation_partial_with_instance(self):
        """run_validation for partial update uses instance's resource type."""
        obj = Model2A(field1="existing_run")
        serializer = self.PolymorphicSerializer(instance=obj, partial=True)
        data = {"field1": "run_updated"}
        result = serializer.run_validation(data)
        assert result["field1"] == "run_updated"

    def test_missing_resource_type_raises_validation_error(self):
        """
        _get_resource_type_from_mapping raises ValidationError when resourcetype is absent.
        Tested via the public run_validation() API with an empty data dict, which
        internally calls _get_resource_type_from_mapping({}).
        """
        from rest_framework import serializers

        s = self.PolymorphicSerializer()
        with pytest.raises(serializers.ValidationError, match="required"):
            s.run_validation({})

    def test_unknown_resource_type_raises_validation_error(self):
        """
        _get_serializer_from_resource_type raises ValidationError for an unknown type.
        Tested via the public run_validation() API with an unrecognized resourcetype,
        which internally calls _get_serializer_from_resource_type("UnknownModel").
        """
        from rest_framework import serializers

        s = self.PolymorphicSerializer()
        with pytest.raises(serializers.ValidationError, match="Invalid"):
            s.run_validation({"resourcetype": "UnknownModel", "field1": "x"})

    def test_unregistered_model_raises_key_error_on_representation(self):
        """
        _get_serializer_from_model_or_instance raises KeyError for an unregistered model.
        Tested via the public to_representation() API with a Duck instance, which
        internally calls _get_serializer_from_model_or_instance(Duck) and raises KeyError
        since Duck is not in model_serializer_mapping.
        """
        from polymorphic.contrib.drf.serializers import PolymorphicSerializer
        from polymorphic.tests.models import Duck

        class TestSer(PolymorphicSerializer):
            model_serializer_mapping = {
                Model2A: self.ModelASerializer,
            }

        s = TestSer()
        # to_representation(Duck()) calls _get_serializer_from_model_or_instance(Duck)
        # Duck is not in model_serializer_mapping and shares no MRO with Model2A
        with pytest.raises(KeyError, match="missing"):
            s.to_representation(Duck(name="test_duck"))

    def test_init_with_callable_serializer(self):
        """When serializer in mapping is callable (class), it gets instantiated."""
        from rest_framework import serializers as drf_serializers

        from polymorphic.contrib.drf.serializers import PolymorphicSerializer

        class ModelCSerializer(drf_serializers.Serializer):
            field3 = drf_serializers.CharField()

        class TestSerializer(PolymorphicSerializer):
            model_serializer_mapping = {
                Model2C: ModelCSerializer,  # Callable (class)
            }

        s = TestSerializer()
        assert Model2C in s.model_serializer_mapping
        # The serializer should be an instance, not the class
        assert isinstance(s.model_serializer_mapping[Model2C], ModelCSerializer)

    def test_is_valid_validation_error_in_child(self):
        """is_valid() handles ValidationError when getting child serializer."""
        from rest_framework import serializers as drf_serializers

        from polymorphic.contrib.drf.serializers import PolymorphicSerializer

        class StrictSerializer(PolymorphicSerializer):
            model_serializer_mapping = {
                Model2A: self.ModelASerializer,
            }

        # Data with unknown resource type -> ValidationError in child serializer lookup
        data = {"resourcetype": "Unknown", "field1": "test"}
        s = StrictSerializer(data=data)
        result = s.is_valid()
        assert result is False


# ===========================================================================
# contrib/extra_views.py - PolymorphicFormSetMixin tests (lines 48-52, 55, 65-69)
# ===========================================================================


class TestPolymorphicFormSetMixin(TestCase):
    """Tests for PolymorphicFormSetMixin in contrib/extra_views.py."""

    pytestmark = pytest.mark.integration

    def test_get_formset_children_raises_when_not_set(self):
        """
        Lines 48-51: get_formset_children() raises ImproperlyConfigured
        when formset_children is not set.
        """
        from django.core.exceptions import ImproperlyConfigured

        from polymorphic.contrib.extra_views import PolymorphicFormSetMixin

        mixin = PolymorphicFormSetMixin()
        mixin.formset_children = None

        with pytest.raises(ImproperlyConfigured, match="formset_children"):
            mixin.get_formset_children()

    def test_get_formset_children_raises_when_empty_list(self):
        """
        Lines 48-51: Empty list also raises ImproperlyConfigured
        (falsy value).
        """
        from django.core.exceptions import ImproperlyConfigured

        from polymorphic.contrib.extra_views import PolymorphicFormSetMixin

        mixin = PolymorphicFormSetMixin()
        mixin.formset_children = []  # falsy

        with pytest.raises(ImproperlyConfigured, match="formset_children"):
            mixin.get_formset_children()

    def test_get_formset_children_returns_when_set(self):
        """
        Line 52: When formset_children is set, returns it.
        """
        from polymorphic.contrib.extra_views import PolymorphicFormSetMixin

        children = [PolymorphicFormSetChild(Model2A)]
        mixin = PolymorphicFormSetMixin()
        mixin.formset_children = children

        result = mixin.get_formset_children()
        assert result is children

    def test_get_formset_child_kwargs_returns_empty_dict(self):
        """
        Line 55: get_formset_child_kwargs() returns empty dict.
        """
        from polymorphic.contrib.extra_views import PolymorphicFormSetMixin

        mixin = PolymorphicFormSetMixin()
        result = mixin.get_formset_child_kwargs()
        assert result == {}

    def test_get_formset_calls_super_and_adds_child_forms(self):
        """
        Lines 65-69: get_formset() calls super().get_formset() then
        adds child_forms via polymorphic_child_forms_factory.

        Tests this through the actual view's get_formset() method to exercise
        lines 65-69 via the public API rather than calling polymorphic_child_forms_factory
        directly.
        """
        from django.test import RequestFactory

        from polymorphic.contrib.extra_views import PolymorphicFormSetView

        children = [
            PolymorphicFormSetChild(Model2A, fields=["field1"]),
            PolymorphicFormSetChild(Model2B, fields=["field1", "field2"]),
        ]

        class TestFormSetView(PolymorphicFormSetView):
            model = Model2A
            formset_children = children
            fields = ["field1"]

        factory = RequestFactory()
        request = factory.get("/")

        view = TestFormSetView()
        view.request = request
        view.kwargs = {}
        view.args = []
        view.object_list = Model2A.objects.none()

        # Call get_formset() via the view - this tests lines 65-69
        FormSet = view.get_formset()
        assert hasattr(FormSet, "child_forms")
        assert Model2A in FormSet.child_forms
        assert Model2B in FormSet.child_forms

    def test_polymorphic_formset_view_integration(self):
        """
        Integration test: PolymorphicFormSetView with get_formset method.
        Tests lines 65-69 by creating an actual FormSet via the view's factory.
        """
        from polymorphic.contrib.extra_views import PolymorphicFormSetView

        children = [
            PolymorphicFormSetChild(Model2A, fields=["field1"]),
            PolymorphicFormSetChild(Model2B, fields=["field1", "field2"]),
        ]

        class TestFormSetView(PolymorphicFormSetView):
            model = Model2A
            formset_children = children
            fields = ["field1"]

        # Create a request factory to simulate a GET request
        from django.test import RequestFactory

        factory = RequestFactory()
        request = factory.get("/")

        view = TestFormSetView()
        view.request = request
        view.kwargs = {}
        view.args = []
        view.object_list = Model2A.objects.none()

        # Call get_formset() - this tests lines 65-69
        FormSet = view.get_formset()
        assert hasattr(FormSet, "child_forms")
        assert Model2A in FormSet.child_forms
        assert Model2B in FormSet.child_forms


# ===========================================================================
# deletion.py - PolymorphicGuard.__init__ TypeError (line 53)
# ===========================================================================


class TestPolymorphicGuardTypeError(TestCase):
    """Test PolymorphicGuard raises TypeError when action is not callable."""

    def test_guard_with_non_callable_raises(self):
        """
        Line 53: PolymorphicGuard.__init__ raises TypeError when action is not callable.
        """
        from polymorphic.deletion import PolymorphicGuard

        with pytest.raises(TypeError, match="action must be callable"):
            PolymorphicGuard("not_a_callable")

    def test_guard_with_integer_raises(self):
        """TypeError also raised for integer action."""
        from polymorphic.deletion import PolymorphicGuard

        with pytest.raises(TypeError, match="action must be callable"):
            PolymorphicGuard(42)


# ===========================================================================
# query.py - real_concrete_class is None (branch 513->527)
# ===========================================================================


class TestRealConcreteClassIsNone(TransactionTestCase):
    """Test branch 513->527 when content_type_manager returns model_class()=None."""

    def test_real_concrete_class_none_from_get_for_id(self):
        """
        Branch 513->527: When content_type_manager.get_for_id(id).model_class()
        returns None, the object is appended as None and filtered out.

        The tricky part: for concrete models, polymorphic_ctype_id and
        real_concrete_class_id are the SAME ContentType pk, so naively patching
        model_class() to return None would also affect get_real_instance_class()
        → causing the stale CT path (continue at line 497) to trigger instead.

        Solution: use a stateful db_manager patch that returns a WrappedMgr only
        for the FIRST call (line 474 in _get_real_instances), which is what the
        content_type_manager uses at line 511. Subsequent calls from
        get_real_instance_class() and get_real_concrete_instance_class_id() get
        the normal manager and resolve correctly.
        """
        from unittest.mock import MagicMock, patch
        from django.contrib.contenttypes.models import ContentTypeManager

        obj_b = Model2B.objects.create(field1="ct_none_real_b", field2="B2")
        qs = Model2A.objects.all()

        b_concrete_ct = ContentType.objects.get_for_model(Model2B, for_concrete_model=True)

        # Create a mock CT whose model_class() returns None
        mock_ct = MagicMock()
        mock_ct.model_class.return_value = None

        # Patch ContentTypeManager.get_for_id with a call-count approach.
        #
        # For a Model2B object in a Model2A queryset, get_for_id(b_concrete_ct.pk)
        # is called 3 times with our target CT id:
        #   Call 1: from get_real_instance_class() at query.py line 492
        #   Call 2: from get_real_instance_class() nested inside get_real_concrete_instance_class_id() at line 493
        #   Call 3: from content_type_manager.get_for_id() at line 511 (the branch we want to cover)
        #
        # The first 2 calls must return the real CT (so get_real_instance_class returns Model2B
        # and real_concrete_class_id is set correctly). The 3rd call returns mock_ct → model_class()=None
        # → real_concrete_class=None → line 513 is False → branch 513->527 is taken.
        get_for_id_count = [0]
        original_get_for_id = ContentTypeManager.get_for_id

        def fake_get_for_id(self_mgr, ct_id):
            if ct_id == b_concrete_ct.pk:
                get_for_id_count[0] += 1
                if get_for_id_count[0] > 2:
                    # 3rd+ call: simulate orphaned content type (model has been uninstalled)
                    return mock_ct
            return original_get_for_id(self_mgr, ct_id)

        with patch.object(ContentTypeManager, "get_for_id", fake_get_for_id):
            # list(qs) triggers _polymorphic_iterator → _get_real_instances
            # Calls 1-2: get_real_instance_class/get_real_concrete_instance_class_id → real CT → Model2B
            # Call 3 at line 511: content_type_manager.get_for_id(b_concrete_ct.pk) → mock_ct → None
            # → branch 513->527: resultlist.append(None) → filtered out at line 636
            results = list(qs)
            model2b_results = [r for r in results if isinstance(r, Model2B)]
            assert len(model2b_results) == 0


# ===========================================================================
# query.py - annotation not on base object (branch 625->624)
# ===========================================================================


class TestAnnotationMissingFromBaseObject(TransactionTestCase):
    """Test branch 625->624 when annotation key is not on base_object."""

    def test_annotation_not_on_base_object_skipped(self):
        """
        Branch 625->624: When iterating annotation_select keys, if the annotation
        is NOT on base_object (hasattr returns False), it is skipped.

        We use the public qs.get_real_instances(base_objs) API (not _get_real_instances),
        passing base objects that have had their annotation stripped to simulate the case
        where a base object lacks an annotation attribute.
        """
        from django.db.models import Value

        Model2B.objects.create(field1="anno_miss_b", field2="B2")

        qs_annotated = Model2A.objects.annotate(phantom_annotation=Value(99))

        # Get base objects from annotated queryset using the public non_polymorphic() API
        base_objs = list(qs_annotated.non_polymorphic())

        # Strip the annotation attribute from base_objs to simulate it being absent
        for obj in base_objs:
            if "phantom_annotation" in obj.__dict__:
                del obj.__dict__["phantom_annotation"]

        # Use the PUBLIC get_real_instances() method (not _get_real_instances)
        # Branch 625->624: hasattr(base_object, "phantom_annotation") is False → skip
        result = qs_annotated.get_real_instances(base_objs)
        # The annotation won't be set on real objects, but no crash should occur
        assert isinstance(result, list)


# ===========================================================================
# query_translate.py - app-label path errors (lines 164-165, 167)
# ===========================================================================


class TestQueryTranslateAppLabelErrors(TestCase):
    """Test error paths in translate_polymorphic_Q_object when using app__Model syntax."""

    def test_nonexistent_model_raises_field_error(self):
        """
        Lines 164-165: LookupError is raised when app__Model doesn't exist,
        converted to FieldError.
        """
        from django.core.exceptions import FieldError

        from polymorphic.query_translate import translate_polymorphic_field_path

        # Use appname__ClassName syntax where the class doesn't exist
        with pytest.raises(FieldError, match="does not exist"):
            translate_polymorphic_field_path(Model2A, "tests__NonExistentModel123___field1")

    def test_unrelated_model_raises_field_error(self):
        """
        Line 167: FieldError raised when model exists but is not derived from queryset model.
        """
        from django.core.exceptions import FieldError

        from polymorphic.query_translate import translate_polymorphic_field_path

        # Duck is not derived from Model2A
        # Use app__ClassName syntax: 'tests__Duck___something'
        with pytest.raises(FieldError, match="not derived from"):
            translate_polymorphic_field_path(Model2A, "tests__Duck___field1")


# ===========================================================================
# contrib/drf/serializers.py - non-callable serializer (branch 35->39)
# ===========================================================================


class TestPolymorphicSerializerNonCallable(TestCase):
    """Test PolymorphicSerializer when mapping contains serializer instances (not classes)."""

    pytestmark = pytest.mark.integration

    def test_init_with_serializer_instance_not_callable(self):
        """
        Branch 35->39: When serializer in model_serializer_mapping is already an instance
        (not callable), it's used directly without calling it.
        """
        from rest_framework import serializers as drf_serializers

        from polymorphic.contrib.drf.serializers import PolymorphicSerializer

        class Model2ASerializer(drf_serializers.Serializer):
            field1 = drf_serializers.CharField()

        # Pass an INSTANCE (not a class) - this should NOT be called again
        serializer_instance = Model2ASerializer()

        class TestSerializer(PolymorphicSerializer):
            model_serializer_mapping = {
                Model2A: serializer_instance,  # already an instance, not callable
            }

        s = TestSerializer()
        # The instance should be used directly (same object)
        assert s.model_serializer_mapping[Model2A] is serializer_instance


# ===========================================================================
# contrib/drf/serializers.py - child_valid True but no _validated_data (branch 98->101)
# ===========================================================================


class TestPolymorphicSerializerChildValidNoValidatedData(TestCase):
    """Test branch 98->101 when child is valid but parent has no _validated_data."""

    pytestmark = pytest.mark.integration

    def test_is_valid_child_valid_parent_fails(self):
        """
        Branch 98->101: child_valid is True but hasattr(self, "_validated_data") is False.
        This happens when super().is_valid() returns False (so _validated_data is not set)
        but the child serializer still validates successfully.

        We can achieve this by making the parent's super() raise a ValidationError
        that is caught by the parent, returning False without setting _validated_data.
        """
        from rest_framework import serializers as drf_serializers

        from polymorphic.contrib.drf.serializers import PolymorphicSerializer

        class Model2ASerializer(drf_serializers.Serializer):
            field1 = drf_serializers.CharField()

            def create(self, validated_data):
                return Model2A(**validated_data)

        class TestSerializer(PolymorphicSerializer):
            model_serializer_mapping = {
                Model2A: Model2ASerializer,
            }

        # Provide valid data but mock super().is_valid() to return False without
        # setting _validated_data
        data = {"resourcetype": "Model2A", "field1": "test_val"}
        s = TestSerializer(data=data)

        # Patch the parent Serializer.is_valid to return False without setting _validated_data
        import unittest.mock as mock

        original_is_valid = drf_serializers.Serializer.is_valid

        def patched_is_valid(self_inner, *args, **kwargs):
            # Call real is_valid but then clear _validated_data to simulate absence
            result = original_is_valid(self_inner, *args, **kwargs)
            # Only affect the PolymorphicSerializer instance (not child serializers)
            if type(self_inner).__name__ == "TestSerializer":
                # Remove _validated_data if it was set
                if hasattr(self_inner, "_validated_data"):
                    del self_inner._validated_data
                return True  # Return True so child serializer is called
            return result

        with mock.patch.object(drf_serializers.Serializer, "is_valid", patched_is_valid):
            result = s.is_valid()
            # _validated_data was deleted before the check, so branch 98->101 executes:
            # the child's _validated_data is NOT merged into the parent, but no
            # AttributeError is raised and is_valid still returns True (parent + child valid).
            assert result is True
