# 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 __ffi_repr__ / ffi.ReprPrint."""

from __future__ import annotations

import ast
import re

import numpy as np
import pytest
import tvm_ffi
import tvm_ffi.testing
from tvm_ffi._ffi_api import ReprPrint

# Regex building blocks
A = r"0x[0-9a-f]+"  # hex address


def _check(result: str, pattern: str) -> None:
    """Assert result matches pattern with re.fullmatch, with a clear error message."""
    assert re.fullmatch(pattern, result), (
        f"fullmatch failed:\n  result:  {result!r}\n  pattern: {pattern!r}"
    )


def test_repr_primitives() -> None:
    """Test repr of primitive types."""
    assert ReprPrint(42) == "42"
    assert ReprPrint(0) == "0"
    assert ReprPrint(-1) == "-1"
    assert ReprPrint(True) == "True"
    assert ReprPrint(False) == "False"
    assert ReprPrint(None) == "None"


def test_repr_float() -> None:
    """Test repr of floating point."""
    assert ReprPrint(3.14) == "3.14"
    assert ReprPrint(0.0) == "0"
    assert ReprPrint(1e10) == "1e+10"


def test_repr_string() -> None:
    """Test repr of FFI String (both SmallStr and StringObj)."""
    # SmallStr (<=7 bytes)
    assert ReprPrint("hello") == '"hello"'
    # StringObj (>7 bytes)
    assert ReprPrint("hello world") == '"hello world"'


# ---------- Array (tuple format) ----------


def test_repr_array() -> None:
    """Test repr of FFI Array uses tuple format."""
    assert ReprPrint(tvm_ffi.Array([1, 2, 3])) == "(1, 2, 3)"


def test_repr_array_single() -> None:
    """Test repr of single-element Array has trailing comma."""
    assert ReprPrint(tvm_ffi.Array([42])) == "(42,)"


def test_repr_array_empty() -> None:
    """Test repr of empty Array."""
    assert ReprPrint(tvm_ffi.Array([])) == "()"


def test_repr_array_nested_strings() -> None:
    """Test repr of Array containing strings."""
    assert ReprPrint(tvm_ffi.Array(["a", "b"])) == '("a", "b")'


def test_repr_array_python_repr() -> None:
    """Test that Array.__repr__ uses the centralized repr."""
    assert repr(tvm_ffi.Array([1, 2])) == "(1, 2)"


# ---------- List ----------


def test_repr_list() -> None:
    """Test repr of FFI List uses list format."""
    assert ReprPrint(tvm_ffi.List([10, 20])) == "[10, 20]"


def test_repr_list_single() -> None:
    """Test repr of single-element List (no trailing comma)."""
    assert ReprPrint(tvm_ffi.List([99])) == "[99]"


def test_repr_list_empty() -> None:
    """Test repr of empty List."""
    assert ReprPrint(tvm_ffi.List([])) == "[]"


def test_repr_list_nested_strings() -> None:
    """Test repr of List containing strings."""
    assert ReprPrint(tvm_ffi.List(["x", "y"])) == '["x", "y"]'


# ---------- Map ----------


def test_repr_map() -> None:
    """Test repr of FFI Map."""
    assert ReprPrint(tvm_ffi.Map({"key": "value"})) == '{"key": "value"}'


def test_repr_map_empty() -> None:
    """Test repr of empty Map."""
    assert ReprPrint(tvm_ffi.Map({})) == "{}"


# ---------- Dict ----------


def test_repr_dict() -> None:
    """Test repr of FFI Dict."""
    assert ReprPrint(tvm_ffi.Dict({"key": "value"})) == '{"key": "value"}'


def test_repr_dict_empty() -> None:
    """Test repr of empty Dict."""
    assert ReprPrint(tvm_ffi.Dict({})) == "{}"


def test_repr_dict_int_keys() -> None:
    """Test repr of Dict with integer keys."""
    d = tvm_ffi.Dict({1: 2, 3: 4})
    result = ReprPrint(d)
    # Dict iteration order is hash-dependent; match either ordering.
    _check(result, r"(?:\{1: 2, 3: 4\}|\{3: 4, 1: 2\})")


