# 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.
"""Comprehensive tests for reflection-driven auto-generated ``__ffi_init__``.

This file exercises:
1. metadata emitted by C++ for auto-init traits
2. Python ``__init__`` signature generation
3. constructor behavior across positional/kw-only/default/init=False combinations
4. low-level KWARGS protocol via TypeAttrColumn __ffi_init__ lookup
5. inheritance behavior for auto-generated init
6. copy/deepcopy/replace interplay with auto-init objects
7. re-initialization, isinstance checks, and instance isolation
"""

# ruff: noqa: D102
from __future__ import annotations

import copy
import inspect
from typing import Any

import pytest
from tvm_ffi import core
from tvm_ffi.dataclasses import py_class
from tvm_ffi.testing import (
    TestCompare,
    TestHash,
    TestIntPair,
    _TestCxxAutoInit,
    _TestCxxAutoInitAllInitOff,
    _TestCxxAutoInitChild,
    _TestCxxAutoInitKwOnlyDefaults,
    _TestCxxAutoInitParent,
    _TestCxxAutoInitSimple,
    _TestCxxClassDerived,
    _TestCxxClassDerivedDerived,
    _TestCxxNoAutoInit,
)
from tvm_ffi.testing.testing import requires_py313


def _ffi_init(obj: Any, *args: Any) -> None:
    """Look up __ffi_init__ from the TypeAttrColumn and call it via __init_handle_by_constructor__."""
    type_index = type(obj).__tvm_ffi_type_info__.type_index
    ffi_init = core._lookup_type_attr(type_index, "__ffi_init__")
    assert ffi_init is not None, f"No __ffi_init__ TypeAttrColumn entry for {type(obj)}"
    obj.__init_handle_by_constructor__(ffi_init, *args)


def _field_map(type_cls: type) -> dict[str, Any]:
    return {field.name: field for field in getattr(type_cls, "__tvm_ffi_type_info__").fields}


def _method_metadata(type_cls: type, method_name: str) -> dict[str, Any]:
    type_info = getattr(type_cls, "__tvm_ffi_type_info__")
    for method in type_info.methods:
        if method.name == method_name:
            return method.metadata
    raise AssertionError(f"Cannot find method metadata: {type_cls.__name__}.{method_name}")


class TestAutoInitMetadata:
    def test_auto_init_method_marked(self) -> None:
        type_info = getattr(_TestCxxAutoInit, "__tvm_ffi_type_info__")
        ffi_init = core._lookup_type_attr(type_info.type_index, "__ffi_init__")
        assert ffi_init is not None
        # Auto-generated init is only in TypeAttrColumn, not in the method list
        assert not any(m.name == "__ffi_init__" for m in type_info.methods)

    def test_field_bitmask_init_kw_only_and_defaults(self) -> None:
        fields = _field_map(_TestCxxAutoInit)
        assert fields["a"].c_init is True
        assert fields["b"].c_init is False
        assert fields["b"].c_has_default is True
        assert fields["c"].c_kw_only is True
        assert fields["c"].c_init is True
        assert fields["d"].c_has_default is True

    def test_all_init_off_field_bitmask(self) -> None:
        fields = _field_map(_TestCxxAutoInitAllInitOff)
        assert fields["x"].c_init is False
        assert fields["x"].c_has_default is True
        assert fields["y"].c_init is False
        assert fields["y"].c_has_default is True
        assert fields["z"].c_init is False
        assert fields["z"].c_has_default is False

    def test_kw_only_defaults_field_bitmask(self) -> None:
        fields = _field_map(_TestCxxAutoInitKwOnlyDefaults)
        assert fields["p_default"].c_has_default is True
        assert fields["k_required"].c_kw_only is True
        assert fields["k_default"].c_kw_only is True
        assert fields["k_default"].c_has_default is True
        assert fields["hidden"].c_init is False
        assert fields["hidden"].c_has_default is True


class TestAutoInitSignature:
    def test_auto_init_signature_layout(self) -> None:
        sig = inspect.signature(_TestCxxAutoInit.__init__)
        assert tuple(sig.parameters) == ("self", "a", "d", "c")
        assert sig.parameters["a"].kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
        assert sig.parameters["d"].kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
        assert sig.parameters["c"].kind == inspect.Parameter.KEYWORD_ONLY

    def test_auto_init_signature_required_vs_default(self) -> None:
        sig = inspect.signature(_TestCxxAutoInit.__init__)
        assert sig.parameters["a"].default is inspect.Parameter.empty
        assert sig.parameters["c"].default is inspect.Parameter.empty
        assert sig.parameters["d"].default is not inspect.Parameter.empty

    def test_simple_signature(self) -> None:
        sig = inspect.signature(_TestCxxAutoInitSimple.__init__)
        assert tuple(sig.parameters) == ("self", "x", "y")
        assert all(
            p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
            for p in (sig.parameters["x"], sig.parameters["y"])
        )

    def test_all_init_off_signature_is_no_arg(self) -> None:
        sig = inspect.signature(_TestCxxAutoInitAllInitOff.__init__)
        assert tuple(sig.parameters) == ("self",)

    def test_kw_only_defaults_signature_layout(self) -> None:
        sig = inspect.signature(_TestCxxAutoInitKwOnlyDefaults.__init__)
        assert tuple(sig.parameters) == (
            "self",
            "p_required",
            "p_default",
            "k_required",
            "k_default",
        )
        assert sig.parameters["p_required"].kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
        assert sig.parameters["p_default"].kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
        assert sig.parameters["k_required"].kind == inspect.Parameter.KEYWORD_ONLY
        assert sig.parameters["k_default"].kind == inspect.Parameter.KEYWORD_ONLY
        assert sig.parameters["p_required"].default is inspect.Parameter.empty
        assert sig.parameters["k_required"].default is inspect.Parameter.empty
        assert sig.parameters["p_default"].default is not inspect.Parameter.empty
        assert sig.parameters["k_default"].default is not inspect.Parameter.empty

    def test_inheritance_signature_includes_parent_fields(self) -> None:
        sig = inspect.signature(_TestCxxAutoInitChild.__init__)
        # Required positional params must precede default ones in Python,
        # so child_required comes before parent_default.
        assert tuple(sig.parameters) == (
            "self",
            "parent_required",
            "child_required",
            "parent_default",
            "child_kw_only",
        )
        assert sig.parameters["child_kw_only"].kind == inspect.Parameter.KEYWORD_ONLY
        assert sig.parameters["parent_default"].default is not inspect.Parameter.empty

    def test_init_false_field_not_in_signature(self) -> None:
        """B is init=False and should not appear in signature."""
        sig = inspect.signature(_TestCxxAutoInit.__init__)
        assert "b" not in sig.parameters

    def test_hidden_field_not_in_signature(self) -> None:
        """Hidden is init=False and should not appear in signature."""
        sig = inspect.signature(_TestCxxAutoInitKwOnlyDefaults.__init__)
        assert "hidden" not in sig.parameters

    def test_kw_only_params_after_positional(self) -> None:
        """Keyword-only params should come after positional in the signature."""
        sig = inspect.signature(_TestCxxAutoInit.__init__)
        params = list(sig.parameters.values())
        saw_kw_only = False
        for p in params[1:]:  # skip self
            if p.kind == inspect.Parameter.KEYWORD_ONLY:
                saw_kw_only = True
            elif saw_kw_only:
                assert p.kind == inspect.Parameter.KEYWORD_ONLY, (
                    f"Parameter {p.name} is {p.kind} after a KEYWORD_ONLY param"
                )

    def test_child_signature_required_before_optional(self) -> None:
        """The child signature should have all required positional before optional."""
        sig = inspect.signature(_TestCxxAutoInitChild.__init__)
        params = list(sig.parameters.values())[1:]  # skip self
        positional = [p for p in params if p.kind != inspect.Parameter.KEYWORD_ONLY]
        saw_default = False
        for p in positional:
            if p.default is not inspect.Parameter.empty:
                saw_default = True
            elif saw_default:
                pytest.fail(f"Required param '{p.name}' appears after a default param")


