# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
"""Tests for Python-defined TVM-FFI types: ``@py_class`` decorator and low-level Field API."""

# ruff: noqa: D102, PLR0124, PLW1641, UP006, UP045
from __future__ import annotations

import copy
import gc
import inspect
import itertools
import math
from typing import Any, ClassVar, Dict, List, Optional

import pytest
import tvm_ffi
from tvm_ffi import core
from tvm_ffi._dunder import _install_dataclass_dunders
from tvm_ffi._ffi_api import DeepCopy, RecursiveEq, RecursiveHash, ReprPrint
from tvm_ffi.core import MISSING, Object, TypeInfo, TypeSchema, _to_py_class_value
from tvm_ffi.dataclasses import KW_ONLY, Field, IntEnum, StrEnum, entry, field, py_class
from tvm_ffi.registry import _add_class_attrs
from tvm_ffi.testing import TestObjectBase as _TestObjectBase
from tvm_ffi.testing.testing import requires_py310

# ---------------------------------------------------------------------------
# Unique type key generator (avoids collisions across tests)
# ---------------------------------------------------------------------------
_counter = itertools.count()


def _unique_key(base: str) -> str:
    return f"testing.py_class_dec.{base}_{next(_counter)}"


def _get_type_info(cls: type) -> TypeInfo:
    ret = cls.__tvm_ffi_type_info__  # ty: ignore[unresolved-attribute]
    assert isinstance(ret, TypeInfo), f"Expected TypeInfo, got {type(ret)}"
    return ret


# ---------------------------------------------------------------------------
# Low-level helpers for _make_type-based tests
# ---------------------------------------------------------------------------
_counter_ff = itertools.count()


def _unique_key_ff(base: str) -> str:
    """Return a globally unique type key for low-level field tests."""
    return f"testing.py_class.{base}_{next(_counter_ff)}"


def _make_type(
    name: str,
    fields: List[Field],
    *,
    parent: type = core.Object,
    eq: bool = False,
    unsafe_hash: bool = False,
    repr: bool = True,
) -> type:
    """Create, register, and fully set up a Python-defined TVM-FFI type.

    Returns the ready-to-use Python class.
    """
    type_key = _unique_key_ff(name)
    parent_info = core._type_cls_to_type_info(parent)
    assert parent_info is not None
    cls = type(name, (parent,), {"__slots__": ()})
    info = core._register_py_class(parent_info, type_key, cls)
    info._register_fields(fields)
    setattr(cls, "__tvm_ffi_type_info__", info)
    _add_class_attrs(cls, info)
    _install_dataclass_dunders(
        cls,
        init=True,
        repr=repr,
        eq=eq,
        order=False,
        unsafe_hash=unsafe_hash,
    )
    return cls


# ###########################################################################
#  1. Basic registration
# ###########################################################################
class TestBasicRegistration:
    """@py_class decorator with different calling conventions."""

    def test_bare_decorator(self) -> None:
        @py_class(_unique_key("Bare"))
        class Bare(Object):
            x: int

        info = _get_type_info(Bare)
        assert info is not None
        assert len(info.fields) == 1
        assert info.fields[0].name == "x"

    def test_decorator_with_options(self) -> None:
        @py_class(_unique_key("Opts"), eq=True)
        class Opts(Object):
            x: int

        assert hasattr(Opts, "__eq__")
        assert Opts(x=1) == Opts(x=1)

    def test_auto_type_key(self) -> None:
        @py_class(_unique_key("AutoKey"))
        class AutoKey(Object):
            x: int

        info = _get_type_info(AutoKey)
        assert info.type_key.startswith("testing.")

    def test_explicit_type_key(self) -> None:
        key = _unique_key("ExplicitKey")

        @py_class(key)
        class ExplicitKey(Object):
            x: int

        assert _get_type_info(ExplicitKey).type_key == key

    def test_empty_class(self) -> None:
        @py_class(_unique_key("Empty"))
        class Empty(Object):
            pass

        obj = Empty()
        assert obj is not None

    def test_isinstance_check(self) -> None:
        @py_class(_unique_key("InstCheck"))
        class InstCheck(Object):
            x: int

        obj = InstCheck(x=42)
        assert isinstance(obj, InstCheck)
        assert isinstance(obj, Object)


# ###########################################################################
#  2. Field parsing
# ###########################################################################
class TestFieldParsing:
    """Annotation-to-Field conversion."""

    def test_int_field(self) -> None:
        @py_class(_unique_key("IntFld"))
        class IntFld(Object):
            x: int

        obj = IntFld(x=42)
        assert obj.x == 42

    def test_float_field(self) -> None:
        @py_class(_unique_key("FltFld"))
        class FltFld(Object):
            x: float

        obj = FltFld(x=3.14)
        assert abs(obj.x - 3.14) < 1e-10

    def test_str_field(self) -> None:
        @py_class(_unique_key("StrFld"))
        class StrFld(Object):
            x: str

        obj = StrFld(x="hello")
        assert obj.x == "hello"

    def test_bool_field(self) -> None:
        @py_class(_unique_key("BoolFld"))
        class BoolFld(Object):
            x: bool

        obj = BoolFld(x=True)
        assert obj.x is True

    def test_payload_enum_fields_end_to_end(self) -> None:
        """IntEnum/StrEnum fields on @py_class accept raw payloads and expose enum objects."""

        class Priority(IntEnum, type_key=_unique_key("Priority")):
            low = entry(value=10)
            high = entry(value=20)

        class Opcode(StrEnum, type_key=_unique_key("Opcode")):
            add = entry(value="+")
            mul = entry(value="*")

        @py_class(_unique_key("EnumFields"))
        class Instruction(Object):
            priority: Priority
            opcode: Opcode

        from_payload = Instruction(priority=20, opcode="*")  # ty: ignore[invalid-argument-type]
        assert isinstance(from_payload.priority, Priority)
        assert isinstance(from_payload.opcode, Opcode)
        assert from_payload.priority.same_as(Priority.high)
        assert from_payload.opcode.same_as(Opcode.mul)
        assert from_payload.priority.value == 20
        assert from_payload.opcode.value == "*"

        from_enum = Instruction(priority=Priority.low, opcode=Opcode.add)
        assert from_enum.priority.same_as(Priority.low)
        assert from_enum.opcode.same_as(Opcode.add)

        from_enum.priority = 20  # ty: ignore[invalid-assignment]
        from_enum.opcode = "*"  # ty: ignore[invalid-assignment]
        assert from_enum.priority.same_as(Priority.high)
        assert from_enum.opcode.same_as(Opcode.mul)

        with pytest.raises((TypeError, RuntimeError), match="expected"):
            from_enum.priority = 99  # ty: ignore[invalid-assignment]
        with pytest.raises((TypeError, RuntimeError), match="expected"):
            from_enum.opcode = "/"  # ty: ignore[invalid-assignment]

    @requires_py310
    def test_optional_field(self) -> None:
        @py_class(_unique_key("OptFld"))
        class OptFld(Object):
            x: Optional[int]

        obj = OptFld(x=42)
        assert obj.x == 42
        obj2 = OptFld(x=None)
        assert obj2.x is None

    def test_multiple_fields(self) -> None:
        @py_class(_unique_key("Multi"))
        class Multi(Object):
            a: int
            b: float
            c: str

        obj = Multi(a=1, b=2.0, c="three")
        assert obj.a == 1
        assert obj.b == 2.0
        assert obj.c == "three"


# ###########################################################################
#  3. Defaults
# ###########################################################################
class TestDefaults:
    """Default values and default_factory."""

    def test_bare_default(self) -> None:
        @py_class(_unique_key("BareDef"))
        class BareDef(Object):
            x: int
            y: int = 10

        obj = BareDef(x=1)
        assert obj.y == 10

    def test_field_default(self) -> None:
        @py_class(_unique_key("FldDef"))
        class FldDef(Object):
            x: int = field(default=42)

        obj = FldDef()
        assert obj.x == 42

    def test_field_default_factory(self) -> None:
        call_count = 0

        def make_default() -> int:
            nonlocal call_count
            call_count += 1
            return 99

        @py_class(_unique_key("FldFact"))
        class FldFact(Object):
            x: int = field(default_factory=make_default)

        obj1 = FldFact()
        assert obj1.x == 99
        obj2 = FldFact()
        assert obj2.x == 99
        assert call_count == 2

    def test_default_and_factory_mutually_exclusive(self) -> None:
        with pytest.raises(ValueError, match="cannot specify both"):
            field(default=1, default_factory=int)

    def test_non_callable_factory_rejected(self) -> None:
        with pytest.raises(TypeError, match="default_factory must be a callable"):
            field(default_factory=42)  # ty: ignore[invalid-argument-type]

    def test_required_before_optional(self) -> None:
        @py_class(_unique_key("ReqOpt"))
        class ReqOpt(Object):
            a: int
            b: int = 10

        obj = ReqOpt(1)
        assert obj.a == 1
        assert obj.b == 10


# ###########################################################################
#  4. KW_ONLY
# ###########################################################################
class TestKwOnly:
    """Keyword-only field support."""

    def test_kw_only_sentinel(self) -> None:
        @py_class(_unique_key("KWSent"))
        class KWSent(Object):
            a: int
            _: KW_ONLY
            b: int = 10

        obj = KWSent(1, b=20)  # ty: ignore[missing-argument]
        assert obj.a == 1
        assert obj.b == 20
        with pytest.raises(TypeError):
            KWSent(1, 2)  # ty: ignore[invalid-argument-type]

    def test_decorator_level_kw_only(self) -> None:
        @py_class(_unique_key("DecKW"), kw_only=True)
        class DecKW(Object):
            a: int
            b: int = 10

        obj = DecKW(a=1)
        assert obj.a == 1
        assert obj.b == 10
        with pytest.raises(TypeError):
            DecKW(1)  # ty: ignore[missing-argument,too-many-positional-arguments]

    def test_field_level_kw_only_override(self) -> None:
        @py_class(_unique_key("FldKW"))
        class FldKW(Object):
            a: int
            b: int = field(default=10, kw_only=True)

        obj = FldKW(1)
        assert obj.a == 1
        assert obj.b == 10
        with pytest.raises(TypeError):
            FldKW(1, 2)  # b is keyword-only


# ###########################################################################
#  5. ClassVar
# ###########################################################################
class TestClassVar:
    """ClassVar annotations are skipped."""

    def test_classvar_skipped(self) -> None:
        @py_class(_unique_key("CV"))
        class CV(Object):
            x: int
            count: ClassVar[int] = 0

        info = _get_type_info(CV)
        field_names = [f.name for f in info.fields]
        assert "x" in field_names
        assert "count" not in field_names

    def test_classvar_preserved_on_class(self) -> None:
        @py_class(_unique_key("CVPres"))
        class CVPres(Object):
            x: int
            tag: ClassVar[str] = "hello"

        assert CVPres.tag == "hello"


# ###########################################################################
#  6. Init generation
# ###########################################################################
class TestInit:
    """Auto-generated __init__."""

    def test_positional_args(self) -> None:
        @py_class(_unique_key("Pos"))
        class Pos(Object):
            a: int
            b: str

        obj = Pos(1, "hello")
        assert obj.a == 1
        assert obj.b == "hello"

    def test_keyword_args(self) -> None:
        @py_class(_unique_key("Kw"))
        class Kw(Object):
            a: int
            b: str

        obj = Kw(a=1, b="hello")
        assert obj.a == 1
        assert obj.b == "hello"

    def test_init_false_field(self) -> None:
        @py_class(_unique_key("NoInit"))
        class NoInit(Object):
            a: int
            b: int = field(default=99, init=False)

        obj = NoInit(a=1)
        assert obj.a == 1
        assert obj.b == 99

    def test_user_defined_init_preserved(self) -> None:
        @py_class(_unique_key("UserInit"), init=False)
        class UserInit(Object):
            a: int

            def __init__(self, val: int) -> None:
                self.a = val

        obj = UserInit(42)
        assert obj.a == 42

    def test_required_after_optional_reordered(self) -> None:
        """Required positional fields are reordered before optional ones in __init__."""

        @py_class(_unique_key("ReorderOwn"))
        class ReorderOwn(Object):
            x: int = 0
            y: int  # ty: ignore[dataclass-field-order]

        sig = inspect.signature(ReorderOwn.__init__)
        param_names = [n for n in sig.parameters if n != "self"]
        assert param_names[0] == "y"  # required comes first
        assert param_names[1] == "x"  # optional comes second

        obj = ReorderOwn(y=1)  # ty: ignore[missing-argument]
        assert obj.x == 0
        assert obj.y == 1

    def test_required_after_optional_in_parent(self) -> None:
        """Child required fields are reordered before parent optional fields."""

        @py_class(_unique_key("OptParent"))
        class OptParent(Object):
            x: int
            y: int = 0

        @py_class(_unique_key("ReqChild"))
        class ReqChild(OptParent):
            z: int

        sig = inspect.signature(ReqChild.__init__)
        param_names = [n for n in sig.parameters if n != "self"]
        # required (x, z) before optional (y)
        assert param_names == ["x", "z", "y"]

        obj = ReqChild(x=1, z=3)
        assert obj.x == 1
        assert obj.y == 0
        assert obj.z == 3

    def test_kw_only_exempt_from_reorder(self) -> None:
        """kw_only fields are not reordered with positional fields."""

        @py_class(_unique_key("KwReorder"))
        class KwReorder(Object):
            x: int = 0
            _: KW_ONLY  # ty: ignore[dataclass-field-order]
            y: int  # ty: ignore[dataclass-field-order]

        sig = inspect.signature(KwReorder.__init__)
        params = sig.parameters
        assert params["x"].kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
        assert params["y"].kind == inspect.Parameter.KEYWORD_ONLY

        obj = KwReorder(y=1)  # ty: ignore[missing-argument]
        assert obj.x == 0
        assert obj.y == 1

    def test_mixed_positional_and_kw_only_with_defaults(self) -> None:
        """Mixed positional/kw_only fields with defaults produce correct signature."""

        @py_class(_unique_key("MixedSig"))
        class MixedSig(Object):
            a: int = 0
            b: int  # ty: ignore[dataclass-field-order]
            _: KW_ONLY  # ty: ignore[dataclass-field-order]
            c: int = 10
            d: int  # ty: ignore[dataclass-field-order]

        sig = inspect.signature(MixedSig.__init__)
        param_names = [n for n in sig.parameters if n != "self"]
        # positional: b (required) before a (optional); kw_only: d (required) before c (optional)
        assert param_names == ["b", "a", "d", "c"]

        obj = MixedSig(b=2, d=4)  # ty: ignore[missing-argument]
        assert obj.a == 0
        assert obj.b == 2
        assert obj.c == 10
        assert obj.d == 4

    def test_init_false_excluded_from_signature(self) -> None:
        """init=False fields do not appear in __init__ signature."""

        @py_class(_unique_key("InitFalseSig"))
        class InitFalseSig(Object):
            a: int
            b: int = field(default=99, init=False)
            c: str

        sig = inspect.signature(InitFalseSig.__init__)
        param_names = [n for n in sig.parameters if n != "self"]
        assert "b" not in param_names
        assert "a" in param_names
        assert "c" in param_names


# ###########################################################################
#  7. __post_init__
# ###########################################################################
class TestPostInit:
    """__post_init__ support."""

    def test_post_init_called(self) -> None:
        post_init_called = False

        @py_class(_unique_key("PostInit"))
        class PostInit(Object):
            x: int

            def __post_init__(self) -> None:
                nonlocal post_init_called
                post_init_called = True

        PostInit(x=1)
        assert post_init_called

    def test_post_init_sees_field_values(self) -> None:
        @py_class(_unique_key("PostInitVal"))
        class PostInitVal(Object):
            x: int
            y: int = 10

            def __post_init__(self) -> None:
                # Fields should be set before __post_init__ is called
                assert self.x is not None
                assert self.y == 10

        PostInitVal(x=5)


# ###########################################################################
#  8. Repr
# ###########################################################################
class TestRepr:
    """__repr__ generation."""

    def test_repr_generated(self) -> None:
        @py_class(_unique_key("Repr"))
        class Repr(Object):
            x: int
            y: str

        obj = Repr(x=1, y="hello")
        r = repr(obj)
        assert "1" in r
        assert "hello" in r

    def test_repr_disabled(self) -> None:
        @py_class(_unique_key("NoRepr"), repr=False)
        class NoRepr(Object):
            x: int

        obj = NoRepr(x=1)
        # Should use default object repr
        r = repr(obj)
        assert "NoRepr" in r or "object at" in r


# ###########################################################################
#  9. Equality
# ###########################################################################
class TestEquality:
    """__eq__ and __ne__ generation."""

    def test_eq_enabled(self) -> None:
        @py_class(_unique_key("Eq"), eq=True)
        class Eq(Object):
            x: int
            y: str

        assert Eq(x=1, y="a") == Eq(x=1, y="a")
        assert Eq(x=1, y="a") != Eq(x=2, y="a")

    def test_eq_disabled_by_default(self) -> None:
        @py_class(_unique_key("NoEq"))
        class NoEq(Object):
            x: int

        a = NoEq(x=1)
        b = NoEq(x=1)
        # Without eq, identity comparison
        assert a != b
        assert a == a


# ###########################################################################
# 10. Order
# ###########################################################################
class TestOrder:
    """Comparison methods."""

    def test_order_enabled(self) -> None:
        @py_class(_unique_key("Ord"), eq=True, order=True)
        class Ord(Object):
            x: int

        assert Ord(x=1) < Ord(x=2)
        assert Ord(x=2) > Ord(x=1)
        assert Ord(x=1) <= Ord(x=1)
        assert Ord(x=1) >= Ord(x=1)


# ###########################################################################
# 11. Hash
# ###########################################################################
class TestHash:
    """__hash__ generation."""

    def test_unsafe_hash(self) -> None:
        @py_class(_unique_key("Hash"), eq=True, unsafe_hash=True)
        class Hash(Object):
            x: int

        a = Hash(x=1)
        b = Hash(x=1)
        assert hash(a) == hash(b)
        # Can be used in sets
        s = {a, b}
        assert len(s) == 1


# ###########################################################################
# 12. Copy
# ###########################################################################
class TestCopy:
    """__copy__, __deepcopy__, __replace__."""

    def test_shallow_copy(self) -> None:
        @py_class(_unique_key("SCopy"))
        class SCopy(Object):
            x: int

        obj = SCopy(x=42)
        obj2 = copy.copy(obj)
        assert obj2.x == 42

    def test_deep_copy(self) -> None:
        @py_class(_unique_key("DCopy"))
        class DCopy(Object):
            x: int

        obj = DCopy(x=42)
        obj2 = copy.deepcopy(obj)
        assert obj2.x == 42

    def test_replace(self) -> None:
        @py_class(_unique_key("Repl"))
        class Repl(Object):
            x: int
            y: str

        obj = Repl(x=1, y="a")
        obj2 = obj.__replace__(x=2)  # ty: ignore[unresolved-attribute]
        assert obj2.x == 2
        assert obj2.y == "a"


# ###########################################################################
# 13. Inheritance
# ###########################################################################
class TestInheritance:
    """Inheritance between py_class types."""

    def test_child_adds_fields(self) -> None:
        @py_class(_unique_key("Parent"))
        class Parent(Object):
            x: int

        @py_class(_unique_key("Child"))
        class Child(Parent):
            y: str

        obj = Child(x=1, y="hello")
        assert obj.x == 1
        assert obj.y == "hello"

    def test_child_isinstance(self) -> None:
        @py_class(_unique_key("P2"))
        class P2(Object):
            x: int

        @py_class(_unique_key("C2"))
        class C2(P2):
            y: str

        obj = C2(x=1, y="hello")
        assert isinstance(obj, C2)
        assert isinstance(obj, P2)
        assert isinstance(obj, Object)

    def test_three_level_inheritance(self) -> None:
        @py_class(_unique_key("L1"))
        class L1(Object):
            a: int

        @py_class(_unique_key("L2"))
        class L2(L1):
            b: int

        @py_class(_unique_key("L3"))
        class L3(L2):
            c: int

        obj = L3(a=1, b=2, c=3)
        assert obj.a == 1
        assert obj.b == 2
        assert obj.c == 3