def test_repr_dict_with_array_values() -> None:
    """Test repr of Dict with Array values."""
    assert ReprPrint(tvm_ffi.Dict({1: tvm_ffi.Array([10, 20])})) == "{1: (10, 20)}"


def test_repr_dict_with_object_values() -> None:
    """Test repr of Dict with object values."""
    pair = tvm_ffi.testing.create_object("testing.TestIntPair", a=1, b=2)
    d = tvm_ffi.Dict({"obj": pair})
    assert ReprPrint(d) == '{"obj": testing.TestIntPair(a=1, b=2)}'


# ---------- Tensor ----------


def test_repr_tensor() -> None:
    """Test repr of Tensor shows dtype, shape, device (no address by default)."""
    x = tvm_ffi.from_dlpack(np.zeros((3, 4), dtype="float32"))
    assert ReprPrint(x) == "float32[3, 4]@cpu:0"


def test_repr_tensor_int8() -> None:
    """Test repr of Tensor with int8 dtype."""
    x = tvm_ffi.from_dlpack(np.zeros((2,), dtype="int8"))
    assert ReprPrint(x) == "int8[2]@cpu:0"


# ---------- Shape ----------


def test_repr_shape() -> None:
    """Test repr of Shape."""
    assert ReprPrint(tvm_ffi.Shape((5, 6))) == "Shape(5, 6)"


# ---------- User-defined objects ----------


def test_repr_user_object_all_fields() -> None:
    """Test repr of user-defined object with all fields shown (no address by default)."""
    obj = tvm_ffi.testing.create_object("testing.TestIntPair", a=10, b=20)
    assert ReprPrint(obj) == "testing.TestIntPair(a=10, b=20)"


def test_repr_user_object_repr_off() -> None:
    """Test repr of object with Repr(false) fields excluded."""
    # Positional order: required first (v_i64, v_i32, v_f64), then optional (v_f32)
    obj = tvm_ffi.testing._TestCxxClassDerived(1, 2, 3.5, 4.5)
    assert ReprPrint(obj) == "testing.TestCxxClassDerived(v_f64=3.5, v_f32=4.5)"


def test_repr_python_repr() -> None:
    """Test that Python __repr__ delegates to ReprPrint."""
    obj = tvm_ffi.testing.create_object("testing.TestIntPair", a=5, b=6)
    assert repr(obj) == "testing.TestIntPair(a=5, b=6)"


# ---------- DAG / shared references (full form on every occurrence) ----------


def test_repr_duplicate_reference() -> None:
    """Test that duplicate object references use full form on every occurrence."""
    inner = tvm_ffi.testing.create_object("testing.TestIntPair", a=1, b=2)
    arr = tvm_ffi.Array([inner, inner])
    result = ReprPrint(arr)
    assert result == "(testing.TestIntPair(a=1, b=2), testing.TestIntPair(a=1, b=2))"


def test_repr_shared_in_map_values() -> None:
    """Test that the same Array shared in Map values uses full form on both."""
    shared = tvm_ffi.Array([1, 2])
    m = tvm_ffi.Map({"a": shared, "b": shared})
    result = ReprPrint(m)
    # Map iteration order is hash-dependent; match either ordering.
    pat_ab = r'\{"a": \(1, 2\), "b": \(1, 2\)\}'
    pat_ba = r'\{"b": \(1, 2\), "a": \(1, 2\)\}'
    _check(result, rf"(?:{pat_ab}|{pat_ba})")


def test_repr_shared_across_nesting_levels() -> None:
    """Test shared object across different nesting levels uses full form everywhere."""
    leaf = tvm_ffi.testing.create_object("testing.TestIntPair", a=7, b=8)
    arr = tvm_ffi.Array([leaf, tvm_ffi.Array([leaf])])
    result = ReprPrint(arr)
    assert result == "(testing.TestIntPair(a=7, b=8), (testing.TestIntPair(a=7, b=8),))"


def test_repr_triple_shared_reference() -> None:
    """Test object appearing three times -- full form every time."""
    inner = tvm_ffi.testing.create_object("testing.TestIntPair", a=0, b=0)
    arr = tvm_ffi.Array([inner, inner, inner])
    result = ReprPrint(arr)
    assert result == (
        "(testing.TestIntPair(a=0, b=0), "
        "testing.TestIntPair(a=0, b=0), "
        "testing.TestIntPair(a=0, b=0))"
    )