class TestAutoInitConstruction:
    def test_auto_init_minimal_required(self) -> None:
        obj = _TestCxxAutoInit(1, c=3)
        assert obj.a == 1
        assert obj.b == 42
        assert obj.c == 3
        assert obj.d == 99

    def test_auto_init_all_keyword_arguments(self) -> None:
        obj = _TestCxxAutoInit(a=10, c=30, d=20)
        assert obj.a == 10
        assert obj.b == 42
        assert obj.c == 30
        assert obj.d == 20

    def test_auto_init_keyword_order_irrelevant(self) -> None:
        obj = _TestCxxAutoInit(c=7, d=8, a=9)
        assert obj.a == 9
        assert obj.c == 7
        assert obj.d == 8
        assert obj.b == 42

    def test_auto_init_second_positional_maps_to_d(self) -> None:
        obj = _TestCxxAutoInit(1, 2, c=3)
        assert obj.a == 1
        assert obj.d == 2
        assert obj.c == 3
        assert obj.b == 42

    def test_mutate_fields_after_construction(self) -> None:
        obj = _TestCxxAutoInit(1, c=2)
        obj.b = 100
        obj.c = 999
        assert obj.b == 100
        assert obj.c == 999


class TestAutoInitErrors:
    def test_missing_required_positional(self) -> None:
        with pytest.raises(TypeError):
            _TestCxxAutoInit(c=3)  # ty: ignore[missing-argument]

    def test_missing_required_kw_only(self) -> None:
        with pytest.raises(TypeError):
            _TestCxxAutoInit(1)  # ty: ignore[missing-argument]

    def test_kw_only_rejects_positional(self) -> None:
        with pytest.raises(TypeError):
            _TestCxxAutoInit(1, 2, 3)  # ty: ignore[missing-argument, too-many-positional-arguments]

    def test_duplicate_argument_detection(self) -> None:
        with pytest.raises(TypeError):
            _TestCxxAutoInit(1, 2, c=3, d=4)  # ty: ignore[parameter-already-assigned]

    def test_unexpected_keyword(self) -> None:
        with pytest.raises(TypeError):
            _TestCxxAutoInit(1, c=2, nope=3)  # ty: ignore[unknown-argument]

    def test_init_false_field_rejected_in_python_init(self) -> None:
        with pytest.raises(TypeError):
            _TestCxxAutoInit(1, b=2, c=3)  # ty: ignore[unknown-argument]

    def test_type_mismatch_for_required_positional(self) -> None:
        with pytest.raises(TypeError):
            _TestCxxAutoInit("x", c=2)  # ty: ignore[invalid-argument-type]

    def test_type_mismatch_for_kw_only(self) -> None:
        with pytest.raises(TypeError):
            _TestCxxAutoInit(1, c="x")  # ty: ignore[invalid-argument-type]

    def test_type_mismatch_for_defaultable_positional(self) -> None:
        with pytest.raises(TypeError):
            _TestCxxAutoInit(1, d="x", c=3)  # ty: ignore[invalid-argument-type]

    def test_init_false_field_rejected_via_dict_unpacking(self) -> None:
        """B is init=False, rejected even when passed via **dict."""
        with pytest.raises(TypeError):
            _TestCxxAutoInit(**{"a": 1, "c": 2, "b": 3})

    def test_positional_and_keyword_same_field(self) -> None:
        """Providing a field both positionally and as keyword should error."""
        with pytest.raises(TypeError):
            _TestCxxAutoInit(1, a=2, c=3)  # ty: ignore[parameter-already-assigned]

    def test_none_for_required_integer_field(self) -> None:
        """Passing None where an int64_t is expected should raise TypeError."""
        with pytest.raises(TypeError):
            _TestCxxAutoInitSimple(None, 1)  # ty: ignore[invalid-argument-type]

    def test_none_for_keyword_integer_field(self) -> None:
        """Passing None as keyword where int64_t is expected."""
        with pytest.raises(TypeError):
            _TestCxxAutoInitSimple(x=None, y=1)  # ty: ignore[invalid-argument-type]