# ###########################################################################
# 14. Forward references / deferred resolution
# ###########################################################################
class TestForwardReferences:
    """Deferred annotation resolution for mutual and self-references."""

    @requires_py310
    def test_self_reference(self) -> None:
        @py_class(_unique_key("SelfRef"))
        class SelfRef(Object):
            value: int
            next_node: SelfRef | None

        leaf = SelfRef(value=2, next_node=None)
        head = SelfRef(value=1, next_node=leaf)
        assert head.next_node is not None
        assert head.next_node.value == 2

    @requires_py310
    def test_mutual_reference(self) -> None:
        """Two classes that reference each other."""

        @py_class(_unique_key("Foo"))
        class Foo(Object):
            value: int
            bar: Bar | None

        @py_class(_unique_key("Bar"))
        class Bar(Object):
            value: int
            foo: Foo | None

        bar = Bar(value=2, foo=None)
        foo = Foo(value=1, bar=bar)
        assert foo.bar is not None
        assert foo.bar.value == 2

    @requires_py310
    def test_deferred_resolution_on_instantiation(self) -> None:
        """Forward ref resolved on first instantiation."""

        @py_class(_unique_key("Early"))
        class Early(Object):
            value: int
            ref: Late | None

        # At this point, Early's fields are deferred because Late doesn't exist

        @py_class(_unique_key("Late"))
        class Late(Object):
            value: int

        # Now Early should resolve (either via flush or on instantiation)
        obj = Early(value=1, ref=Late(value=2))
        assert obj.ref is not None
        assert obj.ref.value == 2

    @requires_py310
    def test_cross_module_forward_ref_via_circular_import(self) -> None:
        """Forward ref to a class in another module that isn't in the declaring
        module's globals (circular import / TYPE_CHECKING-gated) still resolves.

        Mirrors the loom codegen case where ``weave_ir.TaskSpec`` has a field
        ``body: tuple[Op, ...]`` and ``Op`` lives in ``ops`` — ``ops`` already
        imports from ``weave_ir``, so ``Op`` cannot appear in ``weave_ir``'s
        globals without introducing a circular import.
        """
        # Module B: the target of the forward ref.  Define it first so it's
        # registered in ``_PY_CLASS_BY_MODULE`` under its own module key when
        # module A's phase-2 runs.
        ns_b: Dict[str, Any] = {
            "__name__": "testing.cross_mod_b",
            "py_class": py_class,
            "Object": Object,
            "_unique_key": _unique_key,
        }
        exec(
            "from __future__ import annotations\n"
            "@py_class(_unique_key('CrossChild'))\n"
            "class CrossChild(Object):\n"
            "    x: int\n",
            ns_b,
        )
        child_cls = ns_b["CrossChild"]
        assert child_cls.__module__ == "testing.cross_mod_b"

        # Module A: references ``CrossChild`` by name but does NOT have it
        # in its globals (simulating a ``TYPE_CHECKING``-gated import).
        ns_a: Dict[str, Any] = {
            "__name__": "testing.cross_mod_a",
            "py_class": py_class,
            "Object": Object,
            "_unique_key": _unique_key,
        }
        exec(
            "from __future__ import annotations\n"
            "@py_class(_unique_key('CrossHolder'))\n"
            "class Holder(Object):\n"
            "    value: int\n"
            "    child: CrossChild | None = None\n",
            ns_a,
        )
        holder_cls = ns_a["Holder"]
        assert holder_cls.__module__ == "testing.cross_mod_a"

        # Instantiation forces phase-2 to run; the cross-module localns
        # fallback should pick up CrossChild from module B even though it's
        # neither in module A's globals nor in module A's per-module registry.
        child = child_cls(x=7)
        holder = holder_cls(value=1, child=child)
        assert holder.child is not None
        assert holder.child.x == 7


# ###########################################################################
# 15. User-defined dunder preservation
# ###########################################################################
class TestDunderPreservation:
    """User-defined dunders are not overwritten."""

    def test_user_repr_preserved(self) -> None:
        @py_class(_unique_key("UserRepr"))
        class UserRepr(Object):
            x: int

            def __repr__(self) -> str:
                return f"Custom({self.x})"

        obj = UserRepr(x=42)
        assert repr(obj) == "Custom(42)"

    def test_user_eq_preserved(self) -> None:
        @py_class(_unique_key("UserEq"), eq=True)
        class UserEq(Object):
            x: int

            def __eq__(self, other: object) -> bool:
                return False

        assert not (UserEq(x=1) == UserEq(x=1))


# ###########################################################################
# 16. field() API
# ###########################################################################
class TestFieldAPI:
    """field() function returns a Field."""

    def test_field_returns_field(self) -> None:
        f = field(default=42)
        assert isinstance(f, Field)
        assert f.default == 42

    def test_field_defaults(self) -> None:
        f = field()
        assert f.init is True
        assert f.repr is True
        assert f.hash is None  # None = follow compare
        assert f.compare is True

    def test_field_kw_only_missing_by_default(self) -> None:
        f = field()
        assert f.kw_only is None

    def test_field_repr_false(self) -> None:
        @py_class(_unique_key("FldRepr"))
        class FldRepr(Object):
            x: int
            y: int = field(default=0, repr=False)

        obj = FldRepr(x=1)
        r = repr(obj)
        assert "1" in r
        # y with repr=False should not appear in repr
        # (depends on C++ ReprPrint implementation respecting the flag)


# ###########################################################################
# 17. Edge cases
# ###########################################################################
class TestEdgeCases:
    """Edge cases and error conditions."""

    def test_no_ffi_parent_raises(self) -> None:
        with pytest.raises(TypeError, match="must inherit from"):

            @py_class(_unique_key("NoPar"))
            class NoPar:  # no Object parent!
                x: int

    def test_only_classvar(self) -> None:
        @py_class(_unique_key("OnlyCV"))
        class OnlyCV(Object):
            count: ClassVar[int] = 0

        obj = OnlyCV()
        assert obj is not None

    def test_mutation_after_creation(self) -> None:
        @py_class(_unique_key("Mut"))
        class Mut(Object):
            x: int

        obj = Mut(x=1)
        obj.x = 42
        assert obj.x == 42


# ###########################################################################
# 18. hash=None tri-state
# ###########################################################################
class TestHashTriState:
    """field(hash=None) means 'follow compare' (native dataclass semantics)."""

    def test_hash_none_follows_compare_true(self) -> None:
        """hash=None + compare=True → field participates in hash."""

        @py_class(_unique_key("HNT"), eq=True, unsafe_hash=True)
        class HNT(Object):
            x: int  # default: compare=True, hash=None → hash=True

        a = HNT(x=1)
        b = HNT(x=1)
        assert hash(a) == hash(b)

    def test_hash_none_follows_compare_false(self) -> None:
        """hash=None + compare=False → field excluded from hash."""

        @py_class(_unique_key("HNF"), eq=True, unsafe_hash=True)
        class HNF(Object):
            x: int
            y: int = field(compare=False)  # hash=None → follows compare=False

        # y doesn't participate in hash, so different y values → same hash
        a = HNF(x=1, y=10)
        b = HNF(x=1, y=20)
        assert hash(a) == hash(b)

    def test_hash_explicit_true_with_compare_true(self) -> None:
        """hash=True + compare=True → field participates in hash."""

        @py_class(_unique_key("HET"), eq=True, unsafe_hash=True)
        class HET(Object):
            x: int = field(hash=True)  # compare=True (default)

        a = HET(x=1)
        b = HET(x=2)
        assert hash(a) != hash(b)

    def test_hash_explicit_false(self) -> None:
        """hash=False excludes field from hashing even with compare=True."""

        @py_class(_unique_key("HEF"), eq=True, unsafe_hash=True)
        class HEF(Object):
            x: int
            y: int = field(hash=False)  # compare=True but hash=False

        a = HEF(x=1, y=10)
        b = HEF(x=1, y=20)
        assert hash(a) == hash(b)


# ###########################################################################
# 19. Deferred resolution + user __init__ / init=False
# ###########################################################################
class TestDeferredInitPreservation:
    """Deferred resolution preserves user-defined __init__ and init=False."""

    @requires_py310
    def test_deferred_with_user_init(self) -> None:
        """User-defined __init__ is preserved after deferred resolution."""

        @py_class(_unique_key("DefUI"))
        class DefUI(Object):
            value: int
            ref: DefUILate | None

            def __init__(self, value: int) -> None:
                self.value = value
                self.ref = None

        @py_class(_unique_key("DefUILate"))
        class DefUILate(Object):
            x: int

        # DefUI should use the user-defined __init__ (one positional arg)
        obj = DefUI(42)
        assert obj.value == 42
        assert obj.ref is None

    @requires_py310
    def test_deferred_with_init_false(self) -> None:
        """init=False is respected after deferred resolution."""

        @py_class(_unique_key("DefNoInit"), init=False)
        class DefNoInit(Object):
            value: int
            ref: DefNoInitLate | None

            def __init__(self, v: int) -> None:
                self.value = v
                self.ref = None

        @py_class(_unique_key("DefNoInitLate"))
        class DefNoInitLate(Object):
            x: int

        obj = DefNoInit(10)
        assert obj.value == 10


# ###########################################################################
# 21. order=True requires eq=True
# ###########################################################################
class TestOrderEqValidation:
    """order=True without eq=True is rejected."""

    def test_order_without_eq_raises(self) -> None:
        with pytest.raises(ValueError, match="order=True requires eq=True"):

            @py_class(_unique_key("OrdNoEq"), order=True)
            class OrdNoEq(Object):
                x: int


# ###########################################################################
# 23. Registration rollback on failure
# ###########################################################################
class TestRegistrationRollback:
    """Failed decorations don't permanently poison the type registry."""

    def test_failed_decoration_allows_retry(self) -> None:
        key = _unique_key("Rollback")

        with pytest.raises(Exception):

            @py_class(key)
            class Bad(Object):
                x: object  # unsupported annotation type

        # The type key should be available for reuse
        @py_class(key)
        class Good(Object):
            x: int
            y: int = 0

        assert Good(x=1).y == 0


# ###########################################################################
# 24. User-defined __replace__ preserved
# ###########################################################################
class TestUserReplace:
    """User-defined __replace__ is not overwritten by py_class."""

    def test_user_replace_preserved(self) -> None:
        @py_class(_unique_key("UserRepl"))
        class UserRepl(Object):
            x: int

            def __replace__(self, **changes: object) -> str:
                return "custom"

        obj = UserRepl(x=1)
        assert obj.__replace__(x=2) == "custom"


# ###########################################################################
# 25. default_factory=None raises
# ###########################################################################
class TestDefaultFactoryNone:
    """Explicit default_factory=None matches stdlib semantics (raises)."""

    def test_explicit_none_raises(self) -> None:
        with pytest.raises(TypeError, match="default_factory must be a callable"):
            field(default_factory=None)


# ###########################################################################
# 26. Adversarial edge cases for init reordering
# ###########################################################################
class TestInitReorderingAdversarial:
    """Tricky scenarios that catch bugs in naive init-signature generation."""

    def test_positional_call_maps_to_required_not_declared_order(self) -> None:
        """Positional arg 1 maps to the first *required* field, not the first declared."""

        @py_class(_unique_key("PosMap"))
        class PosMap(Object):
            x: int = 0  # optional, declared first
            y: int  # ty: ignore[dataclass-field-order]  # required, declared second

        # Positional call: first arg is y (required), not x (optional)
        obj = PosMap(42)  # ty: ignore[missing-argument]
        assert obj.y == 42
        assert obj.x == 0

    def test_relative_order_preserved_within_groups(self) -> None:
        """Within required and optional groups, declaration order is preserved."""

        @py_class(_unique_key("RelOrder"))
        class RelOrder(Object):
            a: int = 0
            b: int  # ty: ignore[dataclass-field-order]
            c: int = 1
            d: int  # ty: ignore[dataclass-field-order]

        sig = inspect.signature(RelOrder.__init__)
        param_names = [n for n in sig.parameters if n != "self"]
        # required: b, d (declaration order); optional: a, c (declaration order)
        assert param_names == ["b", "d", "a", "c"]

        obj = RelOrder(10, 20)  # ty: ignore[missing-argument]
        assert obj.b == 10
        assert obj.d == 20
        assert obj.a == 0
        assert obj.c == 1

    def test_default_factory_counts_as_optional(self) -> None:
        """default_factory makes a field optional for reordering purposes."""

        @py_class(_unique_key("DFReorder"))
        class DFReorder(Object):
            items: str = field(default_factory=lambda: "hello")
            count: int  # ty: ignore[dataclass-field-order]

        sig = inspect.signature(DFReorder.__init__)
        param_names = [n for n in sig.parameters if n != "self"]
        assert param_names[0] == "count"  # required first
        assert param_names[1] == "items"  # optional (factory) second

        obj = DFReorder(count=5)
        assert obj.count == 5
        assert obj.items == "hello"

    def test_three_level_hierarchy_reorder(self) -> None:
        """Required fields from all levels come before optional fields from all levels."""

        @py_class(_unique_key("G1"))
        class G1(Object):
            a: int  # required

        @py_class(_unique_key("P1"))
        class P1(G1):
            b: int = 0  # optional

        @py_class(_unique_key("C1"))
        class C1(P1):
            c: int  # required

        sig = inspect.signature(C1.__init__)
        param_names = [n for n in sig.parameters if n != "self"]
        # required (a, c) before optional (b)
        assert param_names == ["a", "c", "b"]

        obj = C1(a=1, c=3)
        assert obj.a == 1
        assert obj.b == 0
        assert obj.c == 3

    def test_kw_only_false_overrides_sentinel(self) -> None:
        """kw_only=False on a field after KW_ONLY sentinel makes it positional."""

        @py_class(_unique_key("KwOverride"))
        class KwOverride(Object):
            _: KW_ONLY
            a: int  # kw_only (inherits sentinel)
            b: int = field(kw_only=False)  # positional (explicit override)

        sig = inspect.signature(KwOverride.__init__)
        assert sig.parameters["a"].kind == inspect.Parameter.KEYWORD_ONLY
        assert sig.parameters["b"].kind == inspect.Parameter.POSITIONAL_OR_KEYWORD

        obj = KwOverride(42, a=1)  # ty: ignore[missing-argument,invalid-argument-type]
        assert obj.b == 42
        assert obj.a == 1

    def test_init_false_field_gets_default(self) -> None:
        """init=False field with default is set to default, not left uninitialized."""

        @py_class(_unique_key("InitFalseDef"))
        class InitFalseDef(Object):
            visible: int
            hidden: str = field(default="secret", init=False)

        obj = InitFalseDef(visible=1)
        assert obj.hidden == "secret"

    def test_post_init_sees_reordered_fields(self) -> None:
        """__post_init__ sees correct values even when __init__ reorders fields."""
        seen: Dict[str, int] = {}

        @py_class(_unique_key("PostReorder"))
        class PostReorder(Object):
            x: int = 0
            y: int  # ty: ignore[dataclass-field-order]

            def __post_init__(self) -> None:
                seen["x"] = self.x
                seen["y"] = self.y

        PostReorder(y=10, x=20)
        assert seen == {"x": 20, "y": 10}

    @requires_py310
    def test_deferred_forward_ref_with_reordering(self) -> None:
        """Deferred forward-reference resolution still produces correct reordering."""

        @py_class(_unique_key("DeferReorder"))
        class DeferReorder(Object):
            opt: DeferLate | None = None
            req: int  # ty: ignore[dataclass-field-order]

        @py_class(_unique_key("DeferLate"))
        class DeferLate(Object):
            x: int

        sig = inspect.signature(DeferReorder.__init__)
        param_names = [n for n in sig.parameters if n != "self"]
        assert param_names[0] == "req"
        assert param_names[1] == "opt"

        obj = DeferReorder(req=1)
        assert obj.req == 1
        assert obj.opt is None

    def test_all_optional_preserves_declaration_order(self) -> None:
        """When all fields are optional, declaration order is preserved."""

        @py_class(_unique_key("AllOpt"))
        class AllOpt(Object):
            c: int = 3
            a: int = 1
            b: int = 2

        sig = inspect.signature(AllOpt.__init__)
        param_names = [n for n in sig.parameters if n != "self"]
        assert param_names == ["c", "a", "b"]

        obj = AllOpt()
        assert obj.c == 3
        assert obj.a == 1
        assert obj.b == 2

    def test_all_required_preserves_declaration_order(self) -> None:
        """When all fields are required, declaration order is preserved."""

        @py_class(_unique_key("AllReq"))
        class AllReq(Object):
            c: int
            a: int
            b: int

        sig = inspect.signature(AllReq.__init__)
        param_names = [n for n in sig.parameters if n != "self"]
        assert param_names == ["c", "a", "b"]

        obj = AllReq(10, 20, 30)
        assert obj.c == 10
        assert obj.a == 20
        assert obj.b == 30


# ###########################################################################
#  1. Registration
# ###########################################################################
class TestRegisterPyClass:
    """Low-level _register_py_class: type allocation, ancestors, field lifecycle."""

    def test_basic_registration(self) -> None:
        type_key = _unique_key_ff("RegBasic")
        parent_info = core._type_cls_to_type_info(core.Object)
        assert parent_info is not None
        cls = type("RegBasic", (core.Object,), {"__slots__": ()})
        info = core._register_py_class(parent_info, type_key, cls)
        assert info is not None
        assert info.type_key == type_key

    def test_type_index_allocated(self) -> None:
        type_key = _unique_key_ff("RegIndex")
        parent_info = core._type_cls_to_type_info(core.Object)
        assert parent_info is not None
        cls = type("RegIndex", (core.Object,), {"__slots__": ()})
        info = core._register_py_class(parent_info, type_key, cls)
        assert isinstance(info.type_index, int)
        assert info.type_index > 0

    def test_ancestors_include_parent(self) -> None:
        parent_info = core._type_cls_to_type_info(core.Object)
        assert parent_info is not None
        type_key = _unique_key_ff("RegAncestors")
        cls = type("RegAncestors", (core.Object,), {"__slots__": ()})
        info = core._register_py_class(parent_info, type_key, cls)
        assert parent_info.type_index in info.type_ancestors

    def test_parent_type_info_set(self) -> None:
        parent_info = core._type_cls_to_type_info(core.Object)
        assert parent_info is not None
        type_key = _unique_key_ff("RegParent")
        cls = type("RegParent", (core.Object,), {"__slots__": ()})
        info = core._register_py_class(parent_info, type_key, cls)
        assert info.parent_type_info is parent_info

    def test_initial_fields_none_and_methods_empty(self) -> None:
        parent_info = core._type_cls_to_type_info(core.Object)
        assert parent_info is not None
        type_key = _unique_key_ff("RegEmpty")
        cls = type("RegEmpty", (core.Object,), {"__slots__": ()})
        info = core._register_py_class(parent_info, type_key, cls)
        assert info.fields is None
        assert len(info.methods) == 0

    def test_two_registrations_different_indices(self) -> None:
        parent_info = core._type_cls_to_type_info(core.Object)
        assert parent_info is not None
        cls1 = type("RegDiff1", (core.Object,), {"__slots__": ()})
        cls2 = type("RegDiff2", (core.Object,), {"__slots__": ()})
        info1 = core._register_py_class(parent_info, _unique_key_ff("RegDiff1"), cls1)
        info2 = core._register_py_class(parent_info, _unique_key_ff("RegDiff2"), cls2)
        assert info1.type_index != info2.type_index

    def test_fields_none_before_registration(self) -> None:
        parent_info = core._type_cls_to_type_info(core.Object)
        assert parent_info is not None
        cls = type("Pending", (core.Object,), {"__slots__": ()})
        info = core._register_py_class(parent_info, _unique_key_ff("Pending"), cls)
        assert info.fields is None

    def test_register_fields_is_instance_method(self) -> None:
        parent_info = core._type_cls_to_type_info(core.Object)
        assert parent_info is not None
        cls = type("PendingM", (core.Object,), {"__slots__": ()})
        info = core._register_py_class(parent_info, _unique_key_ff("PendingM"), cls)
        assert hasattr(info, "_register_fields")

    def test_duplicate_type_key_raises(self) -> None:
        parent_info = core._type_cls_to_type_info(core.Object)
        assert parent_info is not None
        type_key = _unique_key_ff("Dup")
        cls1 = type("Dup1", (core.Object,), {"__slots__": ()})
        core._register_py_class(parent_info, type_key, cls1)
        cls2 = type("Dup2", (core.Object,), {"__slots__": ()})
        with pytest.raises((RuntimeError, ValueError)):
            core._register_py_class(parent_info, type_key, cls2)

    def test_duplicate_type_key_preserves_original(self) -> None:
        """After rejected duplicate, original entry is intact."""
        parent_info = core._type_cls_to_type_info(core.Object)
        assert parent_info is not None
        type_key = _unique_key_ff("DupPreserve")
        cls1 = type("DupPreserve1", (core.Object,), {"__slots__": ()})
        info1 = core._register_py_class(parent_info, type_key, cls1)
        info1._register_fields([Field(name="x", _ty_schema=TypeSchema("int"))])
        setattr(cls1, "__tvm_ffi_type_info__", info1)
        _add_class_attrs(cls1, info1)

        cls2 = type("DupPreserve2", (core.Object,), {"__slots__": ()})
        with pytest.raises((RuntimeError, ValueError)):
            core._register_py_class(parent_info, type_key, cls2)

        reloaded = core._lookup_or_register_type_info_from_type_key(type_key)
        assert reloaded.type_cls is cls1
        assert [f.name for f in reloaded.fields] == ["x"]