# ---------- Nested containers ----------


def test_repr_array_of_arrays() -> None:
    """Test repr of Array containing Arrays."""
    inner = tvm_ffi.Array([1, 2])
    outer = tvm_ffi.Array([inner, tvm_ffi.Array([3])])
    assert ReprPrint(outer) == "((1, 2), (3,))"


def test_repr_map_of_containers() -> None:
    """Test repr of Map containing Array values."""
    m = tvm_ffi.Map({"a": tvm_ffi.Array([1, 2])})
    assert ReprPrint(m) == '{"a": (1, 2)}'


def test_repr_list_of_lists() -> None:
    """Test repr of List containing Lists."""
    inner = tvm_ffi.List([1, 2])
    outer = tvm_ffi.List([inner, tvm_ffi.List([3])])
    assert ReprPrint(outer) == "[[1, 2], [3]]"


# ---------- Nested dataclasses ----------


def test_repr_nested_dataclass() -> None:
    """Test repr of object with object-typed fields."""
    inner = tvm_ffi.testing.create_object("testing.TestIntPair", a=10, b=20)
    obj = tvm_ffi.testing.create_object(
        "testing.TestObjectDerived",
        v_i64=1,
        v_f64=2.5,
        v_str="hi",
        v_map=tvm_ffi.Map({}),
        v_array=tvm_ffi.Array([inner]),
    )
    assert ReprPrint(obj) == (
        'testing.TestObjectDerived(v_i64=1, v_f64=2.5, v_str="hi", '
        "v_map={}, "
        "v_array=(testing.TestIntPair(a=10, b=20),))"
    )


def test_repr_object_with_none_field() -> None:
    """Test repr of object where container fields are empty."""
    obj = tvm_ffi.testing.create_object(
        "testing.TestObjectDerived",
        v_i64=0,
        v_f64=0.0,
        v_str="",
        v_map=tvm_ffi.Map({}),
        v_array=tvm_ffi.Array([]),
    )
    assert (
        ReprPrint(obj)
        == 'testing.TestObjectDerived(v_i64=0, v_f64=0, v_str="", v_map={}, v_array=())'
    )


# ---------- Deep nesting ----------


def test_repr_deeply_nested_arrays() -> None:
    """Test repr of deeply nested Arrays (4 levels)."""
    a = tvm_ffi.Array([1])
    for _ in range(3):
        a = tvm_ffi.Array([a])
    assert ReprPrint(a) == "((((1,),),),)"


def test_repr_deeply_nested_lists() -> None:
    """Test repr of deeply nested Lists (4 levels)."""
    lst = tvm_ffi.List([1])
    for _ in range(3):
        lst = tvm_ffi.List([lst])
    assert ReprPrint(lst) == "[[[[1]]]]"


def test_repr_mixed_container_nesting() -> None:
    """Test repr of mixed Array/List/Map nesting."""
    inner_list = tvm_ffi.List([1, 2])
    inner_arr = tvm_ffi.Array([inner_list])
    m = tvm_ffi.Map({"nested": inner_arr})
    assert ReprPrint(m) == '{"nested": ([1, 2],)}'


# ---------- Shared reference patterns ----------


def test_repr_dataclass_shared_subobject() -> None:
    """Test repr of two dataclasses sharing the same sub-object (full form in both)."""
    shared = tvm_ffi.testing.create_object("testing.TestIntPair", a=5, b=5)
    obj1 = tvm_ffi.testing.create_object(
        "testing.TestObjectDerived",
        v_i64=1,
        v_f64=0.0,
        v_str="",
        v_map=tvm_ffi.Map({}),
        v_array=tvm_ffi.Array([shared]),
    )
    obj2 = tvm_ffi.testing.create_object(
        "testing.TestObjectDerived",
        v_i64=2,
        v_f64=0.0,
        v_str="",
        v_map=tvm_ffi.Map({}),
        v_array=tvm_ffi.Array([shared]),
    )
    arr = tvm_ffi.Array([obj1, obj2])
    result = ReprPrint(arr)
    assert result == (
        "("
        'testing.TestObjectDerived(v_i64=1, v_f64=0, v_str="", v_map={}, '
        "v_array=(testing.TestIntPair(a=5, b=5),)), "
        'testing.TestObjectDerived(v_i64=2, v_f64=0, v_str="", v_map={}, '
        "v_array=(testing.TestIntPair(a=5, b=5),))"
        ")"
    )