class TestAutoInitLowLevelFfiInit:
    def test_low_level_kwargs_protocol(self) -> None:
        obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
        _ffi_init(obj, core.KWARGS, "a", 1, "c", 3)
        assert obj.a == 1
        assert obj.b == 42
        assert obj.c == 3
        assert obj.d == 99

    def test_low_level_kwargs_even_pairs_required(self) -> None:
        obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
        with pytest.raises(TypeError):
            _ffi_init(obj, core.KWARGS, "a", 1, "c")

    def test_low_level_kwargs_duplicate_name(self) -> None:
        obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
        with pytest.raises(TypeError):
            _ffi_init(obj, core.KWARGS, "a", 1, "a", 2, "c", 3)

    def test_low_level_kwargs_unknown_name(self) -> None:
        obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
        with pytest.raises(TypeError):
            _ffi_init(obj, core.KWARGS, "a", 1, "unknown", 2, "c", 3)

    def test_low_level_kwargs_key_must_be_string(self) -> None:
        obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
        with pytest.raises(TypeError):
            _ffi_init(obj, core.KWARGS, 1, 2, "a", 3, "c", 4)

    def test_low_level_positional_too_many(self) -> None:
        obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
        with pytest.raises(TypeError):
            _ffi_init(obj, 1, 2, 3, 4)

    def test_low_level_missing_required(self) -> None:
        obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
        with pytest.raises(TypeError):
            _ffi_init(obj, 1)

    def test_low_level_kw_only_field_rejected_positionally(self) -> None:
        obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
        with pytest.raises(TypeError):
            _ffi_init(obj, 1, 2)

    def test_low_level_init_false_field_rejected(self) -> None:
        obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
        with pytest.raises(TypeError):
            _ffi_init(obj, core.KWARGS, "a", 1, "b", 2, "c", 3)

    def test_low_level_kwargs_all_init_fields_explicit(self) -> None:
        """Providing all init=True fields via KWARGS."""
        obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
        _ffi_init(obj, core.KWARGS, "a", 10, "c", 30, "d", 40)
        assert obj.a == 10
        assert obj.b == 42  # init=False, default
        assert obj.c == 30
        assert obj.d == 40

    def test_low_level_kwargs_empty_string_key(self) -> None:
        """Empty string as keyword name should be rejected as unknown."""
        obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
        with pytest.raises(TypeError, match="unexpected keyword"):
            _ffi_init(obj, core.KWARGS, "", 1, "a", 2, "c", 3)

    def test_low_level_kwargs_odd_kv_count(self) -> None:
        """Odd number of key-value args after KWARGS sentinel."""
        obj = _TestCxxAutoInitSimple.__new__(_TestCxxAutoInitSimple)
        with pytest.raises(TypeError):
            _ffi_init(obj, core.KWARGS, "x")

    def test_low_level_positional_only_simple(self) -> None:
        """Positional-only mode (no KWARGS sentinel)."""
        obj = _TestCxxAutoInitSimple.__new__(_TestCxxAutoInitSimple)
        _ffi_init(obj, 10, 20)
        assert obj.x == 10
        assert obj.y == 20

    def test_low_level_zero_args_missing_required(self) -> None:
        """Zero args for a type that requires them."""
        obj = _TestCxxAutoInitSimple.__new__(_TestCxxAutoInitSimple)
        with pytest.raises(TypeError, match="missing required"):
            _ffi_init(obj)

    def test_low_level_kwargs_sentinel_only_no_kv_pairs(self) -> None:
        """KWARGS sentinel with zero key-value pairs; required fields still missing."""
        obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
        with pytest.raises(TypeError, match="missing required"):
            _ffi_init(obj, core.KWARGS)

    def test_low_level_kwargs_positional_then_sentinel_no_kv(self) -> None:
        """Positional args followed by KWARGS sentinel but no key-value pairs."""
        obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
        # a=1 positionally, sentinel, no KV pairs → c is still missing
        with pytest.raises(TypeError, match="missing required"):
            _ffi_init(obj, 1, core.KWARGS)

    def test_low_level_child_positional_routing(self) -> None:
        """Verify the inheritance positional fix at the raw __ffi_init__ level.

        After stable_partition, pos_indices should be:
        [parent_required, child_required, parent_default]
        So 2 positional args map to parent_required=1, child_required=2.
        """
        obj = _TestCxxAutoInitChild.__new__(_TestCxxAutoInitChild)
        _ffi_init(obj, 1, 2, core.KWARGS, "child_kw_only", 3)
        assert obj.parent_required == 1
        assert obj.child_required == 2
        assert obj.parent_default == 5  # default
        assert obj.child_kw_only == 3

    def test_low_level_child_all_three_positional(self) -> None:
        """Three positional args for child at the raw protocol level."""
        obj = _TestCxxAutoInitChild.__new__(_TestCxxAutoInitChild)
        _ffi_init(obj, 1, 2, 3, core.KWARGS, "child_kw_only", 4)
        assert obj.parent_required == 1
        assert obj.child_required == 2
        assert obj.parent_default == 3
        assert obj.child_kw_only == 4

    def test_low_level_child_too_many_positional(self) -> None:
        """Four positional args exceed the 3 positional slots for child."""
        obj = _TestCxxAutoInitChild.__new__(_TestCxxAutoInitChild)
        with pytest.raises(TypeError, match="positional"):
            _ffi_init(obj, 1, 2, 3, 4, core.KWARGS, "child_kw_only", 5)