# ###########################################################################
#  2. Field Registration
# ###########################################################################
class TestFieldRegistration:
    """Low-level _register_fields: field types, metadata, offsets."""

    def test_int_field_registered(self) -> None:
        cls = _make_type(
            "FldInt",
            [Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
        )
        info = getattr(cls, "__tvm_ffi_type_info__")
        assert len(info.fields) == 1
        assert info.fields[0].name == "x"

    def test_float_field_registered(self) -> None:
        cls = _make_type(
            "FldFloat",
            [Field(name="val", _ty_schema=TypeSchema("float"), default=0.0)],
        )
        info = getattr(cls, "__tvm_ffi_type_info__")
        assert info.fields[0].name == "val"

    def test_str_field_registered(self) -> None:
        cls = _make_type(
            "FldStr",
            [Field(name="s", _ty_schema=TypeSchema("str"), default="hello")],
        )
        info = getattr(cls, "__tvm_ffi_type_info__")
        assert info.fields[0].name == "s"

    def test_bool_field_registered(self) -> None:
        cls = _make_type(
            "FldBool",
            [Field(name="flag", _ty_schema=TypeSchema("bool"), default=False)],
        )
        info = getattr(cls, "__tvm_ffi_type_info__")
        assert info.fields[0].name == "flag"

    def test_multiple_fields_count(self) -> None:
        cls = _make_type(
            "FldMulti",
            [
                Field(name="a", _ty_schema=TypeSchema("int"), default=MISSING),
                Field(name="b", _ty_schema=TypeSchema("float"), default=0.0),
                Field(name="c", _ty_schema=TypeSchema("str"), default="x"),
            ],
        )
        info = getattr(cls, "__tvm_ffi_type_info__")
        assert len(info.fields) == 3
        assert [f.name for f in info.fields] == ["a", "b", "c"]

    def test_field_offsets_increasing(self) -> None:
        cls = _make_type(
            "FldOff",
            [
                Field(name="a", _ty_schema=TypeSchema("int"), default=MISSING),
                Field(name="b", _ty_schema=TypeSchema("float"), default=MISSING),
                Field(name="c", _ty_schema=TypeSchema("str"), default=MISSING),
            ],
        )
        info = getattr(cls, "__tvm_ffi_type_info__")
        offsets = [f.offset for f in info.fields]
        for i in range(1, len(offsets)):
            assert offsets[i] > offsets[i - 1], f"Field offsets not increasing: {offsets}"

    def test_ffi_init_method_registered(self) -> None:
        cls = _make_type(
            "FldInit",
            [Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
        )
        info = getattr(cls, "__tvm_ffi_type_info__")
        ffi_init = core._lookup_type_attr(info.type_index, "__ffi_init__")
        assert ffi_init is not None

    def test_field_metadata_repr_flag(self) -> None:
        cls = _make_type(
            "FldReprMeta",
            [
                Field(
                    name="visible",
                    _ty_schema=TypeSchema("int"),
                    default=MISSING,
                    repr=True,
                ),
                Field(
                    name="hidden",
                    _ty_schema=TypeSchema("int"),
                    default=0,
                    repr=False,
                ),
            ],
        )
        info = getattr(cls, "__tvm_ffi_type_info__")
        assert len(info.fields) == 2


# ###########################################################################
#  3. Field Descriptor
# ###########################################################################
class TestFieldDescriptor:
    """Field class: validation, defaults, default_factory checks."""

    def test_compare_default_is_false(self) -> None:
        f = Field(name="x", _ty_schema=TypeSchema("int"))
        assert f.compare is False

    def test_default_and_factory_mutually_exclusive(self) -> None:
        with pytest.raises(ValueError):
            Field(name="x", _ty_schema=TypeSchema("int"), default=0, default_factory=lambda: 0)

    def test_factory_must_be_callable(self) -> None:
        with pytest.raises(TypeError, match="callable"):
            Field(name="x", _ty_schema=TypeSchema("int"), default_factory=0)  # ty: ignore[invalid-argument-type]

    def test_non_callable_factory_rejected(self) -> None:
        with pytest.raises(TypeError, match="callable"):
            Field(name="x", _ty_schema=TypeSchema("int"), default_factory="not_callable")  # ty: ignore[invalid-argument-type]


# ###########################################################################
#  4. Construction
# ###########################################################################
class TestConstruction:
    """Low-level __init__ via _make_type: positional/keyword args, defaults, errors."""

    def test_keyword_args(self) -> None:
        Cls = _make_type(
            "ConKw",
            [
                Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING),
                Field(name="y", _ty_schema=TypeSchema("float"), default=MISSING),
            ],
        )
        obj = Cls(x=42, y=3.14)
        assert obj.x == 42
        assert obj.y == pytest.approx(3.14)

    def test_positional_args(self) -> None:
        Cls = _make_type(
            "ConPos",
            [
                Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING),
                Field(name="y", _ty_schema=TypeSchema("float"), default=MISSING),
            ],
        )
        obj = Cls(10, 2.5)
        assert obj.x == 10
        assert obj.y == pytest.approx(2.5)

    def test_mixed_positional_and_keyword(self) -> None:
        Cls = _make_type(
            "ConMixed",
            [
                Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING),
                Field(name="y", _ty_schema=TypeSchema("float"), default=MISSING),
            ],
        )
        obj = Cls(7, y=1.5)
        assert obj.x == 7
        assert obj.y == pytest.approx(1.5)

    def test_default_value_int(self) -> None:
        Cls = _make_type(
            "ConDefInt",
            [Field(name="x", _ty_schema=TypeSchema("int"), default=99)],
        )
        assert Cls().x == 99

    def test_default_value_float(self) -> None:
        Cls = _make_type(
            "ConDefFloat",
            [Field(name="x", _ty_schema=TypeSchema("float"), default=1.5)],
        )
        assert Cls().x == pytest.approx(1.5)

    def test_default_value_str(self) -> None:
        Cls = _make_type(
            "ConDefStr",
            [Field(name="s", _ty_schema=TypeSchema("str"), default="hello")],
        )
        assert Cls().s == "hello"

    def test_override_default(self) -> None:
        Cls = _make_type(
            "ConOverride",
            [Field(name="x", _ty_schema=TypeSchema("int"), default=0)],
        )
        assert Cls(x=42).x == 42

    def test_required_and_optional_together(self) -> None:
        Cls = _make_type(
            "ConReqOpt",
            [
                Field(name="required", _ty_schema=TypeSchema("int"), default=MISSING),
                Field(name="optional", _ty_schema=TypeSchema("float"), default=0.0),
            ],
        )
        obj = Cls(required=5)
        assert obj.required == 5
        assert obj.optional == pytest.approx(0.0)

    def test_missing_required_raises(self) -> None:
        Cls = _make_type(
            "ConMissing",
            [Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
        )
        with pytest.raises(TypeError):
            Cls()

    def test_extra_kwarg_raises(self) -> None:
        Cls = _make_type(
            "ConExtra",
            [Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
        )
        with pytest.raises(TypeError):
            Cls(x=1, bogus=2)

    def test_str_field_construction(self) -> None:
        Cls = _make_type(
            "ConStr",
            [Field(name="name", _ty_schema=TypeSchema("str"), default=MISSING)],
        )
        assert Cls(name="world").name == "world"

    def test_bool_field_construction(self) -> None:
        Cls = _make_type(
            "ConBool",
            [Field(name="flag", _ty_schema=TypeSchema("bool"), default=MISSING)],
        )
        assert Cls(flag=True).flag is True
        assert Cls(flag=False).flag is False

    def test_kw_only_field(self) -> None:
        Cls = _make_type(
            "ConKwOnly",
            [
                Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING),
                Field(
                    name="y",
                    _ty_schema=TypeSchema("int"),
                    default=MISSING,
                    kw_only=True,
                ),
            ],
        )
        obj = Cls(1, y=2)
        assert obj.x == 1
        assert obj.y == 2

    def test_kw_only_rejects_positional(self) -> None:
        Cls = _make_type(
            "ConKwOnlyReject",
            [
                Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING),
                Field(
                    name="y",
                    _ty_schema=TypeSchema("int"),
                    default=MISSING,
                    kw_only=True,
                ),
            ],
        )
        with pytest.raises(TypeError):
            Cls(1, 2)

    def test_isinstance_check(self) -> None:
        Cls = _make_type(
            "ConIsInstance",
            [Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
        )
        obj = Cls(x=1)
        assert isinstance(obj, Cls)
        assert isinstance(obj, core.Object)


# ###########################################################################
#  5. Getter / Setter
# ###########################################################################
class TestGetterSetter:
    """Field access: get/set POD, str, bool, mutation isolation."""

    def test_get_int(self) -> None:
        Cls = _make_type(
            "GSInt",
            [Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
        )
        assert Cls(x=42).x == 42

    def test_set_int(self) -> None:
        Cls = _make_type(
            "GSSetInt",
            [Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
        )
        obj = Cls(x=1)
        obj.x = 100
        assert obj.x == 100

    def test_get_float(self) -> None:
        Cls = _make_type(
            "GSFloat",
            [Field(name="val", _ty_schema=TypeSchema("float"), default=MISSING)],
        )
        assert Cls(val=3.14).val == pytest.approx(3.14)

    def test_set_float(self) -> None:
        Cls = _make_type(
            "GSSetFloat",
            [Field(name="val", _ty_schema=TypeSchema("float"), default=MISSING)],
        )
        obj = Cls(val=1.0)
        obj.val = 2.718
        assert obj.val == pytest.approx(2.718)

    def test_get_str(self) -> None:
        Cls = _make_type(
            "GSStr",
            [Field(name="s", _ty_schema=TypeSchema("str"), default=MISSING)],
        )
        assert Cls(s="hello").s == "hello"

    def test_set_str(self) -> None:
        Cls = _make_type(
            "GSSetStr",
            [Field(name="s", _ty_schema=TypeSchema("str"), default=MISSING)],
        )
        obj = Cls(s="hello")
        obj.s = "world"
        assert obj.s == "world"

    def test_get_bool(self) -> None:
        Cls = _make_type(
            "GSBool",
            [Field(name="flag", _ty_schema=TypeSchema("bool"), default=MISSING)],
        )
        assert Cls(flag=True).flag is True

    def test_set_bool(self) -> None:
        Cls = _make_type(
            "GSSetBool",
            [Field(name="flag", _ty_schema=TypeSchema("bool"), default=MISSING)],
        )
        obj = Cls(flag=True)
        obj.flag = False
        assert obj.flag is False

    def test_mutation_isolated(self) -> None:
        Cls = _make_type(
            "GSIsolate",
            [Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
        )
        a = Cls(x=1)
        b = Cls(x=1)
        a.x = 99
        assert a.x == 99
        assert b.x == 1

    def test_multiple_fields_mutation(self) -> None:
        Cls = _make_type(
            "GSMultiMut",
            [
                Field(name="a", _ty_schema=TypeSchema("int"), default=MISSING),
                Field(name="b", _ty_schema=TypeSchema("float"), default=MISSING),
                Field(name="c", _ty_schema=TypeSchema("str"), default=MISSING),
            ],
        )
        obj = Cls(a=1, b=2.0, c="x")
        obj.a = 10
        obj.b = 20.0
        obj.c = "y"
        assert obj.a == 10
        assert obj.b == pytest.approx(20.0)
        assert obj.c == "y"

    def test_set_array_field(self) -> None:
        Cls = _make_type(
            "GSSetArr",
            [
                Field(
                    name="arr",
                    _ty_schema=TypeSchema("Array", (TypeSchema("int"),)),
                    default=MISSING,
                ),
            ],
        )
        obj = Cls(arr=[1])
        obj.arr = tvm_ffi.Array([4, 5, 6])
        assert len(obj.arr) == 3
        assert obj.arr[0] == 4


# ###########################################################################
#  6. ObjectRef Fields
# ###########################################################################
class TestObjectRefFields:
    """Fields holding ObjectRef types: Array, custom objects."""

    def test_array_field(self) -> None:
        Cls = _make_type(
            "ObjArr",
            [
                Field(
                    name="arr",
                    _ty_schema=TypeSchema("Array", (TypeSchema("int"),)),
                    default=MISSING,
                ),
            ],
        )
        obj = Cls(arr=tvm_ffi.Array([1, 2, 3]))
        assert len(obj.arr) == 3
        assert obj.arr[0] == 1
        assert obj.arr[2] == 3

    def test_array_field_from_list(self) -> None:
        Cls = _make_type(
            "ObjArrList",
            [
                Field(
                    name="arr",
                    _ty_schema=TypeSchema("Array", (TypeSchema("int"),)),
                    default=MISSING,
                ),
            ],
        )
        obj = Cls(arr=[10, 20, 30])
        assert len(obj.arr) == 3
        assert obj.arr[1] == 20

    def test_nested_object_field(self) -> None:
        Inner = _make_type(
            "ObjInner",
            [Field(name="val", _ty_schema=TypeSchema("int"), default=MISSING)],
        )
        inner_info = getattr(Inner, "__tvm_ffi_type_info__")
        inner_schema = TypeSchema(inner_info.type_key, origin_type_index=inner_info.type_index)
        Outer = _make_type(
            "ObjOuter",
            [Field(name="child", _ty_schema=inner_schema, default=MISSING)],
        )
        assert Outer(child=Inner(val=42)).child.val == 42


# ###########################################################################
#  7. Optional Fields
# ###########################################################################
class TestOptionalFields:
    """Optional/Union fields: None and non-None values."""

    def test_optional_int_with_value(self) -> None:
        Cls = _make_type(
            "OptIntV",
            [
                Field(
                    name="x",
                    _ty_schema=TypeSchema("Optional", (TypeSchema("int"),)),
                    default=MISSING,
                ),
            ],
        )
        assert Cls(x=42).x == 42

    def test_optional_int_with_none(self) -> None:
        Cls = _make_type(
            "OptIntN",
            [
                Field(
                    name="x",
                    _ty_schema=TypeSchema("Optional", (TypeSchema("int"),)),
                    default=None,
                ),
            ],
        )
        assert Cls().x is None

    def test_optional_str_with_value(self) -> None:
        Cls = _make_type(
            "OptStrV",
            [
                Field(
                    name="s",
                    _ty_schema=TypeSchema("Optional", (TypeSchema("str"),)),
                    default=MISSING,
                ),
            ],
        )
        assert Cls(s="hello").s == "hello"

    def test_optional_str_with_none(self) -> None:
        Cls = _make_type(
            "OptStrN",
            [
                Field(
                    name="s",
                    _ty_schema=TypeSchema("Optional", (TypeSchema("str"),)),
                    default=None,
                ),
            ],
        )
        assert Cls().s is None

    def test_optional_set_to_none(self) -> None:
        Cls = _make_type(
            "OptSet",
            [
                Field(
                    name="x",
                    _ty_schema=TypeSchema("Optional", (TypeSchema("int"),)),
                    default=MISSING,
                ),
            ],
        )
        obj = Cls(x=42)
        obj.x = None
        assert obj.x is None

    def test_optional_set_back_to_value(self) -> None:
        Cls = _make_type(
            "OptBack",
            [
                Field(
                    name="x",
                    _ty_schema=TypeSchema("Optional", (TypeSchema("int"),)),
                    default=None,
                ),
            ],
        )
        obj = Cls()
        obj.x = 99
        assert obj.x == 99

    def test_all_optional_fields_default_none(self) -> None:
        Cls = _make_type(
            "AllOpt",
            [
                Field(
                    name="a",
                    _ty_schema=TypeSchema("Optional", (TypeSchema("int"),)),
                    default=None,
                ),
                Field(
                    name="b",
                    _ty_schema=TypeSchema("Optional", (TypeSchema("str"),)),
                    default=None,
                ),
                Field(
                    name="c",
                    _ty_schema=TypeSchema("Optional", (TypeSchema("float"),)),
                    default=None,
                ),
            ],
        )
        obj = Cls()
        assert obj.a is None
        assert obj.b is None
        assert obj.c is None
        obj.a = 42
        assert obj.a == 42
        obj.b = "hello"
        assert obj.b == "hello"

    def test_optional_object_none_and_back(self) -> None:
        Cls = _make_type(
            "OptObjRound",
            [
                Field(
                    name="ref",
                    _ty_schema=TypeSchema("Optional", (TypeSchema("Object"),)),
                    default=None,
                ),
            ],
        )
        obj = Cls()
        assert obj.ref is None
        obj.ref = tvm_ffi.Array([1])
        assert len(obj.ref) == 1
        obj.ref = None
        assert obj.ref is None

    def test_union_int_str(self) -> None:
        """Union[int, str] field should accept both types."""
        Cls = _make_type(
            "UnionIntStr",
            [
                Field(
                    name="val",
                    _ty_schema=TypeSchema("Union", (TypeSchema("int"), TypeSchema("str"))),
                    default=MISSING,
                ),
            ],
        )
        obj = Cls(val=42)
        assert obj.val == 42
        obj.val = "hello"
        assert obj.val == "hello"

    def test_union_int_str_rejects_float(self) -> None:
        """Union[int, str] should reject float (not in union)."""
        Cls = _make_type(
            "UnionReject",
            [
                Field(
                    name="val",
                    _ty_schema=TypeSchema("Union", (TypeSchema("int"), TypeSchema("str"))),
                    default=MISSING,
                ),
            ],
        )
        obj = Cls(val=1)
        with pytest.raises((TypeError, RuntimeError)):
            obj.val = 3.14

    def test_optional_union(self) -> None:
        """Optional[Union[int, str]] should accept None, int, and str."""
        Cls = _make_type(
            "OptUnion",
            [
                Field(
                    name="val",
                    _ty_schema=TypeSchema(
                        "Optional",
                        (TypeSchema("Union", (TypeSchema("int"), TypeSchema("str"))),),
                    ),
                    default=None,
                ),
            ],
        )
        obj = Cls()
        assert obj.val is None
        obj.val = 42
        assert obj.val == 42
        obj.val = "hi"
        assert obj.val == "hi"
        obj.val = None
        assert obj.val is None


# ###########################################################################
#  8. Any Fields
# ###########################################################################
class TestAnyField:
    """Fields with TypeSchema('Any'): hold any value type."""

    def test_any_holds_int(self) -> None:
        Cls = _make_type(
            "AnyI",
            [Field(name="val", _ty_schema=TypeSchema("Any"), default=None)],
        )
        assert Cls(val=42).val == 42

    def test_any_holds_str(self) -> None:
        Cls = _make_type(
            "AnyS",
            [Field(name="val", _ty_schema=TypeSchema("Any"), default=None)],
        )
        assert Cls(val="hello").val == "hello"

    def test_any_holds_none(self) -> None:
        Cls = _make_type(
            "AnyN",
            [Field(name="val", _ty_schema=TypeSchema("Any"), default=None)],
        )
        assert Cls().val is None

    def test_any_holds_object(self) -> None:
        Cls = _make_type(
            "AnyObj",
            [Field(name="val", _ty_schema=TypeSchema("Any"), default=None)],
        )
        arr = tvm_ffi.Array([1, 2])
        assert len(Cls(val=arr).val) == 2

    def test_any_type_change(self) -> None:
        Cls = _make_type(
            "AnyChg",
            [Field(name="val", _ty_schema=TypeSchema("Any"), default=None)],
        )
        obj = Cls()
        obj.val = 42
        assert obj.val == 42
        obj.val = "hello"
        assert obj.val == "hello"
        obj.val = None
        assert obj.val is None
        obj.val = tvm_ffi.Array([1])
        assert len(obj.val) == 1


# ###########################################################################
#  9. Default Factory
# ###########################################################################
class TestDefaultFactory:
    """default_factory support: fresh instances, override, various types."""

    def test_factory_produces_fresh_instances(self) -> None:
        Cls = _make_type(
            "DFFresh",
            [
                Field(
                    name="data",
                    _ty_schema=TypeSchema("Array", (TypeSchema("int"),)),
                    default_factory=lambda: tvm_ffi.Array([]),
                ),
            ],
        )
        a = Cls()
        b = Cls()
        assert not a.data.same_as(b.data)

    def test_factory_with_content(self) -> None:
        Cls = _make_type(
            "DFContent",
            [
                Field(
                    name="items",
                    _ty_schema=TypeSchema("Array", (TypeSchema("int"),)),
                    default_factory=lambda: tvm_ffi.Array([1, 2, 3]),
                ),
            ],
        )
        obj = Cls()
        assert len(obj.items) == 3
        assert obj.items[0] == 1

    def test_factory_override(self) -> None:
        Cls = _make_type(
            "DFOverride",
            [Field(name="x", _ty_schema=TypeSchema("int"), default_factory=lambda: 42)],
        )
        assert Cls(x=99).x == 99

    def test_factory_str(self) -> None:
        Cls = _make_type(
            "DFStr",
            [Field(name="s", _ty_schema=TypeSchema("str"), default_factory=lambda: "generated")],
        )
        assert Cls().s == "generated"


# ###########################################################################
#  10. Repr
# ###########################################################################
class TestFieldRepr:
    """Low-level repr via _make_type: field values, repr=False exclusion."""

    def test_repr_includes_fields(self) -> None:
        Cls = _make_type(
            "ReprBasic",
            [
                Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING),
                Field(name="y", _ty_schema=TypeSchema("float"), default=0.0),
            ],
        )
        r = ReprPrint(Cls(x=42, y=3.14))
        assert "x=42" in r
        assert "y=3.14" in r

    def test_repr_str_field(self) -> None:
        Cls = _make_type(
            "ReprStr",
            [Field(name="name", _ty_schema=TypeSchema("str"), default=MISSING)],
        )
        assert '"hello"' in ReprPrint(Cls(name="hello"))

    def test_repr_bool_field(self) -> None:
        Cls = _make_type(
            "ReprBool",
            [Field(name="flag", _ty_schema=TypeSchema("bool"), default=MISSING)],
        )
        assert "flag=True" in ReprPrint(Cls(flag=True))

    def test_repr_false_excluded(self) -> None:
        Cls = _make_type(
            "ReprExcl",
            [
                Field(name="visible", _ty_schema=TypeSchema("int"), default=MISSING),
                Field(
                    name="hidden",
                    _ty_schema=TypeSchema("int"),
                    default=0,
                    repr=False,
                ),
            ],
        )
        r = ReprPrint(Cls(visible=42))
        assert "visible=42" in r
        assert "hidden" not in r

    def test_python_repr_delegates(self) -> None:
        Cls = _make_type(
            "ReprDeleg",
            [Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
        )
        assert "x=7" in repr(Cls(x=7))

    def test_repr_contains_type_key(self) -> None:
        Cls = _make_type(
            "ReprKey",
            [Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
        )
        info = getattr(Cls, "__tvm_ffi_type_info__")
        assert info.type_key in ReprPrint(Cls(x=1))

    def test_repr_optional_none(self) -> None:
        Cls = _make_type(
            "ReprOptNone",
            [
                Field(
                    name="x",
                    _ty_schema=TypeSchema("Optional", (TypeSchema("int"),)),
                    default=None,
                ),
            ],
        )
        r = ReprPrint(Cls())
        assert isinstance(r, str)

    def test_repr_array_field(self) -> None:
        Cls = _make_type(
            "ReprArr",
            [
                Field(
                    name="items",
                    _ty_schema=TypeSchema("Array", (TypeSchema("int"),)),
                    default=MISSING,
                ),
            ],
        )
        r = ReprPrint(Cls(items=[1, 2, 3]))
        assert isinstance(r, str)


# ###########################################################################
#  11. Hash
# ###########################################################################
class TestFieldHash:
    """Low-level hash via _make_type: equal objects same hash, hash=False exclusion."""

    def test_equal_objects_same_hash(self) -> None:
        Cls = _make_type(
            "HashEq",
            [
                Field(
                    name="x",
                    _ty_schema=TypeSchema("int"),
                    default=MISSING,
                    compare=True,
                ),
                Field(
                    name="y",
                    _ty_schema=TypeSchema("float"),
                    default=MISSING,
                    compare=True,
                ),
            ],
            eq=True,
            unsafe_hash=True,
        )
        assert RecursiveHash(Cls(x=1, y=2.0)) == RecursiveHash(Cls(x=1, y=2.0))

    def test_different_objects_different_hash(self) -> None:
        Cls = _make_type(
            "HashDiff",
            [
                Field(
                    name="x",
                    _ty_schema=TypeSchema("int"),
                    default=MISSING,
                    compare=True,
                ),
            ],
            eq=True,
            unsafe_hash=True,
        )
        assert RecursiveHash(Cls(x=1)) != RecursiveHash(Cls(x=2))

    def test_hash_false_field_ignored(self) -> None:
        Cls = _make_type(
            "HashIgn",
            [
                Field(
                    name="key",
                    _ty_schema=TypeSchema("int"),
                    default=MISSING,
                    compare=True,
                ),
                Field(
                    name="ignored",
                    _ty_schema=TypeSchema("int"),
                    default=0,
                    hash=False,
                ),
            ],
            eq=True,
            unsafe_hash=True,
        )
        assert RecursiveHash(Cls(key=42, ignored=100)) == RecursiveHash(Cls(key=42, ignored=999))

    def test_hash_dunder_installed(self) -> None:
        Cls = _make_type(
            "HashDunder",
            [Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
            eq=True,
            unsafe_hash=True,
        )
        assert isinstance(hash(Cls(x=42)), int)

    def test_usable_as_dict_key(self) -> None:
        Cls = _make_type(
            "HashDict",
            [
                Field(
                    name="x",
                    _ty_schema=TypeSchema("int"),
                    default=MISSING,
                    compare=True,
                ),
            ],
            eq=True,
            unsafe_hash=True,
        )
        assert {Cls(x=1): "value"}[Cls(x=1)] == "value"

    def test_usable_in_set(self) -> None:
        Cls = _make_type(
            "HashSet",
            [
                Field(
                    name="x",
                    _ty_schema=TypeSchema("int"),
                    default=MISSING,
                    compare=True,
                ),
            ],
            eq=True,
            unsafe_hash=True,
        )
        assert len({Cls(x=1), Cls(x=1), Cls(x=2)}) == 2


# ###########################################################################
#  12. Equality
# ###########################################################################
class TestFieldEquality:
    """Low-level equality via _make_type: structural compare, compare=False exclusion."""

    def test_equal_objects(self) -> None:
        Cls = _make_type(
            "EqEqual",
            [
                Field(
                    name="x",
                    _ty_schema=TypeSchema("int"),
                    default=MISSING,
                    compare=True,
                ),
                Field(
                    name="y",
                    _ty_schema=TypeSchema("float"),
                    default=MISSING,
                    compare=True,
                ),
            ],
            eq=True,
        )
        assert Cls(x=1, y=2.0) == Cls(x=1, y=2.0)

    def test_different_objects(self) -> None:
        Cls = _make_type(
            "EqDiff",
            [
                Field(
                    name="x",
                    _ty_schema=TypeSchema("int"),
                    default=MISSING,
                    compare=True,
                ),
            ],
            eq=True,
        )
        assert Cls(x=1) != Cls(x=2)

    def test_compare_false_field_ignored(self) -> None:
        Cls = _make_type(
            "EqIgn",
            [
                Field(
                    name="key",
                    _ty_schema=TypeSchema("int"),
                    default=MISSING,
                    compare=True,
                ),
                Field(
                    name="ignored",
                    _ty_schema=TypeSchema("int"),
                    default=0,
                    compare=False,
                ),
            ],
            eq=True,
        )
        assert RecursiveEq(Cls(key=42, ignored=100), Cls(key=42, ignored=999))

    def test_compare_off_excludes_from_eq(self) -> None:
        """Fields with compare=False (default) are ignored by RecursiveEq."""
        Cls = _make_type(
            "CmpOff",
            [Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
            eq=True,
        )
        assert RecursiveEq(Cls(x=1), Cls(x=2))

    def test_compare_true_includes_in_eq(self) -> None:
        Cls = _make_type(
            "CmpOn",
            [
                Field(
                    name="x",
                    _ty_schema=TypeSchema("int"),
                    default=MISSING,
                    compare=True,
                ),
            ],
            eq=True,
        )
        assert not RecursiveEq(Cls(x=1), Cls(x=2))

    def test_eq_reflexive(self) -> None:
        Cls = _make_type(
            "EqRefl",
            [
                Field(
                    name="x",
                    _ty_schema=TypeSchema("int"),
                    default=MISSING,
                    compare=True,
                ),
            ],
            eq=True,
        )
        a = Cls(x=42)
        assert a == a

    def test_eq_symmetric(self) -> None:
        Cls = _make_type(
            "EqSym",
            [
                Field(
                    name="x",
                    _ty_schema=TypeSchema("int"),
                    default=MISSING,
                    compare=True,
                ),
            ],
            eq=True,
        )
        a, b = Cls(x=1), Cls(x=1)
        assert a == b
        assert b == a

    def test_eq_with_str_field(self) -> None:
        Cls = _make_type(
            "EqStr",
            [
                Field(
                    name="s",
                    _ty_schema=TypeSchema("str"),
                    default=MISSING,
                    compare=True,
                ),
            ],
            eq=True,
        )
        assert RecursiveEq(Cls(s="hello"), Cls(s="hello"))
        assert not RecursiveEq(Cls(s="hello"), Cls(s="world"))

    def test_eq_hash_consistency(self) -> None:
        Cls = _make_type(
            "EqHashCon",
            [
                Field(
                    name="x",
                    _ty_schema=TypeSchema("int"),
                    default=MISSING,
                    compare=True,
                ),
                Field(
                    name="y",
                    _ty_schema=TypeSchema("float"),
                    default=MISSING,
                    compare=True,
                ),
            ],
            eq=True,
            unsafe_hash=True,
        )
        a, b = Cls(x=1, y=2.0), Cls(x=1, y=2.0)
        assert RecursiveEq(a, b)
        assert RecursiveHash(a) == RecursiveHash(b)


# ###########################################################################
#  13. Edge Cases
# ###########################################################################
class TestFieldEdgeCases:
    """Low-level edge cases via _make_type: empty class, extreme values, init=False."""

    def test_empty_class_no_fields(self) -> None:
        Cls = _make_type("EdgeEmpty", [])
        obj = Cls()
        assert isinstance(obj, core.Object)
        assert isinstance(obj, Cls)

    def test_empty_class_repr(self) -> None:
        Cls = _make_type("EdgeEmptyRepr", [])
        info = getattr(Cls, "__tvm_ffi_type_info__")
        assert info.type_key in ReprPrint(Cls())

    def test_bool_true_and_false(self) -> None:
        Cls = _make_type(
            "EdgeBool",
            [Field(name="flag", _ty_schema=TypeSchema("bool"), default=MISSING)],
        )
        assert Cls(flag=True).flag is True
        assert Cls(flag=False).flag is False

    def test_bool_default_false(self) -> None:
        Cls = _make_type(
            "EdgeBoolDef",
            [Field(name="flag", _ty_schema=TypeSchema("bool"), default=False)],
        )
        assert Cls().flag is False

    def test_multiple_types_together(self) -> None:
        Cls = _make_type(
            "EdgeMulti",
            [
                Field(
                    name="i",
                    _ty_schema=TypeSchema("int"),
                    default=MISSING,
                    compare=True,
                ),
                Field(name="f", _ty_schema=TypeSchema("float"), default=MISSING),
                Field(name="s", _ty_schema=TypeSchema("str"), default=MISSING),
                Field(name="b", _ty_schema=TypeSchema("bool"), default=MISSING),
            ],
        )
        obj = Cls(i=42, f=3.14, s="test", b=True)
        assert obj.i == 42
        assert obj.f == pytest.approx(3.14)
        assert obj.s == "test"
        assert obj.b is True

    def test_pod_and_objectref_mixed(self) -> None:
        Cls = _make_type(
            "EdgeMixed",
            [
                Field(name="count", _ty_schema=TypeSchema("int"), default=MISSING),
                Field(
                    name="items",
                    _ty_schema=TypeSchema("Array", (TypeSchema("int"),)),
                    default=MISSING,
                ),
                Field(name="label", _ty_schema=TypeSchema("str"), default=""),
            ],
        )
        obj = Cls(count=3, items=[1, 2, 3])
        assert obj.count == 3
        assert len(obj.items) == 3
        assert obj.label == ""

    def test_multiple_types_with_defaults(self) -> None:
        Cls = _make_type(
            "EdgeMultiDef",
            [
                Field(name="i", _ty_schema=TypeSchema("int"), default=0),
                Field(name="f", _ty_schema=TypeSchema("float"), default=1.0),
                Field(name="s", _ty_schema=TypeSchema("str"), default="default"),
                Field(name="b", _ty_schema=TypeSchema("bool"), default=True),
            ],
        )
        obj = Cls()
        assert obj.i == 0
        assert obj.f == pytest.approx(1.0)
        assert obj.s == "default"
        assert obj.b is True

    def test_zero_values(self) -> None:
        Cls = _make_type(
            "EdgeZero",
            [
                Field(name="i", _ty_schema=TypeSchema("int"), default=MISSING),
                Field(name="f", _ty_schema=TypeSchema("float"), default=MISSING),
            ],
        )
        obj = Cls(i=0, f=0.0)
        assert obj.i == 0
        assert obj.f == 0.0

    def test_negative_values(self) -> None:
        Cls = _make_type(
            "EdgeNeg",
            [
                Field(name="i", _ty_schema=TypeSchema("int"), default=MISSING),
                Field(name="f", _ty_schema=TypeSchema("float"), default=MISSING),
            ],
        )
        obj = Cls(i=-42, f=-3.14)
        assert obj.i == -42
        assert obj.f == pytest.approx(-3.14)

    def test_large_int(self) -> None:
        Cls = _make_type(
            "EdgeLargeInt",
            [Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
        )
        large = 2**62
        assert Cls(x=large).x == large

    def test_empty_string_field(self) -> None:
        Cls = _make_type(
            "EdgeEmptyStr",
            [Field(name="s", _ty_schema=TypeSchema("str"), default=MISSING)],
        )
        assert Cls(s="").s == ""

    def test_long_string_field(self) -> None:
        Cls = _make_type(
            "EdgeLongStr",
            [Field(name="s", _ty_schema=TypeSchema("str"), default=MISSING)],
        )
        long_str = "a" * 1000
        assert Cls(s=long_str).s == long_str

    def test_equality_empty_class(self) -> None:
        Cls = _make_type("EdgeEmptyEq", [], eq=True, unsafe_hash=True)
        assert RecursiveEq(Cls(), Cls())
        assert RecursiveHash(Cls()) == RecursiveHash(Cls())

    def test_init_false_field_excluded_from_init(self) -> None:
        Cls = _make_type(
            "EdgeInitFalse",
            [
                Field(name="visible", _ty_schema=TypeSchema("int"), default=MISSING),
                Field(
                    name="internal",
                    _ty_schema=TypeSchema("int"),
                    default=0,
                    init=False,
                ),
            ],
        )
        obj = Cls(visible=42)
        assert obj.visible == 42
        assert obj.internal == 0

    def test_init_false_field_rejected_as_kwarg(self) -> None:
        Cls = _make_type(
            "EdgeInitFalseReject",
            [
                Field(name="visible", _ty_schema=TypeSchema("int"), default=MISSING),
                Field(
                    name="internal",
                    _ty_schema=TypeSchema("int"),
                    default=0,
                    init=False,
                ),
            ],
        )
        with pytest.raises(TypeError):
            Cls(visible=1, internal=2)

    def test_init_false_field_writable(self) -> None:
        Cls = _make_type(
            "EdgeInitFalseWrite",
            [
                Field(name="visible", _ty_schema=TypeSchema("int"), default=MISSING),
                Field(
                    name="internal",
                    _ty_schema=TypeSchema("int"),
                    default=0,
                    init=False,
                ),
            ],
        )
        obj = Cls(visible=1)
        obj.internal = 99
        assert obj.internal == 99


# ###########################################################################
#  14. Inheritance (Python-defined parent)
# ###########################################################################
class TestFieldInheritance:
    """Low-level inheritance via _make_type: field offsets, parent-child layout."""

    def test_child_fields_after_parent(self) -> None:
        Parent = _make_type(
            "InhParent",
            [Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
        )
        Child = _make_type(
            "InhChild",
            [Field(name="y", _ty_schema=TypeSchema("int"), default=MISSING)],
            parent=Parent,
        )
        obj = Child(1, 2)
        assert obj.x == 1
        assert obj.y == 2

    def test_child_field_offsets_non_overlapping(self) -> None:
        Parent = _make_type(
            "InhParentOff",
            [Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
        )
        Child = _make_type(
            "InhChildOff",
            [Field(name="y", _ty_schema=TypeSchema("int"), default=MISSING)],
            parent=Parent,
        )
        p_info = getattr(Parent, "__tvm_ffi_type_info__")
        c_info = getattr(Child, "__tvm_ffi_type_info__")
        parent_end = p_info.fields[0].offset + p_info.fields[0].size
        assert c_info.fields[0].offset >= parent_end

    def test_mutation_no_aliasing(self) -> None:
        Parent = _make_type(
            "InhParentAlias",
            [Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
        )
        Child = _make_type(
            "InhChildAlias",
            [Field(name="y", _ty_schema=TypeSchema("int"), default=MISSING)],
            parent=Parent,
        )
        obj = Child(1, 2)
        obj.y = 9
        assert obj.x == 1
        assert obj.y == 9

    def test_three_level_inheritance(self) -> None:
        """Object → A → B → C: all fields accessible and non-overlapping."""
        A = _make_type(
            "InhA",
            [Field(name="a", _ty_schema=TypeSchema("int"), default=MISSING)],
        )
        B = _make_type(
            "InhB",
            [Field(name="b", _ty_schema=TypeSchema("str"), default=MISSING)],
            parent=A,
        )
        C = _make_type(
            "InhC",
            [Field(name="c", _ty_schema=TypeSchema("float"), default=MISSING)],
            parent=B,
        )
        obj = C(a=1, b="two", c=3.0)
        assert obj.a == 1
        assert obj.b == "two"
        assert obj.c == pytest.approx(3.0)

    def test_three_level_offsets_non_overlapping(self) -> None:
        A = _make_type(
            "InhAOff",
            [Field(name="a", _ty_schema=TypeSchema("int"), default=MISSING)],
        )
        B = _make_type(
            "InhBOff",
            [Field(name="b", _ty_schema=TypeSchema("int"), default=MISSING)],
            parent=A,
        )
        C = _make_type(
            "InhCOff",
            [Field(name="c", _ty_schema=TypeSchema("int"), default=MISSING)],
            parent=B,
        )
        a_info = getattr(A, "__tvm_ffi_type_info__")
        b_info = getattr(B, "__tvm_ffi_type_info__")
        c_info = getattr(C, "__tvm_ffi_type_info__")
        a_end = a_info.fields[0].offset + a_info.fields[0].size
        b_end = b_info.fields[0].offset + b_info.fields[0].size
        assert b_info.fields[0].offset >= a_end
        assert c_info.fields[0].offset >= b_end

    def test_three_level_mutation_no_aliasing(self) -> None:
        A = _make_type(
            "InhAMut",
            [Field(name="a", _ty_schema=TypeSchema("int"), default=MISSING)],
        )
        B = _make_type(
            "InhBMut",
            [Field(name="b", _ty_schema=TypeSchema("int"), default=MISSING)],
            parent=A,
        )
        C = _make_type(
            "InhCMut",
            [Field(name="c", _ty_schema=TypeSchema("int"), default=MISSING)],
            parent=B,
        )
        obj = C(a=1, b=2, c=3)
        obj.c = 99
        assert obj.a == 1
        assert obj.b == 2
        assert obj.c == 99
        obj.a = 77
        assert obj.a == 77
        assert obj.b == 2
        assert obj.c == 99

    def test_three_level_isinstance(self) -> None:
        A = _make_type(
            "InhAIs",
            [Field(name="a", _ty_schema=TypeSchema("int"), default=MISSING)],
        )
        B = _make_type(
            "InhBIs",
            [Field(name="b", _ty_schema=TypeSchema("int"), default=MISSING)],
            parent=A,
        )
        C = _make_type(
            "InhCIs",
            [Field(name="c", _ty_schema=TypeSchema("int"), default=MISSING)],
            parent=B,
        )
        obj = C(a=1, b=2, c=3)
        assert isinstance(obj, C)
        assert isinstance(obj, B)
        assert isinstance(obj, A)
        assert isinstance(obj, core.Object)

    def test_three_level_deep_copy(self) -> None:
        A = _make_type(
            "InhACopy",
            [Field(name="a", _ty_schema=TypeSchema("int"), default=MISSING)],
        )
        B = _make_type(
            "InhBCopy",
            [Field(name="b", _ty_schema=TypeSchema("int"), default=MISSING)],
            parent=A,
        )
        C = _make_type(
            "InhCCopy",
            [Field(name="c", _ty_schema=TypeSchema("int"), default=MISSING)],
            parent=B,
        )
        obj = C(a=1, b=2, c=3)
        obj_copy = DeepCopy(obj)
        assert not obj.same_as(obj_copy)
        assert obj_copy.a == 1
        assert obj_copy.b == 2
        assert obj_copy.c == 3
        obj_copy.c = 99
        assert obj.c == 3

    # -- Regression: empty intermediate parent must propagate total_size -----

    def test_empty_intermediate_parent_offsets(self) -> None:
        """GrandParent(x,y) -> Parent(no fields) -> Child(a,b): no overlap."""
        GP = _make_type(
            "OffGP",
            [
                Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING),
                Field(name="y", _ty_schema=TypeSchema("int"), default=MISSING),
            ],
        )
        P = _make_type("OffP", [], parent=GP)
        C = _make_type(
            "OffC",
            [
                Field(name="a", _ty_schema=TypeSchema("int"), default=MISSING),
                Field(name="b", _ty_schema=TypeSchema("int"), default=MISSING),
            ],
            parent=P,
        )
        obj = C(x=10, y=20, a=30, b=40)
        assert obj.x == 10
        assert obj.y == 20
        assert obj.a == 30
        assert obj.b == 40

    def test_empty_intermediate_parent_total_size(self) -> None:
        """Parent with no own fields inherits total_size from its parent."""
        GP = _make_type(
            "OffGP2",
            [
                Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING),
                Field(name="y", _ty_schema=TypeSchema("int"), default=MISSING),
            ],
        )
        P = _make_type("OffP2", [], parent=GP)
        gp_info = getattr(GP, "__tvm_ffi_type_info__")
        p_info = getattr(P, "__tvm_ffi_type_info__")
        assert p_info.total_size >= gp_info.total_size

    def test_empty_intermediate_child_offsets_non_overlapping(self) -> None:
        """Child field offsets must start at or after Parent.total_size."""
        GP = _make_type(
            "OffGP3",
            [
                Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING),
                Field(name="y", _ty_schema=TypeSchema("int"), default=MISSING),
            ],
        )
        P = _make_type("OffP3", [], parent=GP)
        C = _make_type(
            "OffC3",
            [Field(name="a", _ty_schema=TypeSchema("int"), default=MISSING)],
            parent=P,
        )
        p_info = getattr(P, "__tvm_ffi_type_info__")
        c_info = getattr(C, "__tvm_ffi_type_info__")
        assert c_info.fields[0].offset >= p_info.total_size

    def test_two_empty_intermediate_parents(self) -> None:
        """GP(x) -> Mid1() -> Mid2() -> Leaf(a): offsets propagated through two empty layers."""
        GP = _make_type(
            "OffGP4",
            [Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
        )
        Mid1 = _make_type("OffMid1", [], parent=GP)
        Mid2 = _make_type("OffMid2", [], parent=Mid1)
        Leaf = _make_type(
            "OffLeaf",
            [Field(name="a", _ty_schema=TypeSchema("int"), default=MISSING)],
            parent=Mid2,
        )
        obj = Leaf(x=1, a=2)
        assert obj.x == 1
        assert obj.a == 2
        gp_info = getattr(GP, "__tvm_ffi_type_info__")
        leaf_info = getattr(Leaf, "__tvm_ffi_type_info__")
        assert leaf_info.fields[0].offset >= gp_info.total_size

    def test_empty_intermediate_mutation_no_aliasing(self) -> None:
        """Mutating Child.a must not corrupt GrandParent.x through an empty parent."""
        GP = _make_type(
            "OffGP5",
            [
                Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING),
                Field(name="y", _ty_schema=TypeSchema("int"), default=MISSING),
            ],
        )
        P = _make_type("OffP5", [], parent=GP)
        C = _make_type(
            "OffC5",
            [
                Field(name="a", _ty_schema=TypeSchema("int"), default=MISSING),
                Field(name="b", _ty_schema=TypeSchema("int"), default=MISSING),
            ],
            parent=P,
        )
        obj = C(x=10, y=20, a=30, b=40)
        obj.a = 99
        assert obj.x == 10
        assert obj.y == 20
        obj.x = 77
        assert obj.a == 99
        assert obj.b == 40

    def test_empty_intermediate_py_class_decorator(self) -> None:
        """Same bug via @py_class (the high-level API)."""

        @py_class(_unique_key("OffDecGP"))
        class GP(Object):
            x: int = 0
            y: int = 0

        @py_class(_unique_key("OffDecP"))
        class P(GP):
            pass

        @py_class(_unique_key("OffDecC"))
        class C(P):
            a: int = 0
            b: int = 0

        obj = C(x=10, y=20, a=30, b=40)
        assert obj.x == 10
        assert obj.y == 20
        assert obj.a == 30
        assert obj.b == 40


# ###########################################################################
#  15. Mutual / Self References
# ###########################################################################
class TestMutualReferences:
    """Low-level mutual and self-referential type fields via two-phase registration."""

    def _register_bare(self, name: str) -> tuple[type, core.TypeInfo]:
        """Register a type with no fields (phase 1 of two-phase)."""
        parent_info = core._type_cls_to_type_info(core.Object)
        assert parent_info is not None
        cls = type(name, (core.Object,), {"__slots__": ()})
        info = core._register_py_class(parent_info, _unique_key_ff(name), cls)
        return cls, info

    def _finalize(self, cls: type, info: core.TypeInfo, fields: List[Field]) -> None:
        """Register fields and install class attrs (phase 2 of two-phase)."""
        info._register_fields(fields)
        setattr(cls, "__tvm_ffi_type_info__", info)
        _add_class_attrs(cls, info)
        _install_dataclass_dunders(
            cls,
            init=True,
            repr=True,
            eq=False,
            order=False,
            unsafe_hash=False,
        )

    def test_mutual_references(self) -> None:
        """Foo has Optional[Bar], Bar has Optional[Foo]."""
        Foo, foo_info = self._register_bare("MutFoo")
        Bar, bar_info = self._register_bare("MutBar")
        foo_schema = TypeSchema(foo_info.type_key, origin_type_index=foo_info.type_index)
        bar_schema = TypeSchema(bar_info.type_key, origin_type_index=bar_info.type_index)
        self._finalize(
            Foo,
            foo_info,
            [
                Field(name="a", _ty_schema=TypeSchema("str"), default=MISSING),
                Field(
                    name="bar",
                    _ty_schema=TypeSchema("Optional", (bar_schema,)),
                    default=None,
                ),
            ],
        )
        self._finalize(
            Bar,
            bar_info,
            [
                Field(
                    name="foo",
                    _ty_schema=TypeSchema("Optional", (foo_schema,)),
                    default=None,
                ),
            ],
        )
        foo = Foo(a="hello")
        bar = Bar()
        bar.foo = foo
        foo.bar = bar
        assert foo.bar.foo.a == "hello"

    def test_self_referential_field(self) -> None:
        """Bar has Optional[Bar] (self-reference)."""
        Bar, bar_info = self._register_bare("SelfRef")
        bar_schema = TypeSchema(bar_info.type_key, origin_type_index=bar_info.type_index)
        self._finalize(
            Bar,
            bar_info,
            [
                Field(name="val", _ty_schema=TypeSchema("int"), default=MISSING),
                Field(
                    name="next",
                    _ty_schema=TypeSchema("Optional", (bar_schema,)),
                    default=None,
                ),
            ],
        )
        a = Bar(val=1)
        b = Bar(val=2, next=a)
        assert b.next.val == 1
        assert a.next is None
        # Circular: a → b → a
        a.next = b
        assert a.next.next.val == 1

    def test_typed_mutual_ref_rejects_wrong_type(self) -> None:
        """Optional[Foo] field should reject Bar objects."""
        Foo, foo_info = self._register_bare("TypedFoo")
        Bar, bar_info = self._register_bare("TypedBar")
        foo_schema = TypeSchema(foo_info.type_key, origin_type_index=foo_info.type_index)
        self._finalize(
            Foo,
            foo_info,
            [
                Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING),
            ],
        )
        self._finalize(
            Bar,
            bar_info,
            [
                Field(
                    name="foo",
                    _ty_schema=TypeSchema("Optional", (foo_schema,)),
                    default=None,
                ),
            ],
        )
        bar = Bar()
        bar.foo = Foo(x=1)  # OK
        assert bar.foo.x == 1
        with pytest.raises((TypeError, RuntimeError)):
            bar.foo = bar  # Bar is not Foo


# ###########################################################################
#  16. Inheritance (native C++ parent)
# ###########################################################################
class TestNativeParentInheritance:
    """Low-level Python child of C++ TestObjectBase: offsets, fields, methods, copy."""

    def test_non_overlapping_offsets(self) -> None:
        parent_info = core._type_cls_to_type_info(_TestObjectBase)
        assert parent_info is not None
        Child = _make_type(
            "InhNativeChild",
            [Field(name="extra", _ty_schema=TypeSchema("int"), default=MISSING)],
            parent=_TestObjectBase,
        )
        child_info = getattr(Child, "__tvm_ffi_type_info__")
        parent_end = max(f.offset + f.size for f in parent_info.fields)
        assert child_info.fields[0].offset >= parent_end

    def test_preserves_parent_fields(self) -> None:
        Child = _make_type(
            "InhNativePreserve",
            [Field(name="extra", _ty_schema=TypeSchema("int"), default=MISSING)],
            parent=_TestObjectBase,
        )
        obj = Child(extra=7, v_i64=1, v_f64=2.0, v_str="x")
        assert obj.extra == 7
        assert obj.v_i64 == 1
        assert obj.v_f64 == 2.0
        assert obj.v_str == "x"

    def test_mutation_no_aliasing(self) -> None:
        Child = _make_type(
            "InhNativeMut",
            [Field(name="extra", _ty_schema=TypeSchema("int"), default=MISSING)],
            parent=_TestObjectBase,
        )
        obj = Child(extra=7, v_i64=1, v_f64=2.0, v_str="x")
        obj.extra = 33
        assert obj.extra == 33
        assert obj.v_i64 == 1
        assert obj.v_f64 == 2.0
        assert obj.v_str == "x"

    def test_parent_method_uses_parent_state(self) -> None:
        Child = _make_type(
            "InhNativeMethod",
            [Field(name="extra", _ty_schema=TypeSchema("int"), default=MISSING)],
            parent=_TestObjectBase,
        )
        obj = Child(extra=7, v_i64=1, v_f64=2.0, v_str="x")
        assert obj.add_i64(5) == 6

    def test_copy_preserves_all_fields(self) -> None:
        Child = _make_type(
            "InhNativeCopy",
            [Field(name="extra", _ty_schema=TypeSchema("int"), default=MISSING)],
            parent=_TestObjectBase,
        )
        obj = Child(extra=7, v_i64=1, v_f64=2.0, v_str="x")
        obj_copy = copy.copy(obj)
        assert obj_copy.extra == 7
        assert obj_copy.v_i64 == 1
        assert obj_copy.v_f64 == 2.0
        assert obj_copy.v_str == "x"

    def test_deepcopy_preserves_all_fields(self) -> None:
        Child = _make_type(
            "InhNativeDeepCopy",
            [Field(name="extra", _ty_schema=TypeSchema("int"), default=MISSING)],
            parent=_TestObjectBase,
        )
        obj = Child(extra=7, v_i64=1, v_f64=2.0, v_str="x")
        obj_copy = copy.deepcopy(obj)
        assert obj_copy.extra == 7
        assert obj_copy.v_i64 == 1
        assert obj_copy.v_f64 == 2.0
        assert obj_copy.v_str == "x"


# ###########################################################################
#  16. Deep Copy
# ###########################################################################
class TestDeepCopy:
    """Low-level DeepCopy via _make_type: nested ObjectRef, mutation independence."""

    def test_deep_copy_basic(self) -> None:
        Cls = _make_type(
            "DCBasic",
            [
                Field(
                    name="x",
                    _ty_schema=TypeSchema("int"),
                    default=MISSING,
                    compare=True,
                ),
                Field(
                    name="s",
                    _ty_schema=TypeSchema("str"),
                    default=MISSING,
                    compare=True,
                ),
            ],
            eq=True,
        )
        obj = Cls(x=42, s="hello")
        obj_copy = DeepCopy(obj)
        assert not obj.same_as(obj_copy)
        assert RecursiveEq(obj, obj_copy)

    def test_deep_copy_nested_objectref(self) -> None:
        Cls = _make_type(
            "DCNested",
            [
                Field(
                    name="items",
                    _ty_schema=TypeSchema("Array", (TypeSchema("int"),)),
                    default=MISSING,
                ),
            ],
        )
        obj = Cls(items=tvm_ffi.Array([1, 2, 3]))
        obj_copy = DeepCopy(obj)
        assert not obj.items.same_as(obj_copy.items)
        assert len(obj_copy.items) == 3

    def test_deep_copy_mutate_independent(self) -> None:
        Cls = _make_type(
            "DCMut",
            [Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
        )
        obj = Cls(x=1)
        obj_copy = DeepCopy(obj)
        obj_copy.x = 99
        assert obj.x == 1

    def test_python_deepcopy_dunder(self) -> None:
        Cls = _make_type(
            "DCPython",
            [Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
        )
        obj = Cls(x=42)
        obj_copy = copy.deepcopy(obj)
        assert obj_copy.x == 42
        assert not obj.same_as(obj_copy)


# ###########################################################################
#  17. Memory / Lifetime
# ###########################################################################
class TestMemoryLifetime:
    """Low-level ref-counting: ObjectRef/Any fields are properly ref-counted."""

    def test_objectref_field_kept_alive(self) -> None:
        Cls = _make_type(
            "MemAlive",
            [
                Field(
                    name="arr",
                    _ty_schema=TypeSchema("Array", (TypeSchema("int"),)),
                    default=MISSING,
                ),
            ],
        )
        arr = tvm_ffi.Array([1, 2, 3])
        obj = Cls(arr=arr)
        del arr
        gc.collect()
        assert len(obj.arr) == 3

    def test_multiple_objects_independent_lifetime(self) -> None:
        Cls = _make_type(
            "MemIndep",
            [
                Field(
                    name="arr",
                    _ty_schema=TypeSchema("Array", (TypeSchema("int"),)),
                    default=MISSING,
                ),
            ],
        )
        shared = tvm_ffi.Array([10, 20])
        a = Cls(arr=shared)
        b = Cls(arr=shared)
        del a
        gc.collect()
        assert len(b.arr) == 2
        assert b.arr[0] == 10

    def test_str_field_any_storage(self) -> None:
        Cls = _make_type(
            "MemStr",
            [Field(name="s", _ty_schema=TypeSchema("str"), default=MISSING)],
        )
        assert Cls(s="hi").s == "hi"
        long_str = "a" * 500
        assert Cls(s=long_str).s == long_str


# ###########################################################################
#  18. Bool Alignment
# ###########################################################################
class TestBoolAlignment:
    """Low-level bool field layout: 1-byte packing, padding, alternating layouts."""

    def test_bool_then_int_alignment(self) -> None:
        Cls = _make_type(
            "BoolAlign",
            [
                Field(name="flag", _ty_schema=TypeSchema("bool"), default=MISSING),
                Field(name="val", _ty_schema=TypeSchema("int"), default=MISSING),
            ],
        )
        info = getattr(Cls, "__tvm_ffi_type_info__")
        assert info.fields[0].offset == 24
        assert info.fields[1].offset % 8 == 0
        assert info.fields[1].offset >= info.fields[0].offset + 1

    def test_bool_then_int_values(self) -> None:
        Cls = _make_type(
            "BoolAlignVal",
            [
                Field(name="flag", _ty_schema=TypeSchema("bool"), default=MISSING),
                Field(name="val", _ty_schema=TypeSchema("int"), default=MISSING),
            ],
        )
        obj = Cls(flag=True, val=42)
        assert obj.flag is True
        assert obj.val == 42
        obj.flag = False
        assert obj.flag is False
        assert obj.val == 42

    def test_multiple_bools_packed(self) -> None:
        Cls = _make_type(
            "MultiBool",
            [
                Field(name="a", _ty_schema=TypeSchema("bool"), default=MISSING),
                Field(name="b", _ty_schema=TypeSchema("bool"), default=MISSING),
                Field(name="c", _ty_schema=TypeSchema("bool"), default=MISSING),
            ],
        )
        info = getattr(Cls, "__tvm_ffi_type_info__")
        assert [f.offset for f in info.fields] == [24, 25, 26]
        obj = Cls(a=True, b=False, c=True)
        assert obj.a is True
        assert obj.b is False
        assert obj.c is True

    def test_bool_int_bool_int_alternating(self) -> None:
        Cls = _make_type(
            "BoolIntBoolInt",
            [
                Field(name="b1", _ty_schema=TypeSchema("bool"), default=MISSING),
                Field(name="i1", _ty_schema=TypeSchema("int"), default=MISSING),
                Field(name="b2", _ty_schema=TypeSchema("bool"), default=MISSING),
                Field(name="i2", _ty_schema=TypeSchema("int"), default=MISSING),
            ],
        )
        obj = Cls(b1=True, i1=100, b2=False, i2=200)
        assert obj.b1 is True
        assert obj.i1 == 100
        assert obj.b2 is False
        assert obj.i2 == 200


# ###########################################################################
#  19. Type Conversion Errors
# ###########################################################################
class TestTypeConversionErrors:
    """Low-level type conversion: wrong-type setter/construction raises."""

    def test_set_int_field_to_str_raises(self) -> None:
        Cls = _make_type(
            "ErrIntStr",
            [Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
        )
        obj = Cls(x=1)
        with pytest.raises((TypeError, RuntimeError)):
            obj.x = "not_an_int"

    def test_set_str_field_to_int_raises(self) -> None:
        Cls = _make_type(
            "ErrStrInt",
            [Field(name="s", _ty_schema=TypeSchema("str"), default=MISSING)],
        )
        obj = Cls(s="hello")
        with pytest.raises((TypeError, RuntimeError)):
            obj.s = 42

    def test_construct_with_wrong_type_raises(self) -> None:
        Cls = _make_type(
            "ErrInit",
            [Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
        )
        with pytest.raises((TypeError, RuntimeError)):
            Cls(x="bad")

    def test_set_wrong_type_preserves_old_value(self) -> None:
        """Failed type-checked mutation preserves old value."""
        Cls = _make_type(
            "ErrPreserve",
            [Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
        )
        obj = Cls(x=42)
        with pytest.raises((TypeError, RuntimeError)):
            obj.x = "bad_value"
        assert obj.x == 42

    def test_type_schema_convert_raises_directly(self) -> None:
        """TypeSchema.convert raises TypeError for incompatible values."""
        ts = TypeSchema("int")
        assert _to_py_class_value(ts.convert(42)) == 42
        with pytest.raises(TypeError):
            ts.convert("not_an_int")

    def test_set_non_optional_object_field_to_none_raises(self) -> None:
        """A non-Optional Object field must reject None."""
        Cls = _make_type(
            "ErrObjNone",
            [
                Field(
                    name="child",
                    _ty_schema=TypeSchema("Object"),
                    default=MISSING,
                ),
            ],
        )
        obj = Cls(child=tvm_ffi.Array([1]))
        with pytest.raises((TypeError, RuntimeError)):
            obj.child = None

    def test_construct_non_optional_object_field_with_none_raises(self) -> None:
        """Constructing with None for a non-Optional Object field must fail."""
        Cls = _make_type(
            "ErrObjNoneInit",
            [
                Field(
                    name="child",
                    _ty_schema=TypeSchema("Object"),
                    default=MISSING,
                ),
            ],
        )
        with pytest.raises((TypeError, RuntimeError)):
            Cls(child=None)

    def test_optional_object_field_accepts_none(self) -> None:
        """An Optional[Object] field should accept None."""
        Cls = _make_type(
            "OptObjNone",
            [
                Field(
                    name="child",
                    _ty_schema=TypeSchema("Optional", (TypeSchema("Object"),)),
                    default=None,
                ),
            ],
        )
        obj = Cls()
        assert obj.child is None
        obj.child = tvm_ffi.Array([1, 2])
        assert len(obj.child) == 2
        obj.child = None
        assert obj.child is None

    def test_set_bool_field_to_str_raises(self) -> None:
        Cls = _make_type(
            "ErrBoolStr",
            [Field(name="b", _ty_schema=TypeSchema("bool"), default=MISSING)],
        )
        obj = Cls(b=True)
        with pytest.raises((TypeError, RuntimeError)):
            obj.b = "not_a_bool"

    def test_set_array_field_to_int_raises(self) -> None:
        Cls = _make_type(
            "ErrArrInt",
            [
                Field(
                    name="arr",
                    _ty_schema=TypeSchema("Array", (TypeSchema("int"),)),
                    default=MISSING,
                ),
            ],
        )
        obj = Cls(arr=[1])
        with pytest.raises((TypeError, RuntimeError)):
            obj.arr = 42

    def test_construct_multiple_wrong_types_first_caught(self) -> None:
        """When the first field has a wrong type, the error is caught."""
        Cls = _make_type(
            "ErrMulti",
            [
                Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING),
                Field(name="y", _ty_schema=TypeSchema("str"), default=MISSING),
            ],
        )
        with pytest.raises((TypeError, RuntimeError)):
            Cls(x="bad", y="ok")

    def test_set_optional_to_wrong_inner_type_raises(self) -> None:
        Cls = _make_type(
            "ErrOptWrong",
            [
                Field(
                    name="x",
                    _ty_schema=TypeSchema("Optional", (TypeSchema("int"),)),
                    default=None,
                ),
            ],
        )
        obj = Cls()
        with pytest.raises((TypeError, RuntimeError)):
            obj.x = "not_an_int"


# ###########################################################################
#  20. Setter / Getter Corner Cases
# ###########################################################################
class TestSetterGetterCornerCases:
    """Low-level setter/getter corner cases: conversions, nesting, edge values."""

    # --- Bool / int coercion ---

    def test_bool_field_accepts_true_false(self) -> None:
        Cls = _make_type(
            "SGBool",
            [Field(name="b", _ty_schema=TypeSchema("bool"), default=MISSING)],
        )
        obj = Cls(b=True)
        assert obj.b is True
        obj.b = False
        assert obj.b is False

    def test_int_field_accepts_bool(self) -> None:
        """Python bool is a subclass of int — FFI should accept it."""
        Cls = _make_type(
            "SGIntBool",
            [Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
        )
        obj = Cls(x=True)
        assert obj.x == 1
        obj.x = False
        assert obj.x == 0

    # --- Float edge values ---

    def test_float_field_inf_nan(self) -> None:
        Cls = _make_type(
            "SGFloatEdge",
            [Field(name="f", _ty_schema=TypeSchema("float"), default=MISSING)],
        )
        obj = Cls(f=float("inf"))
        assert math.isinf(obj.f)
        obj.f = float("-inf")
        assert math.isinf(obj.f) and obj.f < 0
        obj.f = float("nan")
        assert math.isnan(obj.f)

    def test_float_field_accepts_int(self) -> None:
        Cls = _make_type(
            "SGFloatInt",
            [Field(name="f", _ty_schema=TypeSchema("float"), default=MISSING)],
        )
        obj = Cls(f=42)
        assert obj.f == pytest.approx(42.0)

    # --- String edge values ---

    def test_str_field_unicode(self) -> None:
        Cls = _make_type(
            "SGStrUni",
            [Field(name="s", _ty_schema=TypeSchema("str"), default=MISSING)],
        )
        obj = Cls(s="日本語テスト 🎉")
        assert obj.s == "日本語テスト 🎉"

    def test_str_field_null_bytes(self) -> None:
        Cls = _make_type(
            "SGStrNull",
            [Field(name="s", _ty_schema=TypeSchema("str"), default=MISSING)],
        )
        s = "hello\x00world"
        obj = Cls(s=s)
        assert obj.s == s

    # --- Multiple mutations ---

    def test_repeated_mutation_same_field(self) -> None:
        Cls = _make_type(
            "SGRepeat",
            [Field(name="x", _ty_schema=TypeSchema("int"), default=MISSING)],
        )
        obj = Cls(x=0)
        for i in range(100):
            obj.x = i
        assert obj.x == 99

    def test_repeated_str_mutation(self) -> None:
        """Stress: repeated str assignment should not leak."""
        Cls = _make_type(
            "SGRepeatStr",
            [Field(name="s", _ty_schema=TypeSchema("str"), default=MISSING)],
        )
        obj = Cls(s="init")
        for i in range(100):
            obj.s = f"value_{i}"
        assert obj.s == "value_99"

    def test_repeated_objectref_mutation(self) -> None:
        """Stress: repeated ObjectRef assignment should properly DecRef old values."""
        Cls = _make_type(
            "SGRepeatArr",
            [
                Field(
                    name="arr",
                    _ty_schema=TypeSchema("Array", (TypeSchema("int"),)),
                    default=MISSING,
                ),
            ],
        )
        obj = Cls(arr=[0])
        for i in range(50):
            obj.arr = tvm_ffi.Array([i])
        assert obj.arr[0] == 49

    # --- Nested object fields ---

    def test_nested_two_levels(self) -> None:
        Inner = _make_type(
            "SGInner",
            [Field(name="val", _ty_schema=TypeSchema("int"), default=MISSING)],
        )
        inner_info = getattr(Inner, "__tvm_ffi_type_info__")
        inner_schema = TypeSchema(inner_info.type_key, origin_type_index=inner_info.type_index)
        Outer = _make_type(
            "SGOuter",
            [Field(name="child", _ty_schema=inner_schema, default=MISSING)],
        )
        obj = Outer(child=Inner(val=42))
        assert obj.child.val == 42
        # Mutate inner through outer
        new_inner = Inner(val=99)
        obj.child = new_inner
        assert obj.child.val == 99

    def test_self_referential_optional_field(self) -> None:
        """A type with an Optional[Self] field (stored as Any)."""
        Cls = _make_type(
            "SGSelfRef",
            [
                Field(name="val", _ty_schema=TypeSchema("int"), default=MISSING),
                Field(
                    name="next",
                    _ty_schema=TypeSchema("Optional", (TypeSchema("Object"),)),
                    default=None,
                ),
            ],
        )
        a = Cls(val=1)
        b = Cls(val=2, next=a)
        assert b.val == 2
        assert b.next.val == 1
        assert a.next is None

    # --- Default factory edge cases ---

    def test_default_factory_called_each_time(self) -> None:
        call_count = [0]

        def factory() -> int:
            call_count[0] += 1
            return call_count[0]

        Cls = _make_type(
            "SGFactoryCount",
            [Field(name="x", _ty_schema=TypeSchema("int"), default_factory=factory)],
        )
        a = Cls()
        b = Cls()
        c = Cls()
        assert a.x == 1
        assert b.x == 2
        assert c.x == 3

    # --- Mixed types in one class ---

    def test_all_pod_plus_objectref_plus_optional(self) -> None:
        Cls = _make_type(
            "SGKitchenSink",
            [
                Field(name="i", _ty_schema=TypeSchema("int"), default=MISSING),
                Field(name="f", _ty_schema=TypeSchema("float"), default=MISSING),
                Field(name="b", _ty_schema=TypeSchema("bool"), default=MISSING),
                Field(name="s", _ty_schema=TypeSchema("str"), default=MISSING),
                Field(
                    name="arr",
                    _ty_schema=TypeSchema("Array", (TypeSchema("int"),)),
                    default=MISSING,
                ),
                Field(
                    name="opt",
                    _ty_schema=TypeSchema("Optional", (TypeSchema("int"),)),
                    default=None,
                ),
            ],
        )
        obj = Cls(i=1, f=2.0, b=True, s="hi", arr=[10, 20])
        assert obj.i == 1
        assert obj.f == pytest.approx(2.0)
        assert obj.b is True
        assert obj.s == "hi"
        assert len(obj.arr) == 2
        assert obj.opt is None
        # Mutate all fields
        obj.i = -1
        obj.f = -2.0
        obj.b = False
        obj.s = "bye"
        obj.arr = tvm_ffi.Array([30])
        obj.opt = 42
        assert obj.i == -1
        assert obj.f == pytest.approx(-2.0)
        assert obj.b is False
        assert obj.s == "bye"
        assert len(obj.arr) == 1
        assert obj.opt == 42


# ###########################################################################
#  21. FFI Global Function Existence
# ###########################################################################
class TestFFIGlobalFunctions:
    """Low-level FFI global function registration checks."""

    def test_py_class_register_type_attr_columns_exists(self) -> None:
        assert (
            tvm_ffi.get_global_func("ffi._PyClassRegisterTypeAttrColumns", allow_missing=True)
            is not None
        )

    def test_get_field_getter_exists(self) -> None:
        assert tvm_ffi.get_global_func("ffi.MakeFieldGetter", allow_missing=True) is not None

    def test_make_field_setter_exists(self) -> None:
        assert tvm_ffi.get_global_func("ffi.MakeFieldSetter", allow_missing=True) is not None

    def test_make_new_removed(self) -> None:
        assert tvm_ffi.get_global_func("ffi.MakeNew", allow_missing=True) is None


# ###########################################################################
#  22. Container field annotations
# ###########################################################################
class TestContainerFieldAnnotations:
    """Container field annotations: List[T], Dict[K,V], nested."""

    def test_list_int_field(self) -> None:
        @py_class(_unique_key("ListInt"))
        class ListInt(Object):
            items: List[int]

        obj = ListInt(items=[1, 2, 3])
        assert len(obj.items) == 3
        assert obj.items[0] == 1
        assert obj.items[2] == 3

    def test_list_int_from_tuple(self) -> None:
        @py_class(_unique_key("ListIntTup"))
        class ListIntTup(Object):
            items: List[int]

        obj = ListIntTup(items=(10, 20, 30))  # ty:ignore[invalid-argument-type]
        assert len(obj.items) == 3
        assert obj.items[1] == 20

    def test_dict_str_int_field(self) -> None:
        @py_class(_unique_key("DictStrInt"))
        class DictStrInt(Object):
            mapping: Dict[str, int]

        obj = DictStrInt(mapping={"a": 1, "b": 2})
        assert len(obj.mapping) == 2
        assert obj.mapping["a"] == 1
        assert obj.mapping["b"] == 2

    def test_list_list_int_field(self) -> None:
        @py_class(_unique_key("ListListInt"))
        class ListListInt(Object):
            matrix: List[List[int]]

        obj = ListListInt(matrix=[[1, 2, 3], [4, 5, 6]])
        assert len(obj.matrix) == 2
        assert len(obj.matrix[0]) == 3
        assert obj.matrix[0][0] == 1
        assert obj.matrix[1][2] == 6

    def test_dict_str_list_int_field(self) -> None:
        @py_class(_unique_key("DictStrListInt"))
        class DictStrListInt(Object):
            data: Dict[str, List[int]]

        obj = DictStrListInt(data={"x": [1, 2, 3], "y": [4, 5, 6]})
        assert len(obj.data) == 2
        assert tuple(obj.data["x"]) == (1, 2, 3)
        assert tuple(obj.data["y"]) == (4, 5, 6)

    def test_container_field_set(self) -> None:
        @py_class(_unique_key("ContSet"))
        class ContSet(Object):
            items: List[int]

        obj = ContSet(items=[1, 2])
        assert tuple(obj.items) == (1, 2)
        obj.items = [3, 4, 5]
        assert len(obj.items) == 3
        assert obj.items[0] == 3

    def test_dict_field_set(self) -> None:
        @py_class(_unique_key("DictSet"))
        class DictSet(Object):
            mapping: Dict[str, int]

        obj = DictSet(mapping={"a": 1})
        obj.mapping = {"b": 2, "c": 3}
        assert len(obj.mapping) == 2
        assert obj.mapping["b"] == 2

    def test_container_shared_reference(self) -> None:
        @py_class(_unique_key("ContShare"))
        class ContShare(Object):
            a: List[int]
            b: List[int]

        obj = ContShare(a=[1, 2], b=[1, 2])
        assert tuple(obj.a) == tuple(obj.b)

    def test_untyped_list_field(self) -> None:
        @py_class(_unique_key("UList"))
        class UList(Object):
            items: list

        obj = UList(items=[1, "two", 3.0])
        assert len(obj.items) == 3
        assert obj.items[0] == 1
        assert obj.items[1] == "two"

    def test_untyped_dict_field(self) -> None:
        @py_class(_unique_key("UDict"))
        class UDict(Object):
            data: dict

        obj = UDict(data={"a": 1, "b": "two"})
        assert len(obj.data) == 2


# ###########################################################################
#  23. Optional container fields
# ###########################################################################
class TestOptionalContainerFields:
    """Optional[List[T]], Optional[Dict[K,V]] via @py_class."""

    @requires_py310
    def test_optional_list_int(self) -> None:
        @py_class(_unique_key("OptListInt"))
        class OptListInt(Object):
            items: Optional[List[int]]

        obj = OptListInt(items=[1, 2, 3])
        assert len(obj.items) == 3  # ty:ignore[invalid-argument-type]
        obj.items = None
        assert obj.items is None
        obj.items = [4, 5]
        assert len(obj.items) == 2

    @requires_py310
    def test_optional_dict_str_int(self) -> None:
        @py_class(_unique_key("OptDictStrInt"))
        class OptDictStrInt(Object):
            data: Optional[Dict[str, int]]

        obj = OptDictStrInt(data={"a": 1})
        assert obj.data["a"] == 1  # ty:ignore[not-subscriptable]
        obj.data = None
        assert obj.data is None
        obj.data = {"b": 2}
        assert obj.data["b"] == 2

    @requires_py310
    def test_optional_list_list_int(self) -> None:
        @py_class(_unique_key("OptLLI"))
        class OptLLI(Object):
            matrix: Optional[List[List[int]]]

        obj = OptLLI(matrix=[[1, 2], [3, 4]])
        assert obj.matrix[0][0] == 1  # ty:ignore[not-subscriptable]
        obj.matrix = None
        assert obj.matrix is None

    @requires_py310
    def test_optional_dict_str_list_int(self) -> None:
        @py_class(_unique_key("OptDSLI"))
        class OptDSLI(Object):
            data: Optional[Dict[str, List[int]]]

        obj = OptDSLI(data={"x": [1, 2, 3]})
        assert tuple(obj.data["x"]) == (1, 2, 3)  # ty:ignore[not-subscriptable]
        obj.data = None
        assert obj.data is None

    def test_optional_list_with_typing_optional(self) -> None:
        @py_class(_unique_key("OptListTyping"))
        class OptListTyping(Object):
            items: Optional[List[int]]

        obj = OptListTyping(items=[1, 2, 3])
        assert len(obj.items) == 3  # ty:ignore[invalid-argument-type]
        obj.items = None
        assert obj.items is None

    def test_optional_dict_with_typing_optional(self) -> None:
        @py_class(_unique_key("OptDictTyping"))
        class OptDictTyping(Object):
            data: Optional[Dict[str, int]]

        obj = OptDictTyping(data={"a": 1})
        assert obj.data["a"] == 1  # ty:ignore[not-subscriptable]
        obj.data = None
        assert obj.data is None


# ###########################################################################
#  24. Callable / Function fields
# ###########################################################################
class TestFunctionField:
    """Function/Callable field via @py_class decorator."""

    def test_function_field(self) -> None:
        @py_class(_unique_key("FuncFld"))
        class FuncFld(Object):
            func: tvm_ffi.Function

        fn = tvm_ffi.convert(lambda x: x + 1)
        obj = FuncFld(func=fn)
        assert obj.func(1) == 2

    def test_function_field_set(self) -> None:
        @py_class(_unique_key("FuncSet"))
        class FuncSet(Object):
            func: tvm_ffi.Function

        fn1 = tvm_ffi.convert(lambda x: x + 1)
        fn2 = tvm_ffi.convert(lambda x: x + 2)
        obj = FuncSet(func=fn1)
        assert obj.func(1) == 2
        obj.func = fn2
        assert obj.func(1) == 3

    @requires_py310
    def test_optional_function_field(self) -> None:
        @py_class(_unique_key("OptFunc"))
        class OptFunc(Object):
            func: Optional[tvm_ffi.Function]

        obj = OptFunc(func=None)
        assert obj.func is None
        obj.func = tvm_ffi.convert(lambda x: x * 2)
        assert obj.func(3) == 6
        obj.func = None
        assert obj.func is None


# ###########################################################################
#  25. Any-typed fields (decorator level)
# ###########################################################################
class TestAnyFieldDecorator:
    """Any-typed field via @py_class decorator."""

    def test_any_holds_int(self) -> None:
        @py_class(_unique_key("AnyI"))
        class AnyI(Object):
            val: Any

        assert AnyI(val=42).val == 42

    def test_any_holds_str(self) -> None:
        @py_class(_unique_key("AnyS"))
        class AnyS(Object):
            val: Any

        assert AnyS(val="hello").val == "hello"

    def test_any_holds_none(self) -> None:
        @py_class(_unique_key("AnyN"))
        class AnyN(Object):
            val: Any = None

        assert AnyN().val is None

    def test_any_holds_list(self) -> None:
        @py_class(_unique_key("AnyL"))
        class AnyL(Object):
            val: Any

        assert len(AnyL(val=[1, 2, 3]).val) == 3

    def test_any_type_change(self) -> None:
        @py_class(_unique_key("AnyChg"))
        class AnyChg(Object):
            val: Any = None

        obj = AnyChg()
        assert obj.val is None
        obj.val = 42
        assert obj.val == 42
        obj.val = "hello"
        assert obj.val == "hello"
        obj.val = tvm_ffi.Array([1, 2])
        assert len(obj.val) == 2
        obj.val = None
        assert obj.val is None


# ###########################################################################
#  26. Post-init field mutation
# ###########################################################################
class TestPostInitMutation:
    """__post_init__ that mutates field values."""

    def test_post_init_mutates_str(self) -> None:
        @py_class(_unique_key("PostMut"))
        class PostMut(Object):
            a: int
            b: str

            def __post_init__(self) -> None:
                self.b = self.b.upper()

        obj = PostMut(a=1, b="hello")
        assert obj.a == 1
        assert obj.b == "HELLO"

    def test_post_init_computes_derived(self) -> None:
        @py_class(_unique_key("PostDeriv"))
        class PostDeriv(Object):
            x: int
            doubled: int = 0

            def __post_init__(self) -> None:
                self.doubled = self.x * 2

        assert PostDeriv(x=5).doubled == 10


# ###########################################################################
#  27. Custom __init__ with init=False
# ###########################################################################
class TestCustomInitFalse:
    """Custom __init__ with init=False and reordered parameters."""

    def test_custom_init_reordered_params(self) -> None:
        @py_class(_unique_key("CustomOrd"), init=False)
        class CustomOrd(Object):
            a: int
            b: float
            c: str
            d: bool

            def __init__(self, b: float, c: str, a: int, d: bool) -> None:
                self.a = a
                self.b = b
                self.c = c
                self.d = d

        obj = CustomOrd(b=2.0, c="3", a=1, d=True)
        assert obj.a == 1
        assert obj.b == 2.0
        assert obj.c == "3"
        assert obj.d is True

    def test_custom_init_keyword_only(self) -> None:
        @py_class(_unique_key("CustomKW"), init=False)
        class CustomKW(Object):
            a: int
            b: str

            def __init__(self, *, b: str, a: int) -> None:
                self.a = a
                self.b = b

        obj = CustomKW(a=1, b="hello")
        assert obj.a == 1
        assert obj.b == "hello"


# ###########################################################################
#  28. Inheritance with defaults and containers
# ###########################################################################
class TestInheritanceWithDefaults:
    """Inheritance with default values and container fields."""

    def test_base_with_default_factory(self) -> None:
        @py_class(_unique_key("BaseDef"))
        class BaseDef(Object):
            a: int
            b: List[int] = field(default_factory=list)

        obj = BaseDef(a=42)
        assert obj.a == 42
        assert len(obj.b) == 0

    def test_derived_adds_optional_fields(self) -> None:
        @py_class(_unique_key("BaseD"))
        class BaseD(Object):
            a: int
            b: List[int] = field(default_factory=list)

        @py_class(_unique_key("DerivedD"))
        class DerivedD(BaseD):
            c: Optional[int] = None
            d: Optional[str] = "default"

        obj = DerivedD(a=12)
        assert obj.a == 12
        assert len(obj.b) == 0
        assert obj.c is None
        assert obj.d == "default"

    def test_derived_interleaved_required_optional(self) -> None:
        @py_class(_unique_key("BaseIL"))
        class BaseIL(Object):
            a: int
            b: List[int] = field(default_factory=list)

        @py_class(_unique_key("DerivedIL"))
        class DerivedIL(BaseIL):
            c: int
            d: Optional[str] = "default"

        obj = DerivedIL(a=1, c=2)
        assert obj.a == 1
        assert len(obj.b) == 0
        assert obj.c == 2
        assert obj.d == "default"

    def test_three_level_with_defaults(self) -> None:
        @py_class(_unique_key("L1D"))
        class L1D(Object):
            a: int

        @py_class(_unique_key("L2D"))
        class L2D(L1D):
            b: Optional[int] = None
            c: Optional[str] = "hello"

        @py_class(_unique_key("L3D"))
        class L3D(L2D):
            d: str

        obj = L3D(a=1, d="world")
        assert obj.a == 1
        assert obj.b is None
        assert obj.c == "hello"
        assert obj.d == "world"

    def test_derived_with_container_init(self) -> None:
        @py_class(_unique_key("BaseC"))
        class BaseC(Object):
            items: List[int]

        @py_class(_unique_key("DerivedC"))
        class DerivedC(BaseC):
            name: str

        obj = DerivedC(items=[1, 2, 3], name="test")
        assert len(obj.items) == 3
        assert obj.name == "test"


# ###########################################################################
#  29. Decorator-level type validation
# ###########################################################################
class TestFieldTypeValidation:
    """Type validation on set for @py_class fields."""

    def test_set_int_to_str_raises(self) -> None:
        @py_class(_unique_key("ValInt"))
        class ValInt(Object):
            x: int

        obj = ValInt(x=1)
        with pytest.raises((TypeError, RuntimeError)):
            obj.x = "not_an_int"  # ty:ignore[invalid-assignment]

    def test_set_str_to_int_raises(self) -> None:
        @py_class(_unique_key("ValStr"))
        class ValStr(Object):
            x: str

        obj = ValStr(x="hello")
        with pytest.raises((TypeError, RuntimeError)):
            obj.x = 42  # ty:ignore[invalid-assignment]

    def test_set_bool_to_str_raises(self) -> None:
        @py_class(_unique_key("ValBool"))
        class ValBool(Object):
            x: bool

        obj = ValBool(x=True)
        with pytest.raises((TypeError, RuntimeError)):
            obj.x = "not_a_bool"  # ty:ignore[invalid-assignment]

    def test_set_list_to_wrong_type_raises(self) -> None:
        @py_class(_unique_key("ValList"))
        class ValList(Object):
            items: List[int]

        obj = ValList(items=[1, 2])
        with pytest.raises((TypeError, RuntimeError)):
            obj.items = "not_a_list"  # ty:ignore[invalid-assignment]

    def test_set_dict_to_wrong_type_raises(self) -> None:
        @py_class(_unique_key("ValDict"))
        class ValDict(Object):
            data: Dict[str, int]

        obj = ValDict(data={"a": 1})
        with pytest.raises((TypeError, RuntimeError)):
            obj.data = "not_a_dict"  # ty:ignore[invalid-assignment]


# ###########################################################################
#  30. Three-level inheritance with containers
# ###########################################################################
class TestDerivedDerivedContainers:
    """Three-level inheritance with container fields and init reordering."""

    def test_three_level_with_container_and_defaults(self) -> None:
        @py_class(_unique_key("DD_L1"))
        class L1(Object):
            a: int
            b: List[int] = field(default_factory=list)

        @py_class(_unique_key("DD_L2"))
        class L2(L1):
            c: Optional[int] = None
            d: Optional[str] = "hello"

        @py_class(_unique_key("DD_L3"))
        class L3(L2):
            e: str

        obj = L3(a=1, e="world", b=[1, 2])
        assert obj.a == 1
        assert tuple(obj.b) == (1, 2)
        assert obj.c is None
        assert obj.d == "hello"
        assert obj.e == "world"

    def test_three_level_positional_call(self) -> None:
        @py_class(_unique_key("DD2_L1"))
        class L1(Object):
            a: int

        @py_class(_unique_key("DD2_L2"))
        class L2(L1):
            b: List[int] = field(default_factory=list)

        @py_class(_unique_key("DD2_L3"))
        class L3(L2):
            c: str

        obj = L3(a=1, c="x")
        assert obj.a == 1
        assert len(obj.b) == 0
        assert obj.c == "x"


# ###########################################################################
#  31. Container field mutation and type rejection
# ###########################################################################
class TestContainerFieldMutation:
    """Container field set, mutation, and type rejection."""

    def test_untyped_list_mutation(self) -> None:
        obj = _make_multi_type_obj()
        assert len(obj.list_any) == 3
        assert obj.list_any[0] == 1
        obj.list_any = [4, 3.0, "two"]
        assert len(obj.list_any) == 3
        assert obj.list_any[0] == 4
        assert obj.list_any[2] == "two"

    def test_untyped_dict_mutation(self) -> None:
        obj = _make_multi_type_obj()
        assert len(obj.dict_any) == 2
        obj.dict_any = {"4": 4, "3": "two", "2": 3.0}
        assert len(obj.dict_any) == 3
        assert obj.dict_any["4"] == 4
        assert obj.dict_any["3"] == "two"

    def test_list_any_type_rejection(self) -> None:
        @py_class(_unique_key("LAReject"))
        class LAReject(Object):
            items: List[Any]

        obj = LAReject(items=[1, 2])
        with pytest.raises((TypeError, RuntimeError)):
            obj.items = "wrong"  # ty:ignore[invalid-assignment]

    def test_list_list_int_type_rejection(self) -> None:
        @py_class(_unique_key("LLReject"))
        class LLReject(Object):
            matrix: List[List[int]]

        obj = LLReject(matrix=[[1, 2]])
        with pytest.raises((TypeError, RuntimeError)):
            obj.matrix = [4, 3, 2, 1]  # ty:ignore[invalid-assignment]
        with pytest.raises((TypeError, RuntimeError)):
            obj.matrix = None  # ty:ignore[invalid-assignment]
        assert len(obj.matrix) == 1

    def test_dict_any_any_type_rejection(self) -> None:
        @py_class(_unique_key("DAAReject"))
        class DAAReject(Object):
            data: Dict[Any, Any]

        obj = DAAReject(data={1: 2})
        with pytest.raises((TypeError, RuntimeError)):
            obj.data = "wrong"  # ty:ignore[invalid-assignment]
        with pytest.raises((TypeError, RuntimeError)):
            obj.data = 42  # ty:ignore[invalid-assignment]
        with pytest.raises((TypeError, RuntimeError)):
            obj.data = None  # ty:ignore[invalid-assignment]

    def test_dict_str_any_type_rejection(self) -> None:
        @py_class(_unique_key("DSAReject"))
        class DSAReject(Object):
            data: Dict[str, Any]

        obj = DSAReject(data={"a": 1})
        with pytest.raises((TypeError, RuntimeError)):
            obj.data = "wrong"  # ty:ignore[invalid-assignment]
        with pytest.raises((TypeError, RuntimeError)):
            obj.data = 42  # ty:ignore[invalid-assignment]
        with pytest.raises((TypeError, RuntimeError)):
            obj.data = None  # ty:ignore[invalid-assignment]
        with pytest.raises((TypeError, RuntimeError)):
            obj.data = {4: 4, 3.0: 3}  # ty:ignore[invalid-assignment]

    def test_dict_str_list_int_type_rejection(self) -> None:
        @py_class(_unique_key("DSLReject"))
        class DSLReject(Object):
            data: Dict[str, List[int]]

        obj = DSLReject(data={"a": [1, 2]})
        with pytest.raises((TypeError, RuntimeError)):
            obj.data = "wrong"  # ty:ignore[invalid-assignment]
        with pytest.raises((TypeError, RuntimeError)):
            obj.data = 42  # ty:ignore[invalid-assignment]
        with pytest.raises((TypeError, RuntimeError)):
            obj.data = None  # ty:ignore[invalid-assignment]
        with pytest.raises((TypeError, RuntimeError)):
            obj.data = {"a": 1, "b": [2]}  # ty:ignore[invalid-assignment]


# ###########################################################################
#  32. Optional field set/unset cycles with type rejection
# ###########################################################################
class TestOptionalFieldCycles:
    """Optional field set → None → set-back cycles with type rejection."""

    def test_opt_func_type_rejection(self) -> None:
        @py_class(_unique_key("OptFuncR"))
        class OptFuncR(Object):
            func: Optional[tvm_ffi.Function]

        obj = OptFuncR(func=None)
        assert obj.func is None
        obj.func = tvm_ffi.convert(lambda x: x + 2)
        assert obj.func(1) == 3
        obj.func = None
        assert obj.func is None
        with pytest.raises((TypeError, RuntimeError)):
            obj.func = "wrong"  # ty:ignore[invalid-assignment]
        with pytest.raises((TypeError, RuntimeError)):
            obj.func = 42  # ty:ignore[invalid-assignment]

    def test_opt_ulist_cycle(self) -> None:
        @py_class(_unique_key("OptUListC"))
        class OptUListC(Object):
            items: Optional[list]

        obj = OptUListC(items=None)
        assert obj.items is None
        obj.items = [4, 3.0, "two"]
        assert len(obj.items) == 3
        assert obj.items[0] == 4
        with pytest.raises((TypeError, RuntimeError)):
            obj.items = "wrong"  # ty:ignore[invalid-assignment]
        with pytest.raises((TypeError, RuntimeError)):
            obj.items = 42  # ty:ignore[invalid-assignment]
        obj.items = None
        assert obj.items is None

    def test_opt_udict_cycle(self) -> None:
        @py_class(_unique_key("OptUDictC"))
        class OptUDictC(Object):
            data: Optional[dict]

        obj = OptUDictC(data=None)
        assert obj.data is None
        obj.data = {"4": 4, "3": "two"}
        assert len(obj.data) == 2
        with pytest.raises((TypeError, RuntimeError)):
            obj.data = "wrong"  # ty:ignore[invalid-assignment]
        with pytest.raises((TypeError, RuntimeError)):
            obj.data = 42  # ty:ignore[invalid-assignment]
        obj.data = None
        assert obj.data is None

    def test_opt_list_any_cycle(self) -> None:
        @py_class(_unique_key("OptLAC"))
        class OptLAC(Object):
            items: Optional[List[Any]]

        obj = OptLAC(items=[1, 2.0, "three"])
        assert len(obj.items) == 3  # ty:ignore[invalid-argument-type]
        obj.items = [4, 3.0, "two"]
        assert obj.items[0] == 4
        with pytest.raises((TypeError, RuntimeError)):
            obj.items = "wrong"  # ty:ignore[invalid-assignment]
        with pytest.raises((TypeError, RuntimeError)):
            obj.items = 42  # ty:ignore[invalid-assignment]
        obj.items = None
        assert obj.items is None

    def test_opt_list_list_int_cycle(self) -> None:
        @py_class(_unique_key("OptLLIC"))
        class OptLLIC(Object):
            matrix: Optional[List[List[int]]]

        obj = OptLLIC(matrix=[[1, 2, 3], [4, 5, 6]])
        assert tuple(obj.matrix[0]) == (1, 2, 3)  # ty:ignore[not-subscriptable]
        obj.matrix = [[4, 3, 2]]
        assert len(obj.matrix) == 1
        with pytest.raises((TypeError, RuntimeError)):
            obj.matrix = "wrong"  # ty:ignore[invalid-assignment]
        with pytest.raises((TypeError, RuntimeError)):
            obj.matrix = [1, 2, 3]  # ty:ignore[invalid-assignment]
        obj.matrix = None
        assert obj.matrix is None

    def test_opt_dict_any_any_cycle(self) -> None:
        @py_class(_unique_key("OptDAAC"))
        class OptDAAC(Object):
            data: Optional[Dict[Any, Any]]

        obj = OptDAAC(data=None)
        assert obj.data is None
        obj.data = {4: 4, "three": "two"}
        assert len(obj.data) == 2
        with pytest.raises((TypeError, RuntimeError)):
            obj.data = "wrong"  # ty:ignore[invalid-assignment]
        with pytest.raises((TypeError, RuntimeError)):
            obj.data = 42  # ty:ignore[invalid-assignment]
        obj.data = None
        assert obj.data is None

    def test_opt_dict_str_any_cycle(self) -> None:
        @py_class(_unique_key("OptDSAC"))
        class OptDSAC(Object):
            data: Optional[Dict[str, Any]]

        obj = OptDSAC(data={"a": 1})
        assert obj.data["a"] == 1  # ty:ignore[not-subscriptable]
        obj.data = {}
        assert len(obj.data) == 0
        with pytest.raises((TypeError, RuntimeError)):
            obj.data = "wrong"  # ty:ignore[invalid-assignment]
        with pytest.raises((TypeError, RuntimeError)):
            obj.data = 42  # ty:ignore[invalid-assignment]
        obj.data = None
        assert obj.data is None

    def test_opt_dict_any_str_cycle(self) -> None:
        @py_class(_unique_key("OptDASC"))
        class OptDASC(Object):
            data: Optional[Dict[Any, str]]

        obj = OptDASC(data={1: "a", "two": "b"})
        assert obj.data[1] == "a"  # ty:ignore[not-subscriptable]
        obj.data = {4: "4", "three": "two"}
        assert len(obj.data) == 2
        with pytest.raises((TypeError, RuntimeError)):
            obj.data = "wrong"  # ty:ignore[invalid-assignment]
        with pytest.raises((TypeError, RuntimeError)):
            obj.data = 42  # ty:ignore[invalid-assignment]
        obj.data = None
        assert obj.data is None

    def test_opt_dict_str_list_int_cycle(self) -> None:
        @py_class(_unique_key("OptDSLIC"))
        class OptDSLIC(Object):
            data: Optional[Dict[str, List[int]]]

        obj = OptDSLIC(data={"1": [1, 2, 3], "2": [4, 5, 6]})
        assert tuple(obj.data["1"]) == (1, 2, 3)  # ty:ignore[not-subscriptable]
        obj.data = {"a": [7, 8]}
        assert tuple(obj.data["a"]) == (7, 8)
        with pytest.raises((TypeError, RuntimeError)):
            obj.data = "wrong"  # ty:ignore[invalid-assignment]
        with pytest.raises((TypeError, RuntimeError)):
            obj.data = 42  # ty:ignore[invalid-assignment]
        obj.data = None
        assert obj.data is None


# ###########################################################################
#  33. Multi-type field class
# ###########################################################################
@py_class(_unique_key("MultiType"))
class _PyClassMultiType(Object):
    """@py_class with many field types for cross-cutting field tests."""

    bool_: bool
    i64: int
    f64: float
    str_: str
    any_val: Any
    list_int: List[int]
    list_any: list
    dict_str_int: Dict[str, int]
    dict_any: dict
    list_list_int: List[List[int]]
    dict_str_list_int: Dict[str, List[int]]
    opt_bool: Optional[bool]
    opt_int: Optional[int]
    opt_float: Optional[float]
    opt_str: Optional[str]
    opt_list_int: Optional[List[int]]
    opt_dict_str_int: Optional[Dict[str, int]]


def _make_multi_type_obj() -> _PyClassMultiType:
    return _PyClassMultiType(
        bool_=False,
        i64=64,
        f64=2.5,
        str_="world",
        any_val="hello",
        list_int=[1, 2, 3],
        list_any=[1, "two", 3.0],
        dict_str_int={"a": 1, "b": 2},
        dict_any={"x": 1, "y": "two"},
        list_list_int=[[1, 2, 3], [4, 5, 6]],
        dict_str_list_int={"p": [1, 2], "q": [3, 4]},
        opt_bool=True,
        opt_int=-64,
        opt_float=None,
        opt_str=None,
        opt_list_int=[10, 20],
        opt_dict_str_int=None,
    )


class TestMultiTypeFieldOps:
    """Per-field get/set/validation on a many-field @py_class type."""

    def test_bool_field(self) -> None:
        obj = _make_multi_type_obj()
        assert obj.bool_ is False
        obj.bool_ = True
        assert obj.bool_ is True
        with pytest.raises((TypeError, RuntimeError)):
            obj.bool_ = "not_a_bool"  # ty:ignore[invalid-assignment]

    def test_int_field(self) -> None:
        obj = _make_multi_type_obj()
        assert obj.i64 == 64
        obj.i64 = -128
        assert obj.i64 == -128
        with pytest.raises((TypeError, RuntimeError)):
            obj.i64 = "wrong"  # ty:ignore[invalid-assignment]

    def test_float_field(self) -> None:
        obj = _make_multi_type_obj()
        assert abs(obj.f64 - 2.5) < 1e-10
        obj.f64 = 5.0
        assert abs(obj.f64 - 5.0) < 1e-10
        with pytest.raises((TypeError, RuntimeError)):
            obj.f64 = "wrong"  # ty:ignore[invalid-assignment]

    def test_str_field(self) -> None:
        obj = _make_multi_type_obj()
        assert obj.str_ == "world"
        obj.str_ = "hello"
        assert obj.str_ == "hello"

    def test_any_field(self) -> None:
        obj = _make_multi_type_obj()
        assert obj.any_val == "hello"
        obj.any_val = 42
        assert obj.any_val == 42
        obj.any_val = [1, 2]
        assert len(obj.any_val) == 2

    def test_list_int_field(self) -> None:
        obj = _make_multi_type_obj()
        assert tuple(obj.list_int) == (1, 2, 3)
        obj.list_int = [4, 5]
        assert len(obj.list_int) == 2
        with pytest.raises((TypeError, RuntimeError)):
            obj.list_int = "wrong"  # ty:ignore[invalid-assignment]

    def test_untyped_list_field(self) -> None:
        obj = _make_multi_type_obj()
        assert len(obj.list_any) == 3
        assert obj.list_any[0] == 1
        assert obj.list_any[1] == "two"
        obj.list_any = [4, 3.0, "new"]
        assert len(obj.list_any) == 3

    def test_dict_str_int_field(self) -> None:
        obj = _make_multi_type_obj()
        assert obj.dict_str_int["a"] == 1
        obj.dict_str_int = {"c": 3}
        assert obj.dict_str_int["c"] == 3
        with pytest.raises((TypeError, RuntimeError)):
            obj.dict_str_int = "wrong"  # ty:ignore[invalid-assignment]

    def test_untyped_dict_field(self) -> None:
        obj = _make_multi_type_obj()
        assert len(obj.dict_any) == 2
        obj.dict_any = {"new": 42}
        assert obj.dict_any["new"] == 42

    def test_nested_list_field(self) -> None:
        obj = _make_multi_type_obj()
        assert tuple(obj.list_list_int[0]) == (1, 2, 3)
        assert tuple(obj.list_list_int[1]) == (4, 5, 6)

    def test_nested_dict_field(self) -> None:
        obj = _make_multi_type_obj()
        assert tuple(obj.dict_str_list_int["p"]) == (1, 2)
        assert tuple(obj.dict_str_list_int["q"]) == (3, 4)

    def test_optional_bool(self) -> None:
        obj = _make_multi_type_obj()
        assert obj.opt_bool is True
        obj.opt_bool = False
        assert obj.opt_bool is False
        obj.opt_bool = None
        assert obj.opt_bool is None

    def test_optional_int(self) -> None:
        obj = _make_multi_type_obj()
        assert obj.opt_int == -64
        obj.opt_int = None
        assert obj.opt_int is None
        obj.opt_int = 128
        assert obj.opt_int == 128

    def test_optional_float(self) -> None:
        obj = _make_multi_type_obj()
        assert obj.opt_float is None
        obj.opt_float = 1.5
        assert abs(obj.opt_float - 1.5) < 1e-10
        obj.opt_float = None
        assert obj.opt_float is None

    def test_optional_str(self) -> None:
        obj = _make_multi_type_obj()
        assert obj.opt_str is None
        obj.opt_str = "hello"
        assert obj.opt_str == "hello"
        obj.opt_str = None
        assert obj.opt_str is None

    def test_optional_list_int(self) -> None:
        obj = _make_multi_type_obj()
        assert tuple(obj.opt_list_int) == (10, 20)  # ty:ignore[invalid-argument-type]
        obj.opt_list_int = None
        assert obj.opt_list_int is None
        obj.opt_list_int = [30]
        assert len(obj.opt_list_int) == 1

    def test_optional_dict_str_int(self) -> None:
        obj = _make_multi_type_obj()
        assert obj.opt_dict_str_int is None
        obj.opt_dict_str_int = {"z": 99}
        assert obj.opt_dict_str_int["z"] == 99
        obj.opt_dict_str_int = None
        assert obj.opt_dict_str_int is None


class TestMultiTypeCopy:
    """Copy with the comprehensive multi-type class."""

    def test_shallow_copy_comprehensive(self) -> None:
        obj = _make_multi_type_obj()
        obj2 = copy.copy(obj)
        assert obj2.bool_ == obj.bool_
        assert obj2.i64 == obj.i64
        assert obj2.f64 == obj.f64
        assert obj2.str_ == obj.str_
        assert obj2.any_val == obj.any_val
        assert obj.list_int.same_as(obj2.list_int)  # ty:ignore[unresolved-attribute]
        assert obj.dict_str_int.same_as(obj2.dict_str_int)  # ty:ignore[unresolved-attribute]

    def test_deep_copy_comprehensive(self) -> None:
        obj = _make_multi_type_obj()
        obj2 = copy.deepcopy(obj)
        assert obj2.bool_ == obj.bool_
        assert obj2.i64 == obj.i64
        assert not obj.list_int.same_as(obj2.list_int)  # ty:ignore[unresolved-attribute]
        assert not obj.dict_str_int.same_as(obj2.dict_str_int)  # ty:ignore[unresolved-attribute]
        assert tuple(obj2.list_int) == (1, 2, 3)
        assert obj2.dict_str_int["a"] == 1


# ---------------------------------------------------------------------------
# _collect_py_methods allowlist and method introspection
# ---------------------------------------------------------------------------


class TestPyMethodAllowlist:
    """Only names in ``_FFI_RECOGNIZED_METHODS`` are collected by ``_collect_py_methods``."""

    def test_system_methods_not_in_allowlist(self) -> None:
        from tvm_ffi.dataclasses.py_class import _collect_py_methods  # noqa: PLC0415

        @py_class(_unique_key("Allow"))
        class Allow(core.Object):
            x: int

            def __ffi_init__(self, x: int) -> None:  # ty: ignore[invalid-method-override]
                pass

            def __ffi_shallow_copy__(self) -> None:
                pass

            def __ffi_repr__(self, fn_repr: Any) -> str:
                return "repr"

        collected = _collect_py_methods(Allow)
        assert collected is not None
        names = {name for name, _, _ in collected}
        assert "__ffi_repr__" in names
        assert "__ffi_init__" not in names
        assert "__ffi_shallow_copy__" not in names

    def test_arbitrary_ffi_dunder_not_collected(self) -> None:
        from tvm_ffi.dataclasses.py_class import _collect_py_methods  # noqa: PLC0415

        @py_class(_unique_key("Arb"))
        class Arb(core.Object):
            x: int

            def __ffi_custom_op__(self, y: int) -> int:
                return self.x + y

        collected = _collect_py_methods(Arb)
        assert collected is None


class TestPyMethodIntrospection:
    """Registered __ffi_* hooks appear in TypeAttrColumn (not TypeMethod)."""

    def test_ffi_repr_in_type_attr(self) -> None:
        @py_class(_unique_key("IntrRepr"))
        class IntrRepr(core.Object):
            x: int

            def __ffi_repr__(self, fn_repr: Any) -> str:
                return "repr"

        info = getattr(IntrRepr, "__tvm_ffi_type_info__")
        # User-defined hooks are registered as TypeAttrColumn, not TypeMethod
        repr_attr = core._lookup_type_attr(info.type_index, "__ffi_repr__")
        assert repr_attr is not None
        # System-generated hooks are also TypeAttrColumn only
        ffi_init = core._lookup_type_attr(info.type_index, "__ffi_init__")
        assert ffi_init is not None
        ffi_copy = core._lookup_type_attr(info.type_index, "__ffi_shallow_copy__")
        assert ffi_copy is not None


# ---------------------------------------------------------------------------
# super().__init__() support for @py_class(init=False) subclasses
# ---------------------------------------------------------------------------
class TestSuperInitPattern:
    """Regression tests for using ``super().__init__()`` + field assignment
    in ``@py_class(init=False)`` custom ``__init__`` methods.

    Previously, ``super().__init__()`` would dispatch to the parent's
    auto-generated ``__init__`` which called ``self.__ffi_init__()`` with
    the **child** type's C++ constructor (requiring field arguments that
    weren't provided), causing a crash.
    """

    def test_basic_super_init_with_field_setters(self) -> None:
        """The original crash scenario: init=False with super().__init__() and field setters."""

        @py_class(_unique_key("SIBase"))
        class SIBase(Object):
            pass

        @py_class(_unique_key("SIChild"), init=False)
        class SIChild(SIBase):
            x: int
            y: str

            def __init__(self, x: int, y: str) -> None:
                super().__init__()
                self.x = x
                self.y = y

        obj = SIChild(42, "hello")
        assert obj.x == 42
        assert obj.y == "hello"

    def test_super_init_deep_hierarchy(self) -> None:
        """super().__init__() through multiple levels of py_class inheritance."""

        @py_class(_unique_key("SIDH_Node"))
        class Node(Object):
            pass

        @py_class(_unique_key("SIDH_BaseType"))
        class BaseType(Node):
            pass

        @py_class(_unique_key("SIDH_PtrType"), init=False)
        class PtrType(BaseType):
            base_type: Any
            specifiers: list
            use_bracket: bool

            def __init__(
                self,
                base_type: Any,
                specifiers: Optional[list] = None,
                use_bracket: bool = False,
            ) -> None:
                super().__init__()
                self.base_type = base_type
                self.specifiers = list(specifiers) if specifiers else []
                self.use_bracket = use_bracket

        void_p = PtrType("void")
        assert void_p.base_type == "void"
        assert list(void_p.specifiers) == []
        assert void_p.use_bracket is False

        int_p = PtrType("int", ["const", "volatile"], True)
        assert int_p.base_type == "int"
        assert list(int_p.specifiers) == ["const", "volatile"]
        assert int_p.use_bracket is True

    def test_super_init_with_defaults_from_calloc(self) -> None:
        """Fields not set after super().__init__() should be zero-initialized."""

        @py_class(_unique_key("SIDef"))
        class SIDefBase(Object):
            pass

        @py_class(_unique_key("SIDef_Child"), init=False)
        class SIDefChild(SIDefBase):
            a: int
            b: float
            c: bool
            d: Any

            def __init__(self) -> None:
                super().__init__()
                # Don't set any fields — they should be zero/None from calloc

        obj = SIDefChild()
        assert obj.a == 0
        assert obj.b == 0.0
        assert obj.c is False
        assert obj.d is None

    def test_super_init_intermediate_custom_init(self) -> None:
        """Chained super().__init__() through an intermediate init=False class."""

        @py_class(_unique_key("SIChain_A"))
        class A(Object):
            pass

        @py_class(_unique_key("SIChain_B"), init=False)
        class B(A):
            x: int

            def __init__(self, x: int) -> None:
                super().__init__()
                self.x = x

        @py_class(_unique_key("SIChain_C"), init=False)
        class C(B):
            y: str

            def __init__(self, x: int, y: str) -> None:
                super().__init__(x)
                self.y = y

        obj = C(10, "world")
        assert obj.x == 10
        assert obj.y == "world"

    def test_super_init_intermediate_auto_init(self) -> None:
        """py_class init=False: no need to call super().__init__(), just set fields directly."""

        @py_class(_unique_key("SIAI_Mid"))
        class Mid(Object):
            a: int

        @py_class(_unique_key("SIAI_Leaf"), init=False)
        class Leaf(Mid):
            b: str

            def __init__(self, a: int, b: str) -> None:
                # For py_class, __new__ already allocated via __ffi_new__.
                # No need to call super().__init__() — just set fields directly.
                self.a = a
                self.b = b

        obj = Leaf(5, "hi")
        assert obj.a == 5
        assert obj.b == "hi"

    def test_normal_init_unaffected(self) -> None:
        """Normal init=True construction must still work correctly."""

        @py_class(_unique_key("SINorm"))
        class SINorm(Object):
            x: int
            y: str

        obj = SINorm(1, "a")
        assert obj.x == 1
        assert obj.y == "a"

    def test_non_pyclass_subclass_no_args_errors(self) -> None:
        """A non-py_class subclass calling parent init with no args should still error
        for required fields (not silently create an empty object).
        """

        @py_class(_unique_key("SINonPC"))
        class SINonPC(Object):
            x: int

        class Plain(SINonPC):
            pass

        with pytest.raises(TypeError):
            Plain()  # type: ignore[missing-argument]

    def test_super_init_isinstance(self) -> None:
        """Objects created via super().__init__() pattern have correct isinstance."""

        @py_class(_unique_key("SIInst_B"))
        class Base(Object):
            pass

        @py_class(_unique_key("SIInst_C"), init=False)
        class Child(Base):
            val: int

            def __init__(self, val: int) -> None:
                super().__init__()
                self.val = val

        obj = Child(99)
        assert isinstance(obj, Child)
        assert isinstance(obj, Base)
        assert isinstance(obj, Object)

    def test_super_init_field_overwrite(self) -> None:
        """Fields can be overwritten multiple times after super().__init__()."""

        @py_class(_unique_key("SIOverwrite_B"))
        class Base(Object):
            pass

        @py_class(_unique_key("SIOverwrite_C"), init=False)
        class Child(Base):
            x: int

            def __init__(self, x: int) -> None:
                super().__init__()
                self.x = 0
                self.x = x

        obj = Child(42)
        assert obj.x == 42

    def test_super_init_copy_deepcopy(self) -> None:
        """copy/deepcopy work on objects created via the super().__init__() pattern."""

        @py_class(_unique_key("SICopyBase"))
        class SICopyBase(Object):
            pass

        @py_class(_unique_key("SICopy"), init=False)
        class SICopy(SICopyBase):
            x: int
            y: str

            def __init__(self, x: int, y: str) -> None:
                super().__init__()
                self.x = x
                self.y = y

        obj = SICopy(42, "hello")
        obj2 = copy.copy(obj)
        assert obj2.x == 42
        assert obj2.y == "hello"
        assert not obj.same_as(obj2)

        obj3 = copy.deepcopy(obj)
        assert obj3.x == 42
        assert obj3.y == "hello"
        assert not obj.same_as(obj3)

    def test_super_init_direct_from_object(self) -> None:
        """super().__init__() works when inheriting directly from Object (no intermediate)."""

        @py_class(_unique_key("SIDirect"), init=False)
        class SIDirect(Object):
            x: int
            y: str

            def __init__(self, x: int, y: str) -> None:
                super().__init__()
                self.x = x
                self.y = y

        obj = SIDirect(10, "direct")
        assert obj.x == 10
        assert obj.y == "direct"
        assert isinstance(obj, Object)

    def test_super_init_direct_from_object_copy(self) -> None:
        """copy/deepcopy work for init=False classes inheriting directly from Object."""

        @py_class(_unique_key("SIDirectCopy"), init=False)
        class SIDirectCopy(Object):
            x: int
            y: str

            def __init__(self, x: int, y: str) -> None:
                super().__init__()
                self.x = x
                self.y = y

        obj = SIDirectCopy(42, "hello")
        obj2 = copy.copy(obj)
        assert obj2.x == 42
        assert obj2.y == "hello"
        assert not obj.same_as(obj2)

        obj3 = copy.deepcopy(obj)
        assert obj3.x == 42
        assert obj3.y == "hello"
        assert not obj.same_as(obj3)


class TestDtypeDeviceFields:
    """Regression: @py_class should accept tvm_ffi.dtype and tvm_ffi.Device as field types."""

    def test_dtype_field(self) -> None:
        @py_class(_unique_key("DtypeField"))
        class DtypeHolder(Object):
            dt: tvm_ffi.dtype

        obj = DtypeHolder(dt=tvm_ffi.dtype("float32"))
        assert obj.dt == "float32"
        assert isinstance(obj.dt, tvm_ffi.dtype)

    def test_dtype_field_setter(self) -> None:
        @py_class(_unique_key("DtypeFieldSet"))
        class DtypeHolder2(Object):
            dt: tvm_ffi.dtype

        obj = DtypeHolder2(dt=tvm_ffi.dtype("float32"))
        obj.dt = tvm_ffi.dtype("int8")
        assert obj.dt == "int8"

    def test_device_field(self) -> None:
        @py_class(_unique_key("DeviceField"))
        class DeviceHolder(Object):
            dev: tvm_ffi.Device

        dev = tvm_ffi.device("cpu", 0)
        obj = DeviceHolder(dev=dev)
        assert obj.dev == dev

    def test_dtype_device_together(self) -> None:
        @py_class(_unique_key("DtypeDeviceTogether"))
        class DtypeDeviceHolder(Object):
            dt: tvm_ffi.dtype
            dev: tvm_ffi.Device
            name: str

        dev = tvm_ffi.device("cpu", 0)
        obj = DtypeDeviceHolder(dt=tvm_ffi.dtype("float16"), dev=dev, name="test")
        assert obj.dt == "float16"
        assert obj.dev == dev
        assert obj.name == "test"

    def test_optional_dtype_field(self) -> None:
        @py_class(_unique_key("OptDtype"))
        class OptDtype(Object):
            dt: Optional[tvm_ffi.dtype] = None

        obj_none = OptDtype()
        assert obj_none.dt is None
        obj_val = OptDtype(dt=tvm_ffi.dtype("bfloat16"))
        assert obj_val.dt == "bfloat16"

    def test_optional_device_field(self) -> None:
        @py_class(_unique_key("OptDevice"))
        class OptDevice(Object):
            dev: Optional[tvm_ffi.Device] = None

        obj_none = OptDevice()
        assert obj_none.dev is None
        obj_val = OptDevice(dev=tvm_ffi.device("cpu", 0))
        assert obj_val.dev == tvm_ffi.device("cpu", 0)


# ###########################################################################
#  kw_only regression tests (py_class via __ffi_init__)
# ###########################################################################


class TestPyClassKwOnlyRegression:
    """Regression tests ensuring kw_only enforcement via C++ __ffi_init__."""

    def test_missing_kw_only_error_says_keyword_only(self) -> None:
        """Missing required kw_only field produces 'keyword-only' in the error."""

        @py_class(_unique_key("KwOnlyErr"))
        class KwOnlyErr(Object):
            x: int
            _: KW_ONLY
            y: int

        with pytest.raises(TypeError, match="keyword-only"):
            KwOnlyErr(1)  # ty: ignore[missing-argument]

    def test_missing_positional_error_not_keyword_only(self) -> None:
        """Missing required positional field does NOT say 'keyword-only'."""

        @py_class(_unique_key("PosErr"))
        class PosErr(Object):
            x: int
            _: KW_ONLY
            y: int

        with pytest.raises(TypeError, match="missing required argument") as exc_info:
            PosErr(y=1)  # ty: ignore[missing-argument]
        assert "keyword-only" not in str(exc_info.value)

    def test_kw_only_with_default(self) -> None:
        """kw_only field with default can be omitted."""

        @py_class(_unique_key("KwOnlyDef"))
        class KwOnlyDef(Object):
            x: int
            _: KW_ONLY
            y: int = 42

        obj = KwOnlyDef(1)  # ty: ignore[missing-argument]
        assert obj.x == 1
        assert obj.y == 42

    def test_kw_only_rejects_positional(self) -> None:
        """kw_only fields cannot be passed positionally."""

        @py_class(_unique_key("KwOnlyReject"))
        class KwOnlyReject(Object):
            x: int
            _: KW_ONLY
            y: int

        with pytest.raises(TypeError):
            KwOnlyReject(1, 2)  # ty: ignore[missing-argument, invalid-argument-type]

    def test_kw_only_all_fields(self) -> None:
        """All fields kw_only via decorator kwarg."""

        @py_class(_unique_key("AllKw"), kw_only=True)
        class AllKw(Object):
            a: int
            b: int = 10

        obj = AllKw(a=1)
        assert obj.a == 1
        assert obj.b == 10
        with pytest.raises(TypeError):
            AllKw(1)  # ty: ignore[missing-argument, too-many-positional-arguments]

    def test_kw_only_field_override_false(self) -> None:
        """kw_only=False on a field after KW_ONLY sentinel makes it positional."""

        @py_class(_unique_key("KwOverride2"))
        class KwOverride2(Object):
            _: KW_ONLY
            a: int
            b: int = field(kw_only=False)

        obj = KwOverride2(42, a=1)  # ty: ignore[missing-argument, invalid-argument-type]
        assert obj.b == 42
        assert obj.a == 1

    def test_decorator_kw_only_with_override(self) -> None:
        """Decorator-level kw_only with field-level override."""

        @py_class(_unique_key("DecKwOverride"), kw_only=True)
        class DecKwOverride(Object):
            a: int = field(kw_only=False)
            b: int

        obj = DecKwOverride(1, b=2)  # ty: ignore[missing-argument, too-many-positional-arguments]
        assert obj.a == 1
        assert obj.b == 2

    def test_kw_only_inheritance(self) -> None:
        """kw_only enforcement works through inheritance."""

        @py_class(_unique_key("KwParent"))
        class KwParent(Object):
            x: int

        @py_class(_unique_key("KwChild"))
        class KwChild(KwParent):
            _: KW_ONLY
            y: int

        obj = KwChild(1, y=2)  # ty: ignore[missing-argument]
        assert obj.x == 1
        assert obj.y == 2

        with pytest.raises(TypeError):
            KwChild(1, 2)  # ty: ignore[missing-argument, invalid-argument-type]

        with pytest.raises(TypeError, match="keyword-only"):
            KwChild(1)  # ty: ignore[missing-argument]


# ###########################################################################
#  No-leak / no-spurious-allocation tests for py_class
# ###########################################################################


class TestPyClassNoLeak:
    """Verify that the removal of custom __new__ eliminates the memory leak.

    The original bug: ``@py_class`` installed a custom ``__new__`` that called
    ``__ffi_new__`` to pre-allocate a C++ object. When a py_class object was
    returned from C++ through ``make_ret_object``, ``cls.__new__(cls)``
    triggered that custom ``__new__``, allocating a spurious C++ object whose
    refcount was never decremented.
    """

    def test_no_custom_new_on_py_class(self) -> None:
        """py_class must NOT install a custom __new__."""

        @py_class(_unique_key("NoNew"))
        class NoNew(Object):
            x: int

        assert "__new__" not in NoNew.__dict__

    def test_no_custom_new_with_user_init(self) -> None:
        """py_class with user-defined __init__ must NOT install a custom __new__."""

        @py_class(_unique_key("NoNewUI"), init=False)
        class NoNewUI(Object):
            x: int

            def __init__(self, x: int) -> None:
                self.x = x

        assert "__new__" not in NoNewUI.__dict__

    def test_make_ret_no_spurious_alloc(self) -> None:
        """Objects returned from C++ (via DeepCopy) must not trigger spurious allocation."""

        @py_class(_unique_key("RetTest"))
        class RetTest(Object):
            x: int

        obj = RetTest(42)
        # DeepCopy returns through make_ret_object
        obj2 = DeepCopy(obj)
        assert obj2.x == 42
        assert not obj.same_as(obj2)

    def test_repeated_roundtrip_no_leak(self) -> None:
        """Repeated construct + DeepCopy cycles must not leak."""

        @py_class(_unique_key("RoundTrip"))
        class RoundTrip(Object):
            x: int
            y: str = "default"

        gc.collect()
        for i in range(1000):
            o = RoundTrip(i, str(i))
            o2 = DeepCopy(o)
            del o, o2
        gc.collect()

    def test_user_init_wraps_metadata(self) -> None:
        """Wrapped user __init__ preserves docstring and __wrapped__."""

        @py_class(_unique_key("WrapMeta"), init=False)
        class WrapMeta(Object):
            x: int

            def __init__(self, x: int) -> None:
                """Initialize WrapMeta."""
                self.x = x

        assert WrapMeta.__init__.__doc__ == "Initialize WrapMeta."
        assert hasattr(WrapMeta.__init__, "__wrapped__")

    def test_chandle_guard_skips_on_make_ret(self) -> None:
        """Auto-generated __init__ with chandle guard: make_ret never calls __init__."""

        @py_class(_unique_key("ChandleGuard"))
        class ChandleGuard(Object):
            x: int

        obj = ChandleGuard(7)
        obj2 = DeepCopy(obj)
        # If __init__ were called by make_ret, it would fail (no args)
        # or overwrite fields. Verify the value survived intact.
        assert obj2.x == 7

    def test_super_init_noop_after_ffi_init(self) -> None:
        """super().__init__() is a no-op when chandle is already set."""

        @py_class(_unique_key("SuperBase"))
        class SuperBase(Object):
            pass

        call_log: list[str] = []

        @py_class(_unique_key("SuperChild"), init=False)
        class SuperChild(SuperBase):
            x: int

            def __init__(self, x: int) -> None:
                call_log.append("before_super")
                super().__init__()
                call_log.append("after_super")
                self.x = x

        obj = SuperChild(42)
        assert obj.x == 42
        assert call_log == ["before_super", "after_super"]

    def test_user_init_mismatched_signature(self) -> None:
        """User __init__ whose args don't match field layout still works."""

        @py_class(_unique_key("Mismatch"), init=False)
        class Mismatch(Object):
            value: int
            ref: Optional[Object]

            def __init__(self, val: int) -> None:
                self.value = val
                self.ref = None

        obj = Mismatch(99)
        assert obj.value == 99
        assert obj.ref is None