# ---------- Container with dataclass nesting ----------


def test_repr_array_of_dataclasses() -> None:
    """Test repr of Array of user-defined objects."""
    objs = [tvm_ffi.testing.create_object("testing.TestIntPair", a=i, b=i * 10) for i in range(3)]
    arr = tvm_ffi.Array(objs)
    assert ReprPrint(arr) == (
        "(testing.TestIntPair(a=0, b=0), "
        "testing.TestIntPair(a=1, b=10), "
        "testing.TestIntPair(a=2, b=20))"
    )


def test_repr_map_with_object_values() -> None:
    """Test repr of Map with object values."""
    pair = tvm_ffi.testing.create_object("testing.TestIntPair", a=1, b=2)
    m = tvm_ffi.Map({"obj": pair})
    assert ReprPrint(m) == '{"obj": testing.TestIntPair(a=1, b=2)}'


# ---------- Repr(false) inheritance ----------


def test_repr_derived_derived_shows_all_own_fields() -> None:
    """TestCxxClassDerivedDerived should show v_f64, v_f32, v_str, v_bool (not v_i64, v_i32)."""
    # Positional order: required (v_i64, v_i32, v_f64, v_bool), then optional (v_f32, v_str)
    obj = tvm_ffi.testing._TestCxxClassDerivedDerived(1, 2, 3.0, True, 4.0, "test")
    assert (
        ReprPrint(obj)
        == 'testing.TestCxxClassDerivedDerived(v_f64=3, v_f32=4, v_str="test", v_bool=True)'
    )


# ---------- Edge cases: special values ----------


def test_repr_large_integer() -> None:
    """Test repr of large integers."""
    assert ReprPrint(2**62) == str(2**62)
    assert ReprPrint(-(2**62)) == str(-(2**62))


def test_repr_negative_float() -> None:
    """Test repr of negative floats."""
    assert ReprPrint(-1.5) == "-1.5"


def test_repr_empty_string() -> None:
    """Test repr of empty string (SmallStr)."""
    assert ReprPrint("") == '""'


def test_repr_string_with_spaces() -> None:
    """Test repr of string with spaces."""
    assert ReprPrint("a b c") == '"a b c"'


def test_repr_array_of_none() -> None:
    """Test repr of Array containing None values."""
    assert ReprPrint(tvm_ffi.Array([None, None])) == "(None, None)"


def test_repr_array_of_booleans() -> None:
    """Test repr of Array containing boolean values."""
    assert ReprPrint(tvm_ffi.Array([True, False])) == "(True, False)"


def test_repr_array_of_mixed_types() -> None:
    """Test repr of Array containing mixed primitive types."""
    assert ReprPrint(tvm_ffi.Array([1, "hello", True, None])) == '(1, "hello", True, None)'


def test_repr_map_int_keys() -> None:
    """Test repr of Map with integer keys."""
    m = tvm_ffi.Map({1: 2, 3: 4})
    result = ReprPrint(m)
    # Map iteration order is hash-dependent; match either ordering.
    _check(result, r"(?:\{1: 2, 3: 4\}|\{3: 4, 1: 2\})")


def test_repr_map_with_array_values() -> None:
    """Test repr of Map with Array values."""
    assert ReprPrint(tvm_ffi.Map({1: tvm_ffi.Array([10, 20])})) == "{1: (10, 20)}"


# ---------- Nested dataclass edge cases ----------