class TestAutoInitSimple:
    def test_simple_positional(self) -> None:
        obj = _TestCxxAutoInitSimple(10, 20)
        assert obj.x == 10
        assert obj.y == 20

    def test_simple_keyword(self) -> None:
        obj = _TestCxxAutoInitSimple(x=10, y=20)
        assert obj.x == 10
        assert obj.y == 20

    def test_simple_mixed(self) -> None:
        obj = _TestCxxAutoInitSimple(10, y=20)
        assert obj.x == 10
        assert obj.y == 20

    def test_simple_missing_required(self) -> None:
        with pytest.raises(TypeError):
            _TestCxxAutoInitSimple(x=10)  # ty: ignore[missing-argument]

    def test_simple_too_many_positional(self) -> None:
        with pytest.raises(TypeError):
            _TestCxxAutoInitSimple(1, 2, 3)  # ty: ignore[too-many-positional-arguments]

    def test_simple_unexpected_keyword(self) -> None:
        with pytest.raises(TypeError):
            _TestCxxAutoInitSimple(x=1, y=2, z=3)  # ty: ignore[unknown-argument]

    def test_simple_low_level_kwargs(self) -> None:
        obj = _TestCxxAutoInitSimple.__new__(_TestCxxAutoInitSimple)
        _ffi_init(obj, core.KWARGS, "x", 10, "y", 20)
        assert obj.x == 10
        assert obj.y == 20

    def test_zero_values(self) -> None:
        obj = _TestCxxAutoInitSimple(0, 0)
        assert obj.x == 0
        assert obj.y == 0

    def test_negative_values(self) -> None:
        obj = _TestCxxAutoInitSimple(-1, -9999999)
        assert obj.x == -1
        assert obj.y == -9999999

    def test_large_values(self) -> None:
        large = 2**62
        obj = _TestCxxAutoInitSimple(large, -large)
        assert obj.x == large
        assert obj.y == -large

    def test_float_for_integer_field(self) -> None:
        """Passing float where int64_t is expected - should truncate or error."""
        try:
            obj = _TestCxxAutoInitSimple(1.5, 2)  # ty: ignore[invalid-argument-type]
            assert isinstance(obj.x, int)
        except TypeError:
            pass  # Also acceptable

    def test_bool_for_integer_field(self) -> None:
        """Booleans are valid ints in Python but should work correctly."""
        obj = _TestCxxAutoInitSimple(True, False)
        assert obj.x == 1
        assert obj.y == 0


class TestAutoInitAllInitOff:
    def test_no_arg_constructor(self) -> None:
        obj = _TestCxxAutoInitAllInitOff()
        assert obj.x == 7
        assert obj.y == 9
        assert obj.z == 1234

    def test_rejects_positional_args(self) -> None:
        with pytest.raises(TypeError):
            _TestCxxAutoInitAllInitOff(1)  # ty: ignore[too-many-positional-arguments]

    def test_rejects_keyword_args(self) -> None:
        with pytest.raises(TypeError):
            _TestCxxAutoInitAllInitOff(x=1)  # ty: ignore[unknown-argument]

    def test_low_level_empty_init(self) -> None:
        obj = _TestCxxAutoInitAllInitOff.__new__(_TestCxxAutoInitAllInitOff)
        _ffi_init(obj)
        assert obj.x == 7
        assert obj.y == 9
        assert obj.z == 1234

    def test_mutate_fields(self) -> None:
        obj = _TestCxxAutoInitAllInitOff()
        obj.x = 101
        obj.y = 202
        obj.z = 303
        assert (obj.x, obj.y, obj.z) == (101, 202, 303)

    def test_empty_kwargs_dict_star(self) -> None:
        """Passing **{} should be fine (empty kwargs)."""
        obj = _TestCxxAutoInitAllInitOff(**{})
        assert obj.x == 7

    def test_low_level_rejects_positional(self) -> None:
        obj = _TestCxxAutoInitAllInitOff.__new__(_TestCxxAutoInitAllInitOff)
        with pytest.raises(TypeError):
            _ffi_init(obj, 1)


class TestAutoInitKwOnlyDefaults:
    def test_minimal_required(self) -> None:
        obj = _TestCxxAutoInitKwOnlyDefaults(1, k_required=2)
        assert obj.p_required == 1
        assert obj.p_default == 11
        assert obj.k_required == 2
        assert obj.k_default == 22
        assert obj.hidden == 33

    def test_override_defaults(self) -> None:
        obj = _TestCxxAutoInitKwOnlyDefaults(p_required=1, p_default=4, k_required=5, k_default=6)
        assert obj.p_required == 1
        assert obj.p_default == 4
        assert obj.k_required == 5
        assert obj.k_default == 6
        assert obj.hidden == 33

    def test_missing_required_positional(self) -> None:
        with pytest.raises(TypeError):
            _TestCxxAutoInitKwOnlyDefaults(k_required=2)  # ty: ignore[missing-argument]

    def test_missing_required_kw_only(self) -> None:
        with pytest.raises(TypeError):
            _TestCxxAutoInitKwOnlyDefaults(1)  # ty: ignore[missing-argument]

    def test_kw_only_rejects_positional(self) -> None:
        with pytest.raises(TypeError):
            _TestCxxAutoInitKwOnlyDefaults(1, 2, 3)  # ty: ignore[missing-argument, too-many-positional-arguments]

    def test_hidden_init_false_field_not_accepted(self) -> None:
        with pytest.raises(TypeError):
            _TestCxxAutoInitKwOnlyDefaults(1, k_required=2, hidden=4)  # ty: ignore[unknown-argument]

    def test_type_mismatch(self) -> None:
        with pytest.raises(TypeError):
            _TestCxxAutoInitKwOnlyDefaults("x", k_required=2)  # ty: ignore[invalid-argument-type]

    def test_low_level_kwargs_call(self) -> None:
        obj = _TestCxxAutoInitKwOnlyDefaults.__new__(_TestCxxAutoInitKwOnlyDefaults)
        _ffi_init(obj, core.KWARGS, "p_required", 1, "k_required", 2)
        assert obj.p_required == 1
        assert obj.p_default == 11
        assert obj.k_required == 2
        assert obj.k_default == 22
        assert obj.hidden == 33

    def test_kw_only_via_dict_unpacking(self) -> None:
        """Verify kw_only fields work via **dict."""
        kwargs = {"k_required": 100, "k_default": 200}
        obj = _TestCxxAutoInitKwOnlyDefaults(1, **kwargs)
        assert obj.p_required == 1
        assert obj.p_default == 11  # default
        assert obj.k_required == 100
        assert obj.k_default == 200
        assert obj.hidden == 33  # init=False default


class TestAutoInitInheritance:
    def test_parent_constructor(self) -> None:
        obj = _TestCxxAutoInitParent(10)
        assert obj.parent_required == 10
        assert obj.parent_default == 5

    def test_parent_all_keyword(self) -> None:
        obj = _TestCxxAutoInitParent(parent_required=10, parent_default=20)
        assert obj.parent_required == 10
        assert obj.parent_default == 20

    def test_parent_positional_then_keyword(self) -> None:
        obj = _TestCxxAutoInitParent(10, parent_default=20)
        assert obj.parent_required == 10
        assert obj.parent_default == 20

    def test_child_constructor_uses_parent_and_child_fields(self) -> None:
        obj = _TestCxxAutoInitChild(parent_required=1, child_required=2, child_kw_only=3)
        assert obj.parent_required == 1
        assert obj.parent_default == 5
        assert obj.child_required == 2
        assert obj.child_kw_only == 3

    def test_child_all_keyword_with_parent_default_override(self) -> None:
        # fmt: off
        obj = _TestCxxAutoInitChild(parent_required=1, child_required=2, parent_default=99, child_kw_only=3)
        # fmt: on
        assert obj.parent_required == 1
        assert obj.child_required == 2
        assert obj.parent_default == 99
        assert obj.child_kw_only == 3

    def test_child_missing_parent_required(self) -> None:
        with pytest.raises(TypeError):
            _TestCxxAutoInitChild(child_required=2, child_kw_only=3)  # ty: ignore[missing-argument]

    def test_child_two_positional_args_routes_correctly(self) -> None:
        """Calling Child(1, 2, child_kw_only=3) should set parent_required=1, child_required=2.

        The Python signature is:
          (self, parent_required, child_required, parent_default=..., *, child_kw_only)
        So positional arg 0 = parent_required, positional arg 1 = child_required.
        """
        obj = _TestCxxAutoInitChild(1, 2, child_kw_only=3)
        assert obj.parent_required == 1
        assert obj.child_required == 2
        assert obj.parent_default == 5  # should use the default
        assert obj.child_kw_only == 3

    def test_child_three_positional_args_no_silent_swap(self) -> None:
        """Calling Child(1, 2, 3, child_kw_only=4) should map correctly.

        Python signature order: parent_required=1, child_required=2, parent_default=3
        """
        obj = _TestCxxAutoInitChild(1, 2, 3, child_kw_only=4)
        assert obj.parent_required == 1
        assert obj.child_required == 2
        assert obj.parent_default == 3
        assert obj.child_kw_only == 4

    def test_child_one_positional_rest_keyword(self) -> None:
        """Mix of positional and keyword to verify correct mapping."""
        obj = _TestCxxAutoInitChild(10, child_required=20, parent_default=30, child_kw_only=40)
        assert obj.parent_required == 10
        assert obj.child_required == 20
        assert obj.parent_default == 30
        assert obj.child_kw_only == 40


class TestAutoInitCopyBehavior:
    """Test copy/deepcopy/replace interplay with auto-init objects."""

    def test_shallow_copy(self) -> None:
        obj = _TestCxxAutoInitSimple(10, 20)
        obj_copy = copy.copy(obj)
        assert obj_copy.x == 10
        assert obj_copy.y == 20
        assert not obj.same_as(obj_copy)

    def test_deepcopy(self) -> None:
        obj = _TestCxxAutoInitSimple(10, 20)
        obj_copy = copy.deepcopy(obj)
        assert obj_copy.x == 10
        assert obj_copy.y == 20
        assert not obj.same_as(obj_copy)

    @requires_py313
    def test_replace(self) -> None:
        obj = _TestCxxAutoInit(1, c=3)
        replaced = copy.replace(obj, a=100, c=300)  # type: ignore[attr-defined]
        assert replaced.a == 100
        assert replaced.b == 42
        assert replaced.c == 300
        assert replaced.d == 99

    def test_copy_preserves_init_false_field(self) -> None:
        """After construction, mutating the init=False field and copying."""
        obj = _TestCxxAutoInit(1, c=3)
        assert obj.b == 42
        obj.b = 999
        assert obj.b == 999
        obj_copy = copy.copy(obj)
        assert obj_copy.b == 999

    def test_copy_preserves_default_override(self) -> None:
        """Override a default field, then copy should preserve the override."""
        obj = _TestCxxAutoInit(1, c=3, d=55)
        obj_copy = copy.copy(obj)
        assert obj_copy.d == 55

    def test_deepcopy_all_init_off(self) -> None:
        """Deepcopy of an object with all fields init=False."""
        obj = _TestCxxAutoInitAllInitOff()
        obj.x = 111
        obj.y = 222
        obj.z = 333
        obj_copy = copy.deepcopy(obj)
        assert obj_copy.x == 111
        assert obj_copy.y == 222
        assert obj_copy.z == 333
        assert not obj.same_as(obj_copy)

    @requires_py313
    def test_replace_kw_only_defaults(self) -> None:
        obj = _TestCxxAutoInitKwOnlyDefaults(1, k_required=2)
        replaced = copy.replace(obj, k_required=99, p_default=88)  # type: ignore[attr-defined]
        assert replaced.p_required == 1
        assert replaced.p_default == 88
        assert replaced.k_required == 99
        assert replaced.k_default == 22
        assert replaced.hidden == 33


class TestAutoInitReinitialization:
    """Test what happens when __ffi_init__ is called multiple times."""

    def test_reinit_changes_handle(self) -> None:
        """Calling __ffi_init__ again should create a new underlying object."""
        obj = _TestCxxAutoInit(1, c=3)
        original_handle = obj.__chandle__()
        assert obj.a == 1

        _ffi_init(obj, core.KWARGS, "a", 100, "c", 300)
        assert obj.a == 100
        assert obj.c == 300
        assert obj.__chandle__() != original_handle

    def test_reinit_resets_init_false_field(self) -> None:
        """Re-initialization should reset init=False fields to defaults."""
        obj = _TestCxxAutoInit(1, c=3)
        obj.b = 999
        assert obj.b == 999

        _ffi_init(obj, core.KWARGS, "a", 2, "c", 4)
        assert obj.b == 42  # reset to default