def test_repr_dataclass_with_array_field() -> None:
    """Test repr of dataclass whose field is an Array of objects."""
    pair1 = tvm_ffi.testing.create_object("testing.TestIntPair", a=1, b=2)
    pair2 = tvm_ffi.testing.create_object("testing.TestIntPair", a=3, b=4)
    obj = tvm_ffi.testing.create_object(
        "testing.TestObjectDerived",
        v_i64=0,
        v_f64=0.0,
        v_str="test",
        v_map=tvm_ffi.Map({}),
        v_array=tvm_ffi.Array([pair1, pair2]),
    )
    assert ReprPrint(obj) == (
        'testing.TestObjectDerived(v_i64=0, v_f64=0, v_str="test", '
        "v_map={}, "
        "v_array=(testing.TestIntPair(a=1, b=2), testing.TestIntPair(a=3, b=4)))"
    )


def test_repr_dataclass_with_map_field() -> None:
    """Test repr of dataclass whose field is a Map."""
    obj = tvm_ffi.testing.create_object(
        "testing.TestObjectDerived",
        v_i64=42,
        v_f64=1.0,
        v_str="s",
        v_map=tvm_ffi.Map({"x": 10}),
        v_array=tvm_ffi.Array([]),
    )
    assert ReprPrint(obj) == (
        'testing.TestObjectDerived(v_i64=42, v_f64=1, v_str="s", v_map={"x": 10}, v_array=())'
    )


# ---------- Cycle detection ----------


def test_repr_self_reference_cycle() -> None:
    """Test that self-referencing cycles show '...' marker."""
    obj = tvm_ffi.testing.create_object(
        "testing.TestObjectDerived",
        v_i64=1,
        v_f64=2.0,
        v_str="hi",
        v_map=tvm_ffi.Map({}),
        v_array=tvm_ffi.Array([]),
    )
    obj.v_array = tvm_ffi.Array([obj])  # type: ignore[unresolved-attribute]
    result = ReprPrint(obj)
    assert result == (
        'testing.TestObjectDerived(v_i64=1, v_f64=2, v_str="hi", v_map={}, v_array=(...,))'
    )


def test_repr_mutual_reference_cycle() -> None:
    """Test that mutual reference cycles show '...' marker."""
    v_map = tvm_ffi.Map({})
    obj_a = tvm_ffi.testing.create_object(
        "testing.TestObjectDerived",
        v_i64=1,
        v_f64=0.0,
        v_str="a",
        v_map=v_map,
        v_array=tvm_ffi.Array([]),
    )
    obj_b = tvm_ffi.testing.create_object(
        "testing.TestObjectDerived",
        v_i64=2,
        v_f64=0.0,
        v_str="b",
        v_map=v_map,
        v_array=tvm_ffi.Array([obj_a]),
    )
    obj_a.v_array = tvm_ffi.Array([obj_b])  # type: ignore[unresolved-attribute]
    result = ReprPrint(obj_a)
    assert result == (
        'testing.TestObjectDerived(v_i64=1, v_f64=0, v_str="a", v_map={}, '
        "v_array=(testing.TestObjectDerived(v_i64=2, v_f64=0, "
        'v_str="b", v_map={}, v_array=(...,)),))'
    )


# ---------- TVM_FFI_REPR_WITH_ADDR ----------


def test_repr_with_addr_user_object(monkeypatch: pytest.MonkeyPatch) -> None:
    """Test that user objects show address when TVM_FFI_REPR_WITH_ADDR is set."""
    monkeypatch.setenv("TVM_FFI_REPR_WITH_ADDR", "1")
    obj = tvm_ffi.testing.create_object("testing.TestIntPair", a=1, b=2)
    _check(ReprPrint(obj), rf"testing\.TestIntPair@{A}\(a=1, b=2\)")


def test_repr_with_addr_array(monkeypatch: pytest.MonkeyPatch) -> None:
    """Test that Array shows address suffix when TVM_FFI_REPR_WITH_ADDR is set."""
    monkeypatch.setenv("TVM_FFI_REPR_WITH_ADDR", "1")
    arr = tvm_ffi.Array([1, 2, 3])
    _check(ReprPrint(arr), rf"\(1, 2, 3\)@{A}")


def test_repr_with_addr_list(monkeypatch: pytest.MonkeyPatch) -> None:
    """Test that List shows address suffix when TVM_FFI_REPR_WITH_ADDR is set."""
    monkeypatch.setenv("TVM_FFI_REPR_WITH_ADDR", "1")
    lst = tvm_ffi.List([10, 20])
    _check(ReprPrint(lst), rf"\[10, 20\]@{A}")