class TestAutoInitTypeChecks:
    """Verify isinstance relationships for auto-init objects."""

    def test_parent_isinstance(self) -> None:
        obj = _TestCxxAutoInitParent(1)
        assert isinstance(obj, _TestCxxAutoInitParent)
        assert isinstance(obj, core.Object)

    def test_child_isinstance_parent(self) -> None:
        obj = _TestCxxAutoInitChild(parent_required=1, child_required=2, child_kw_only=3)
        assert isinstance(obj, _TestCxxAutoInitChild)
        assert isinstance(obj, _TestCxxAutoInitParent)
        assert isinstance(obj, core.Object)

    def test_parent_not_isinstance_child(self) -> None:
        """A parent object should not pass isinstance check for a child class."""
        obj = _TestCxxAutoInitParent(1)
        assert not isinstance(obj, _TestCxxAutoInitChild)


class TestAutoInitInstanceIsolation:
    """Verify that multiple instances don't share mutable state."""

    def test_separate_instances_are_independent(self) -> None:
        a = _TestCxxAutoInitSimple(1, 2)
        b = _TestCxxAutoInitSimple(3, 4)
        assert a.x == 1
        assert b.x == 3
        assert not a.same_as(b)

    def test_mutating_one_instance_doesnt_affect_another(self) -> None:
        a = _TestCxxAutoInit(1, c=3)
        b = _TestCxxAutoInit(1, c=3)
        assert a.b == 42
        assert b.b == 42
        a.b = 999
        assert a.b == 999
        assert b.b == 42

    def test_all_init_off_instances_are_independent(self) -> None:
        a = _TestCxxAutoInitAllInitOff()
        b = _TestCxxAutoInitAllInitOff()
        a.x = 100
        assert b.x == 7


class TestAutoInitReinitInitOffNoDefault:
    """Test reinit behavior for init=False fields with and without reflection defaults."""

    def test_reinit_init_false_with_default_resets(self) -> None:
        """Fields b (init=False, default=42) should reset to default on reinit."""
        obj = _TestCxxAutoInit(1, c=3)
        obj.b = 999
        _ffi_init(obj, core.KWARGS, "a", 2, "c", 4)
        assert obj.b == 42

    def test_reinit_init_false_without_reflection_default(self) -> None:
        """Field z has init=False AND no reflection default (c_has_default=False).

        On reinit, z should get whatever the C++ creator sets (1234).
        """
        obj = _TestCxxAutoInitAllInitOff()
        assert obj.z == 1234
        obj.z = 9999
        assert obj.z == 9999
        # Reinit via low-level call
        _ffi_init(obj)
        # z has no reflection default, so creator's C++ default (1234) is used
        assert obj.z == 1234


class TestAutoInitErrorMessages:
    """Verify that error messages name the correct field after pos_indices reordering."""

    def test_missing_child_required_names_correct_field(self) -> None:
        """When child_required is missing, error should mention 'child_required'."""
        with pytest.raises(TypeError, match="child_required"):
            _TestCxxAutoInitChild(parent_required=1, child_kw_only=3)  # ty: ignore[missing-argument]

    def test_missing_parent_required_names_correct_field(self) -> None:
        """When parent_required is missing, error should mention 'parent_required'."""
        with pytest.raises(TypeError, match="parent_required"):
            _TestCxxAutoInitChild(child_required=2, child_kw_only=3)  # ty: ignore[missing-argument]

    def test_missing_kw_only_names_correct_field(self) -> None:
        """When child_kw_only is missing, error should mention 'child_kw_only'."""
        with pytest.raises(TypeError, match="child_kw_only"):
            _TestCxxAutoInitChild(parent_required=1, child_required=2)  # ty: ignore[missing-argument]

    def test_too_many_positional_error_message(self) -> None:
        """Error for too many positional args should mention the correct count."""
        with pytest.raises(TypeError, match="3 positional"):
            _TestCxxAutoInitChild(1, 2, 3, 4, child_kw_only=5)  # ty: ignore[too-many-positional-arguments]

    def test_unexpected_keyword_error_message(self) -> None:
        """Error for unknown keyword should mention the keyword name."""
        with pytest.raises(TypeError, match="bogus"):
            _TestCxxAutoInit(1, c=2, bogus=3)  # ty: ignore[unknown-argument]

    def test_duplicate_arg_error_message(self) -> None:
        """Error for duplicate argument should mention the field name."""
        with pytest.raises(TypeError, match=r"multiple values.*a"):
            _TestCxxAutoInit(1, c=2, a=3)  # ty: ignore[parameter-already-assigned]


class TestAutoInitLowLevelKwOnlyDefaults:
    """Low-level KWARGS protocol tests for the KwOnlyDefaults type."""

    def test_low_level_positional_plus_kwargs_kw_only(self) -> None:
        """Positional arg for p_required, then KWARGS for kw_only fields."""
        obj = _TestCxxAutoInitKwOnlyDefaults.__new__(_TestCxxAutoInitKwOnlyDefaults)
        _ffi_init(obj, 1, core.KWARGS, "k_required", 2)
        assert obj.p_required == 1
        assert obj.p_default == 11
        assert obj.k_required == 2
        assert obj.k_default == 22
        assert obj.hidden == 33

    def test_low_level_two_positional_plus_kwargs(self) -> None:
        """Two positional args (p_required, p_default) then KWARGS for kw_only."""
        obj = _TestCxxAutoInitKwOnlyDefaults.__new__(_TestCxxAutoInitKwOnlyDefaults)
        _ffi_init(obj, 1, 2, core.KWARGS, "k_required", 3, "k_default", 4)
        assert obj.p_required == 1
        assert obj.p_default == 2
        assert obj.k_required == 3
        assert obj.k_default == 4
        assert obj.hidden == 33

    def test_low_level_all_via_kwargs(self) -> None:
        """All init=True fields via KWARGS, no positional."""
        obj = _TestCxxAutoInitKwOnlyDefaults.__new__(_TestCxxAutoInitKwOnlyDefaults)
        _ffi_init(
            obj, core.KWARGS, "p_required", 10, "p_default", 20, "k_required", 30, "k_default", 40
        )
        assert obj.p_required == 10
        assert obj.p_default == 20
        assert obj.k_required == 30
        assert obj.k_default == 40
        assert obj.hidden == 33


class TestClassLevelInitFalse:
    """init(false) passed to ObjectDef constructor suppresses __ffi_init__."""

    def test_no_ffi_init_method(self) -> None:
        type_index = getattr(_TestCxxNoAutoInit, "__tvm_ffi_type_info__").type_index
        ffi_init = core._lookup_type_attr(type_index, "__ffi_init__")
        assert ffi_init is None

    def test_has_fields(self) -> None:
        type_info = getattr(_TestCxxNoAutoInit, "__tvm_ffi_type_info__")
        field_names = [f.name for f in type_info.fields]
        assert field_names == ["x", "y"]

    def test_direct_construction_raises(self) -> None:
        with pytest.raises(TypeError, match="cannot be constructed directly"):
            _TestCxxNoAutoInit(1, 2)  # ty: ignore[too-many-positional-arguments]

    def test_has_shallow_copy(self) -> None:
        type_info = getattr(_TestCxxNoAutoInit, "__tvm_ffi_type_info__")
        # __ffi_shallow_copy__ is now TypeAttrColumn only, not a method
        fn = core._lookup_type_attr(type_info.type_index, "__ffi_shallow_copy__")
        assert fn is not None


# ###########################################################################
#  kw_only error message regression tests
# ###########################################################################


class TestKwOnlyErrorMessages:
    """Verify that error messages distinguish keyword-only from positional."""

    def test_missing_kw_only_error_says_keyword_only(self) -> None:
        """Missing required kw_only field should say 'keyword-only'."""
        with pytest.raises(TypeError, match="keyword-only"):
            _TestCxxAutoInit(1)  # ty: ignore[missing-argument]

    def test_missing_positional_error_does_not_say_keyword_only(self) -> None:
        """Missing required positional field should NOT say 'keyword-only'."""
        with pytest.raises(TypeError, match="missing required argument") as exc_info:
            _TestCxxAutoInit(c=3)  # ty: ignore[missing-argument]
        assert "keyword-only" not in str(exc_info.value)

    def test_child_missing_kw_only_error_says_keyword_only(self) -> None:
        """Inherited kw_only field should produce 'keyword-only' error."""
        with pytest.raises(TypeError, match="keyword-only"):
            _TestCxxAutoInitChild(parent_required=1, child_required=2)  # ty: ignore[missing-argument]

    def test_kw_only_defaults_missing_error(self) -> None:
        """KwOnlyDefaults type: missing k_required should say 'keyword-only'."""
        with pytest.raises(TypeError, match="keyword-only"):
            _TestCxxAutoInitKwOnlyDefaults(1)  # ty: ignore[missing-argument]

    def test_low_level_kw_only_error_says_keyword_only(self) -> None:
        """Low-level __ffi_init__ also gives 'keyword-only' error."""
        obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
        with pytest.raises(TypeError, match="keyword-only"):
            _ffi_init(obj, 1)


# ###########################################################################
#  __ffi_init__ TypeMethod registration regression tests
#
#  These tests verify that def(init<>) in C++ registers __ffi_init__ as a
#  TypeMethod (not just TypeAttrColumn), preserving type_schema metadata.
#  They also verify the __ffi_init__ instance method wrapper and the MISSING
#  sentinel used for default parameter values.
#  See commit 2987899a for the original fix.
# ###########################################################################


class TestFfiInitAsTypeMethod:
    """Verify that classes using ``def(init<>)`` expose __ffi_init__ as a TypeMethod."""

    @pytest.mark.parametrize("cls", [TestIntPair, TestCompare, TestHash])
    def test_explicit_init_has_ffi_init_type_method(self, cls: type) -> None:
        type_info = cls.__tvm_ffi_type_info__  # ty: ignore[unresolved-attribute]
        method_names = {m.name for m in type_info.methods}
        assert "__ffi_init__" in method_names

    def test_auto_init_does_not_have_ffi_init_type_method(self) -> None:
        type_info = _TestCxxAutoInit.__tvm_ffi_type_info__  # ty: ignore[unresolved-attribute]
        method_names = {m.name for m in type_info.methods}
        assert "__ffi_init__" not in method_names

    @pytest.mark.parametrize("cls", [TestIntPair, TestCompare, TestHash])
    def test_ffi_init_type_method_has_func(self, cls: type) -> None:
        type_info = cls.__tvm_ffi_type_info__  # ty: ignore[unresolved-attribute]
        for method in type_info.methods:
            if method.name == "__ffi_init__":
                assert method.func is not None
                break
        else:
            pytest.fail(f"__ffi_init__ not found in {cls.__name__} methods")  # ty: ignore[invalid-argument-type]


class TestFfiInitAsInstanceMethod:
    """Verify that __ffi_init__ is available as a callable method on registered classes."""

    def test_explicit_init_class_has_ffi_init_attr(self) -> None:
        assert hasattr(TestIntPair, "__ffi_init__")

    def test_auto_init_class_has_ffi_init_attr(self) -> None:
        assert hasattr(_TestCxxAutoInit, "__ffi_init__")

    def test_ffi_init_constructs_explicit_init_object(self) -> None:
        obj = TestIntPair.__new__(TestIntPair)
        obj.__ffi_init__(10, 20)  # ty: ignore[unresolved-attribute]
        assert obj.a == 10
        assert obj.b == 20
        assert obj.sum() == 30

    def test_ffi_init_constructs_auto_init_object(self) -> None:
        obj = _TestCxxAutoInitSimple.__new__(_TestCxxAutoInitSimple)
        obj.__ffi_init__(7, 8)  # ty: ignore[unresolved-attribute]
        assert obj.x == 7
        assert obj.y == 8

    def test_ffi_init_with_kwargs_protocol(self) -> None:
        obj = _TestCxxAutoInit.__new__(_TestCxxAutoInit)
        obj.__ffi_init__(a=1, c=3)  # ty: ignore[unresolved-attribute]
        assert obj.a == 1
        assert obj.b == 42
        assert obj.c == 3
        assert obj.d == 99