def test_repr_with_addr_dict(monkeypatch: pytest.MonkeyPatch) -> None:
    """Test that Dict shows address suffix when TVM_FFI_REPR_WITH_ADDR is set."""
    monkeypatch.setenv("TVM_FFI_REPR_WITH_ADDR", "1")
    d = tvm_ffi.Dict({"a": 1})
    _check(ReprPrint(d), rf'\{{"a": 1\}}@{A}')


def test_repr_with_addr_dag(monkeypatch: pytest.MonkeyPatch) -> None:
    """Test DAG with addresses: both occurrences show full form with same address."""
    monkeypatch.setenv("TVM_FFI_REPR_WITH_ADDR", "1")
    inner = tvm_ffi.testing.create_object("testing.TestIntPair", a=1, b=2)
    arr = tvm_ffi.Array([inner, inner])
    result = ReprPrint(arr)
    _check(
        result,
        rf"\(testing\.TestIntPair@(?P<a>{A})\(a=1, b=2\), "
        rf"testing\.TestIntPair@(?P=a)\(a=1, b=2\)\)@{A}",
    )


def test_repr_with_addr_cycle(monkeypatch: pytest.MonkeyPatch) -> None:
    """Test cycle with addresses: '...@ADDR' points back to the cyclic object."""
    monkeypatch.setenv("TVM_FFI_REPR_WITH_ADDR", "1")
    obj = tvm_ffi.testing.create_object(
        "testing.TestObjectDerived",
        v_i64=1,
        v_f64=0.0,
        v_str="",
        v_map=tvm_ffi.Map({}),
        v_array=tvm_ffi.Array([]),
    )
    obj.v_array = tvm_ffi.Array([obj])  # type: ignore[unresolved-attribute]
    result = ReprPrint(obj)
    _check(
        result,
        rf"testing\.TestObjectDerived@(?P<obj>{A})\("
        rf'v_i64=1, v_f64=0, v_str="", v_map=\{{\}}@{A}, '
        rf"v_array=\(\.\.\.@(?P=obj),\)@{A}"
        rf"\)",
    )


def test_repr_with_addr_tensor(monkeypatch: pytest.MonkeyPatch) -> None:
    """Test that Tensor shows address suffix when TVM_FFI_REPR_WITH_ADDR is set."""
    monkeypatch.setenv("TVM_FFI_REPR_WITH_ADDR", "1")
    x = tvm_ffi.from_dlpack(np.zeros((3, 4), dtype="float32"))
    _check(ReprPrint(x), rf"float32\[3, 4\]@cpu:0@{A}")


def test_repr_with_addr_no_fields(monkeypatch: pytest.MonkeyPatch) -> None:
    """Test that object with no visible fields shows TypeKey@ADDR with env var."""
    monkeypatch.setenv("TVM_FFI_REPR_WITH_ADDR", "1")
    # TestCxxClassBase has v_i64 and v_i32, both with Repr(false)
    obj = tvm_ffi.testing._TestCxxClassBase(v_i64=1, v_i32=2)
    _check(ReprPrint(obj), rf"testing\.TestCxxClassBase@{A}")


# ---------- Additional corner cases (fail-first) ----------


@pytest.mark.parametrize(
    "value",
    [
        'a"b',
        "a\\b",
        "\\",
        '"',
        "a\nb",
        "a\rb",
        "\x1b",
        "a\x00b",
    ],
)
def test_repr_string_literal_roundtrip_special_chars(value: str) -> None:
    """String repr should be parseable and round-trip through ast.literal_eval."""
    assert ast.literal_eval(ReprPrint(value)) == value


@pytest.mark.parametrize(
    ("value", "expected"),
    [
        (tvm_ffi.Array(['a"b']), ('a"b',)),
        (tvm_ffi.List(["a\\b"]), ["a\\b"]),
        (tvm_ffi.Array(["\\"]), ("\\",)),
        (tvm_ffi.Dict({"k": "a\nb"}), {"k": "a\nb"}),
        (tvm_ffi.Map({"k": "a\x00b"}), {"k": "a\x00b"}),
    ],
)
def test_repr_container_literal_roundtrip_special_strings(value: object, expected: object) -> None:
    """Container repr with string payloads should remain parseable literals."""
    assert ast.literal_eval(ReprPrint(value)) == expected