class TestFfiInitTypeOwnerGuard:
    """Verify the type-owner guard prevents subclass misuse of __ffi_init__."""

    def test_same_type_ffi_init_succeeds(self) -> None:
        obj = TestIntPair.__new__(TestIntPair)
        TestIntPair.__ffi_init__(obj, 1, 2)  # ty: ignore[unresolved-attribute]
        assert obj.a == 1
        assert obj.b == 2

    def test_subclass_ffi_init_raises_type_error(self) -> None:
        obj = _TestCxxClassDerivedDerived.__new__(_TestCxxClassDerivedDerived)
        with pytest.raises(TypeError, match="not supported"):
            _TestCxxClassDerived.__ffi_init__(obj, 1, 2, 3.0)  # ty: ignore[unresolved-attribute]


class TestMissingSentinelDefaults:
    """Verify that auto-generated __init__ signatures use core.MISSING for defaults."""

    def test_default_params_use_core_missing(self) -> None:
        sig = inspect.signature(_TestCxxAutoInit.__init__)
        d_param = sig.parameters["d"]
        assert d_param.default is not inspect.Parameter.empty
        assert d_param.default is core.MISSING

    def test_kw_only_default_uses_core_missing(self) -> None:
        sig = inspect.signature(_TestCxxAutoInitKwOnlyDefaults.__init__)
        assert sig.parameters["k_default"].default is core.MISSING
        assert sig.parameters["p_default"].default is core.MISSING

    def test_required_params_have_no_default(self) -> None:
        sig = inspect.signature(_TestCxxAutoInit.__init__)
        assert sig.parameters["a"].default is inspect.Parameter.empty
        assert sig.parameters["c"].default is inspect.Parameter.empty

    def test_missing_not_sent_to_ffi(self) -> None:
        """MISSING sentinel values should be stripped, not forwarded to C++."""
        obj = _TestCxxAutoInitKwOnlyDefaults(p_required=1, k_required=2)
        assert obj.p_default == 11
        assert obj.k_default == 22
        assert obj.hidden == 33

    def test_derived_class_defaults(self) -> None:
        obj = _TestCxxClassDerived(v_i64=1, v_i32=2, v_f64=3.0)
        assert obj.v_f32 == 8.0


class TestFfiInitDualRegistration:
    """Verify __ffi_init__ is available from both TypeMethod and TypeAttrColumn."""

    @pytest.mark.parametrize("cls", [TestIntPair, TestCompare])
    def test_explicit_init_in_type_attr_column(self, cls: type) -> None:
        type_info = cls.__tvm_ffi_type_info__  # ty: ignore[unresolved-attribute]
        ffi_init = core._lookup_type_attr(type_info.type_index, "__ffi_init__")
        assert ffi_init is not None

    def test_auto_init_in_type_attr_column(self) -> None:
        type_info = _TestCxxAutoInit.__tvm_ffi_type_info__  # ty: ignore[unresolved-attribute]
        ffi_init = core._lookup_type_attr(type_info.type_index, "__ffi_init__")
        assert ffi_init is not None

    def test_explicit_init_method_and_attr_produce_same_result(self) -> None:
        type_info = TestIntPair.__tvm_ffi_type_info__  # ty: ignore[unresolved-attribute]
        method_func = None
        for method in type_info.methods:
            if method.name == "__ffi_init__":
                method_func = method.func
                break
        assert method_func is not None
        attr_func = core._lookup_type_attr(type_info.type_index, "__ffi_init__")
        assert attr_func is not None
        obj1 = TestIntPair.__new__(TestIntPair)
        obj1.__init_handle_by_constructor__(method_func, 10, 20)
        obj2 = TestIntPair.__new__(TestIntPair)
        obj2.__init_handle_by_constructor__(attr_func, 10, 20)
        assert obj1.a == obj2.a == 10
        assert obj1.b == obj2.b == 20


# ###########################################################################
#  Python 3.14 annotation regression (PEP 749)
#
#  Python 3.14 stores annotations lazily via __annotate_func__ instead of
#  directly in cls.__dict__["__annotations__"].  This broke @py_class field
#  discovery when it used cls.__dict__.get("__annotations__", {}).
# ###########################################################################


@py_class("testing.PyClassSimple")
class _PyClassSimple(core.Object):
    x: int
    y: int


@py_class("testing.PyClassWithDefault")
class _PyClassWithDefault(core.Object):
    a: int
    b: int = 42


class TestPyClassAnnotationDiscovery:
    """Regression: @py_class must discover fields on Python 3.14+ (PEP 749)."""

    def test_fields_registered(self) -> None:
        ti: core.TypeInfo = _PyClassSimple.__tvm_ffi_type_info__  # type: ignore[unresolved-attribute]
        names = [f.name for f in ti.fields]
        assert names == ["x", "y"]

    def test_construct_kwargs(self) -> None:
        obj = _PyClassSimple(x=1, y=2)
        assert obj.x == 1
        assert obj.y == 2

    def test_construct_positional(self) -> None:
        obj = _PyClassSimple(10, 20)
        assert obj.x == 10
        assert obj.y == 20

    def test_default_field(self) -> None:
        obj = _PyClassWithDefault(a=7)
        assert obj.a == 7
        assert obj.b == 42

    def test_override_default(self) -> None:
        obj = _PyClassWithDefault(a=1, b=2)
        assert obj.a == 1
        assert obj.b == 2