def test_repr_device_trn_name() -> None:
    """DLDeviceType.kDLTrn should print as trn:<id>, not unknown:<id>."""
    assert ReprPrint(tvm_ffi.Device("trn", 0)) == "trn:0"


def test_repr_unregistered_object_no_duplicate_field_names() -> None:
    """Inherited fields should not appear twice in generic repr."""
    obj = tvm_ffi.testing.make_unregistered_object()
    result = ReprPrint(obj)
    assert result.count("v1=") == 1


# --------------------------------------------------------------------------- #
#  @py_class repr
# --------------------------------------------------------------------------- #

import itertools as _itertools_repr
from typing import Optional as _Optional_repr

from tvm_ffi.core import Object as _Object_repr
from tvm_ffi.dataclasses import py_class as _py_class_repr

_counter_repr = _itertools_repr.count()


def _unique_key_repr(base: str) -> str:
    return f"testing.repr_pc.{base}_{next(_counter_repr)}"


def test_repr_py_class_base() -> None:
    """Repr of a simple @py_class contains field names and values."""

    @_py_class_repr(_unique_key_repr("ReprBase"))
    class ReprBase(_Object_repr):
        a: int
        b: str

    r = repr(ReprBase(a=1, b="hello"))
    assert "a=1" in r or "a: 1" in r
    assert "hello" in r


def test_repr_py_class_derived() -> None:
    """Repr of a derived @py_class shows all fields including parent."""

    @_py_class_repr(_unique_key_repr("ReprP"))
    class ReprP(_Object_repr):
        base_a: int
        base_b: str

    @_py_class_repr(_unique_key_repr("ReprD"))
    class ReprD(ReprP):
        derived_a: float
        derived_b: _Optional_repr[str]  # noqa: UP045

    r = repr(ReprD(base_a=1, base_b="b", derived_a=2.0, derived_b="c"))
    assert "1" in r
    assert "2" in r


def test_repr_py_class_in_array() -> None:
    """@py_class objects inside Array have proper repr."""

    @_py_class_repr(_unique_key_repr("ReprInArr"))
    class ReprInArr(_Object_repr):
        x: int

    r = repr(tvm_ffi.Array([ReprInArr(x=1), ReprInArr(x=2)]))
    assert "1" in r
    assert "2" in r


# ---------------------------------------------------------------------------
# Custom __ffi_repr__ hook via @py_class
# ---------------------------------------------------------------------------
from typing import Any as _Any_repr
from typing import Callable as _Callable_repr


def test_py_class_custom_ffi_repr() -> None:
    """ReprPrint dispatches the user-defined __ffi_repr__ hook."""

    @_py_class_repr(_unique_key_repr("CRepr"))
    class CRepr(_Object_repr):
        value: int

        def __ffi_repr__(self, fn_repr: _Callable_repr[..., _Any_repr]) -> str:
            return f"<CRepr:{self.value}>"

    assert ReprPrint(CRepr(42)) == "<CRepr:42>"
    assert ReprPrint(CRepr(999)) == "<CRepr:999>"


def test_py_class_ffi_repr_with_fields_and_copy() -> None:
    """Fields work normally and copy preserves __ffi_repr__ behaviour."""
    import copy as _copy_repr  # noqa: PLC0415

    @_py_class_repr(_unique_key_repr("FnR"))
    class FnR(_Object_repr):
        a: int
        b: str

        def __ffi_repr__(self, fn_repr: _Callable_repr[..., _Any_repr]) -> str:
            return f"FnR({self.a}, {self.b!r})"

    obj = FnR(10, "hi")
    assert obj.a == 10
    assert obj.b == "hi"
    assert ReprPrint(obj) == "FnR(10, 'hi')"
    obj2 = _copy_repr.copy(obj)
    assert ReprPrint(obj2) == "FnR(10, 'hi')"


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
