from __future__ import annotations

import contextlib
import sys
import types

import pytest

import env
from pybind11_tests import detailed_error_messages_enabled
from pybind11_tests import pytypes as m


def test_obj_class_name():
    assert m.obj_class_name(None) == "NoneType"
    assert m.obj_class_name(list) == "list"
    assert m.obj_class_name([]) == "list"


def test_handle_from_move_only_type_with_operator_PyObject():
    assert m.handle_from_move_only_type_with_operator_PyObject_ncnst()
    assert m.handle_from_move_only_type_with_operator_PyObject_const()


def test_bool(doc):
    assert doc(m.get_bool) == "get_bool() -> bool"


def test_int(doc):
    assert doc(m.get_int) == "get_int() -> int"


def test_iterator(doc):
    assert doc(m.get_iterator) == "get_iterator() -> collections.abc.Iterator"


@pytest.mark.parametrize(
    ("pytype", "from_iter_func"),
    [
        (frozenset, m.get_frozenset_from_iterable),
        (list, m.get_list_from_iterable),
        (set, m.get_set_from_iterable),
        (tuple, m.get_tuple_from_iterable),
    ],
)
def test_from_iterable(pytype, from_iter_func):
    my_iter = iter(range(10))
    s = from_iter_func(my_iter)
    assert type(s) == pytype
    assert s == pytype(range(10))


def test_iterable(doc):
    assert doc(m.get_iterable) == "get_iterable() -> collections.abc.Iterable"
    lst = [1, 2, 3]
    i = m.get_first_item_from_iterable(lst)
    assert i == 1
    i = m.get_second_item_from_iterable(lst)
    assert i == 2


def test_float(doc):
    assert doc(m.get_float) == "get_float() -> float"
    assert doc(m.float_roundtrip) == "float_roundtrip(arg0: float) -> float"
    f1 = m.float_roundtrip(5.5)
    assert isinstance(f1, float)
    assert f1 == 5.5
    f2 = m.float_roundtrip(5)
    assert isinstance(f2, float)
    assert f2 == 5.0


def test_list(capture, doc):
    assert m.list_no_args() == []
    assert m.list_ssize_t() == []
    assert m.list_size_t() == []
    lst = [1, 2]
    m.list_insert_ssize_t(lst)
    assert lst == [1, 83, 2]
    m.list_insert_size_t(lst)
    assert lst == [1, 83, 2, 57]
    m.list_clear(lst)
    assert lst == []

    with capture:
        lst = m.get_list()
        assert lst == ["inserted-0", "overwritten", "inserted-2"]

        lst.append("value2")
        m.print_list(lst)
    assert (
        capture.unordered
        == """
        Entry at position 0: value
        list item 0: inserted-0
        list item 1: overwritten
        list item 2: inserted-2
        list item 3: value2
    """
    )

    assert doc(m.get_list) == "get_list() -> list"
    assert doc(m.print_list) == "print_list(arg0: list) -> None"


def test_none(doc):
    assert doc(m.get_none) == "get_none() -> None"
    assert doc(m.print_none) == "print_none(arg0: None) -> None"


def test_set(capture, doc):
    s = m.get_set()
    assert isinstance(s, set)
    assert s == {"key1", "key2", "key3"}

    s.add("key4")
    with capture:
        m.print_anyset(s)
    assert (
        capture.unordered
        == """
        key: key1
        key: key2
        key: key3
        key: key4
    """
    )

    m.set_add(s, "key5")
    assert m.anyset_size(s) == 5

    m.set_clear(s)
    assert m.anyset_empty(s)

    assert not m.anyset_contains(set(), 42)
    assert m.anyset_contains({42}, 42)
    assert m.anyset_contains({"foo"}, "foo")

    assert doc(m.get_set) == "get_set() -> set"
    assert doc(m.print_anyset) == "print_anyset(arg0: set | frozenset) -> None"


def test_frozenset(capture, doc):
    s = m.get_frozenset()
    assert isinstance(s, frozenset)
    assert s == frozenset({"key1", "key2", "key3"})

    with capture:
        m.print_anyset(s)
    assert (
        capture.unordered
        == """
        key: key1
        key: key2
        key: key3
    """
    )
    assert m.anyset_size(s) == 3
    assert not m.anyset_empty(s)

    assert not m.anyset_contains(frozenset(), 42)
    assert m.anyset_contains(frozenset({42}), 42)
    assert m.anyset_contains(frozenset({"foo"}), "foo")

    assert doc(m.get_frozenset) == "get_frozenset() -> frozenset"


def test_dict(capture, doc):
    d = m.get_dict()
    assert d == {"key": "value"}

    with capture:
        d["key2"] = "value2"
        m.print_dict(d)
    assert (
        capture.unordered
        == """
        key: key, value=value
        key: key2, value=value2
    """
    )

    assert not m.dict_contains({}, 42)
    assert m.dict_contains({42: None}, 42)
    assert m.dict_contains({"foo": None}, "foo")

    assert doc(m.get_dict) == "get_dict() -> dict"
    assert doc(m.print_dict) == "print_dict(arg0: dict) -> None"

    assert m.dict_keyword_constructor() == {"x": 1, "y": 2, "z": 3}


class CustomContains:
    d = {"key": None}

    def __contains__(self, m):
        return m in self.d


@pytest.mark.parametrize(
    ("arg", "func"),
    [
        (set(), m.anyset_contains),
        ({}, m.dict_contains),
        (CustomContains(), m.obj_contains),
    ],
)
def test_unhashable_exceptions(arg, func):
    class Unhashable:
        __hash__ = None

    with pytest.raises(TypeError) as exc_info:
        func(arg, Unhashable())
    assert "unhashable type:" in str(exc_info.value)


def test_tuple():
    assert m.tuple_no_args() == ()
    assert m.tuple_ssize_t() == ()
    assert m.tuple_size_t() == ()
    assert m.get_tuple() == (42, None, "spam")


def test_simple_namespace():
    ns = m.get_simple_namespace()
    assert ns.attr == 42
    assert ns.x == "foo"
    assert ns.right == 2
    assert not hasattr(ns, "wrong")


def test_str(doc):
    assert m.str_from_char_ssize_t().encode().decode() == "red"
    assert m.str_from_char_size_t().encode().decode() == "blue"
    assert m.str_from_string().encode().decode() == "baz"
    assert m.str_from_bytes().encode().decode() == "boo"

    assert doc(m.str_from_bytes) == "str_from_bytes() -> str"

    class A:
        def __str__(self):
            return "this is a str"

        def __repr__(self):
            return "this is a repr"

    assert m.str_from_object(A()) == "this is a str"
    assert m.repr_from_object(A()) == "this is a repr"
    assert m.str_from_handle(A()) == "this is a str"

    s1, s2 = m.str_format()
    assert s1 == "1 + 2 = 3"
    assert s1 == s2

    malformed_utf8 = b"\x80"
    if hasattr(m, "PYBIND11_STR_LEGACY_PERMISSIVE"):
        assert m.str_from_object(malformed_utf8) is malformed_utf8
    else:
        assert m.str_from_object(malformed_utf8) == "b'\\x80'"
    assert m.str_from_handle(malformed_utf8) == "b'\\x80'"

    assert m.str_from_string_from_str("this is a str") == "this is a str"
    ucs_surrogates_str = "\udcc3"
    with pytest.raises(UnicodeEncodeError):
        m.str_from_string_from_str(ucs_surrogates_str)


@pytest.mark.parametrize(
    "func",
    [
        m.str_from_bytes_input,
        m.str_from_cstr_input,
        m.str_from_std_string_input,
    ],
)
@pytest.mark.xfail("env.GRAALPY", reason="TODO should be fixed on GraalPy side")
def test_surrogate_pairs_unicode_error(func):
    input_str = "\ud83d\ude4f".encode("utf-8", "surrogatepass")
    with pytest.raises(UnicodeDecodeError):
        func(input_str)


def test_bytes(doc):
    assert m.bytes_from_char_ssize_t().decode() == "green"
    assert m.bytes_from_char_size_t().decode() == "purple"
    assert m.bytes_from_string().decode() == "foo"
    assert m.bytes_from_str().decode() == "bar"

    assert doc(m.bytes_from_str) == "bytes_from_str() -> bytes"


def test_bytearray():
    assert m.bytearray_from_char_ssize_t().decode() == "$%"
    assert m.bytearray_from_char_size_t().decode() == "@$!"
    assert m.bytearray_from_string().decode() == "foo"
    assert m.bytearray_size() == len("foo")


def test_capsule(capture):
    pytest.gc_collect()
    with capture:
        a = m.return_capsule_with_destructor()
        del a
        pytest.gc_collect()
    assert (
        capture.unordered
        == """
        creating capsule
        destructing capsule
    """
    )

    with capture:
        a = m.return_renamed_capsule_with_destructor()
        del a
        pytest.gc_collect()
    assert (
        capture.unordered
        == """
        creating capsule
        renaming capsule
        destructing capsule
    """
    )

    with capture:
        a = m.return_capsule_with_destructor_2()
        del a
        pytest.gc_collect()
    assert (
        capture.unordered
        == """
        creating capsule
        destructing capsule: 1234
    """
    )

    with capture:
        a = m.return_capsule_with_destructor_3()
        del a
        pytest.gc_collect()
    assert (
        capture.unordered
        == """
        creating capsule
        destructing capsule: 1233
        original name: oname
    """
    )

    with capture:
        a = m.return_renamed_capsule_with_destructor_2()
        del a
        pytest.gc_collect()
    assert (
        capture.unordered
        == """
        creating capsule
        renaming capsule
        destructing capsule: 1234
    """
    )

    with capture:
        a = m.return_capsule_with_name_and_destructor()
        del a
        pytest.gc_collect()
    assert (
        capture.unordered
        == """
        created capsule (1234, 'pointer type description')
        destructing capsule (1234, 'pointer type description')
    """
    )

    with capture:
        a = m.return_capsule_with_explicit_nullptr_dtor()
        del a
        pytest.gc_collect()
    assert (
        capture.unordered
        == """
        creating capsule with explicit nullptr dtor
    """
    )


def test_accessors():
    class SubTestObject:
        attr_obj = 1
        attr_char = 2

    class TestObject:
        basic_attr = 1
        begin_end = [1, 2, 3]
        d = {"operator[object]": 1, "operator[char *]": 2}
        sub = SubTestObject()

        def func(self, x, *args):
            return self.basic_attr + x + sum(args)

    d = m.accessor_api(TestObject())
    assert d["basic_attr"] == 1
    assert d["begin_end"] == [1, 2, 3]
    assert d["operator[object]"] == 1
    assert d["operator[char *]"] == 2
    assert d["attr(object)"] == 1
    assert d["attr(char *)"] == 2
    assert d["missing_attr_ptr"] == "raised"
    assert d["missing_attr_chain"] == "raised"
    assert d["is_none"] is False
    assert d["operator()"] == 2
    assert d["operator*"] == 7
    assert d["implicit_list"] == [1, 2, 3]
    assert all(x in TestObject.__dict__ for x in d["implicit_dict"])

    assert m.tuple_accessor(()) == (0, 1, 2)

    d = m.accessor_assignment()
    assert d["get"] == 0
    assert d["deferred_get"] == 0
    assert d["set"] == 1
    assert d["deferred_set"] == 1
    assert d["var"] == 99


def test_accessor_moves():
    inc_refs = m.accessor_moves()
    if inc_refs:
        assert inc_refs == [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0]
    else:
        pytest.skip("Not defined: PYBIND11_HANDLE_REF_DEBUG")


@pytest.mark.xfail("env.GRAALPY", reason="TODO should be fixed on GraalPy side")
def test_constructors():
    """C++ default and converting constructors are equivalent to type calls in Python"""
    types = [bytes, bytearray, str, bool, int, float, tuple, list, dict, set]
    expected = {t.__name__: t() for t in types}
    assert m.default_constructors() == expected

    data = {
        bytes: b"41",  # Currently no supported or working conversions.
        bytearray: bytearray(b"41"),
        str: 42,
        bool: "Not empty",
        int: "42",
        float: "+1e3",
        tuple: range(3),
        list: range(3),
        dict: [("two", 2), ("one", 1), ("three", 3)],
        set: [4, 4, 5, 6, 6, 6],
        frozenset: [4, 4, 5, 6, 6, 6],
        memoryview: b"abc",
    }
    inputs = {k.__name__: v for k, v in data.items()}
    expected = {k.__name__: k(v) for k, v in data.items()}

    assert m.converting_constructors(inputs) == expected
    assert m.cast_functions(inputs) == expected

    # Converting constructors and cast functions should just reference rather
    # than copy when no conversion is needed:
    noconv1 = m.converting_constructors(expected)
    for k in noconv1:
        assert noconv1[k] is expected[k]

    noconv2 = m.cast_functions(expected)
    for k in noconv2:
        assert noconv2[k] is expected[k]


def test_non_converting_constructors():
    non_converting_test_cases = [
        ("bytes", range(10)),
        ("none", 42),
        ("ellipsis", 42),
        ("type", 42),
    ]
    for t, v in non_converting_test_cases:
        for move in [True, False]:
            with pytest.raises(TypeError) as excinfo:
                m.nonconverting_constructor(t, v, move)
            expected_error = (
                f"Object of type '{type(v).__name__}' is not an instance of '{t}'"
            )
            assert str(excinfo.value) == expected_error


def test_pybind11_str_raw_str():
    # specifically to exercise pybind11::str::raw_str
    cvt = m.convert_to_pybind11_str
    assert cvt("Str") == "Str"
    assert cvt(b"Bytes") == "b'Bytes'"
    assert cvt(None) == "None"
    assert cvt(False) == "False"
    assert cvt(True) == "True"
    assert cvt(42) == "42"
    assert cvt(2**65) == "36893488147419103232"
    assert cvt(-1.50) == "-1.5"
    assert cvt(()) == "()"
    assert cvt((18,)) == "(18,)"
    assert cvt([]) == "[]"
    assert cvt([28]) == "[28]"
    assert cvt({}) == "{}"
    assert cvt({3: 4}) == "{3: 4}"
    assert cvt(set()) == "set()"
    assert cvt({3}) == "{3}"

    valid_orig = "Ǳ"
    valid_utf8 = valid_orig.encode("utf-8")
    valid_cvt = cvt(valid_utf8)
    if hasattr(m, "PYBIND11_STR_LEGACY_PERMISSIVE"):
        assert valid_cvt is valid_utf8
    else:
        assert type(valid_cvt) is str
        assert valid_cvt == "b'\\xc7\\xb1'"

    malformed_utf8 = b"\x80"
    if hasattr(m, "PYBIND11_STR_LEGACY_PERMISSIVE"):
        assert cvt(malformed_utf8) is malformed_utf8
    else:
        malformed_cvt = cvt(malformed_utf8)
        assert type(malformed_cvt) is str
        assert malformed_cvt == "b'\\x80'"


def test_implicit_casting():
    """Tests implicit casting when assigning or appending to dicts and lists."""
    z = m.get_implicit_casting()
    assert z["d"] == {
        "char*_i1": "abc",
        "char*_i2": "abc",
        "char*_e": "abc",
        "char*_p": "abc",
        "str_i1": "str",
        "str_i2": "str1",
        "str_e": "str2",
        "str_p": "str3",
        "int_i1": 42,
        "int_i2": 42,
        "int_e": 43,
        "int_p": 44,
    }
    assert z["l"] == [3, 6, 9, 12, 15]


def test_print(capture):
    with capture:
        m.print_function()
    assert (
        capture
        == """
        Hello, World!
        1 2.0 three True -- multiple args
        *args-and-a-custom-separator
        no new line here -- next print
        flush
        py::print + str.format = this
    """
    )
    assert capture.stderr == "this goes to stderr"

    with pytest.raises(RuntimeError) as excinfo:
        m.print_failure()
    assert str(excinfo.value) == "Unable to convert call argument " + (
        "'1' of type 'UnregisteredType' to Python object"
        if detailed_error_messages_enabled
        else "'1' to Python object (#define PYBIND11_DETAILED_ERROR_MESSAGES or compile in debug mode for details)"
    )


def test_hash():
    class Hashable:
        def __init__(self, value):
            self.value = value

        def __hash__(self):
            return self.value

    class Unhashable:
        __hash__ = None

    assert m.hash_function(Hashable(42)) == 42
    with pytest.raises(TypeError):
        m.hash_function(Unhashable())


def test_number_protocol():
    for a, b in [(1, 1), (3, 5)]:
        li = [
            a == b,
            a != b,
            a < b,
            a <= b,
            a > b,
            a >= b,
            a + b,
            a - b,
            a * b,
            a / b,
            a | b,
            a & b,
            a ^ b,
            a >> b,
            a << b,
        ]
        assert m.test_number_protocol(a, b) == li


def test_list_slicing():
    li = list(range(100))
    assert li[0:-1:2] == m.test_list_slicing(li)
    assert li[::] == m.test_list_slicing_default(li)


def test_issue2361():
    # See issue #2361
    assert m.issue2361_str_implicit_copy_none() == "None"
    with pytest.raises(TypeError) as excinfo:
        assert m.issue2361_dict_implicit_copy_none()
    assert "NoneType" in str(excinfo.value)
    assert "iterable" in str(excinfo.value)


@pytest.mark.parametrize(
    ("method", "args", "fmt", "expected_view"),
    [
        (m.test_memoryview_object, (b"red",), "B", b"red"),
        (m.test_memoryview_buffer_info, (b"green",), "B", b"green"),
        (m.test_memoryview_from_buffer, (False,), "h", [3, 1, 4, 1, 5]),
        (m.test_memoryview_from_buffer, (True,), "H", [2, 7, 1, 8]),
        (m.test_memoryview_from_buffer_nativeformat, (), "@i", [4, 7, 5]),
    ],
)
def test_memoryview(method, args, fmt, expected_view):
    view = method(*args)
    assert isinstance(view, memoryview)
    assert view.format == fmt
    assert list(view) == list(expected_view)


@pytest.mark.xfail("env.PYPY", reason="getrefcount is not available")
@pytest.mark.parametrize(
    "method",
    [
        m.test_memoryview_object,
        m.test_memoryview_buffer_info,
    ],
)
def test_memoryview_refcount(method):
    # Avoiding a literal to avoid an immortal object in free-threaded builds
    buf = "\x0a\x0b\x0c\x0d".encode("ascii")
    ref_before = sys.getrefcount(buf)
    view = method(buf)
    ref_after = sys.getrefcount(buf)
    assert ref_before < ref_after
    assert list(view) == list(buf)


def test_memoryview_from_buffer_empty_shape():
    view = m.test_memoryview_from_buffer_empty_shape()
    assert isinstance(view, memoryview)
    assert view.format == "B"
    assert bytes(view) == b""


def test_test_memoryview_from_buffer_invalid_strides():
    with pytest.raises(RuntimeError):
        m.test_memoryview_from_buffer_invalid_strides()


def test_test_memoryview_from_buffer_nullptr():
    with pytest.raises(ValueError):
        m.test_memoryview_from_buffer_nullptr()


def test_memoryview_from_memory():
    view = m.test_memoryview_from_memory()
    assert isinstance(view, memoryview)
    assert view.format == "B"
    assert bytes(view) == b"\xff\xe1\xab\x37"


def test_builtin_functions():
    assert m.get_len(list(range(42))) == 42
    with pytest.raises(TypeError) as exc_info:
        m.get_len(i for i in range(42))
    assert str(exc_info.value) in [
        "object of type 'generator' has no len()",
        "'generator' has no length",
    ]  # PyPy


def test_isinstance_string_types():
    assert m.isinstance_pybind11_bytes(b"")
    assert not m.isinstance_pybind11_bytes("")

    assert m.isinstance_pybind11_str("")
    if hasattr(m, "PYBIND11_STR_LEGACY_PERMISSIVE"):
        assert m.isinstance_pybind11_str(b"")
    else:
        assert not m.isinstance_pybind11_str(b"")


def test_pass_bytes_or_unicode_to_string_types():
    assert m.pass_to_pybind11_bytes(b"Bytes") == 5
    with pytest.raises(TypeError):
        m.pass_to_pybind11_bytes("Str")

    if hasattr(m, "PYBIND11_STR_LEGACY_PERMISSIVE"):
        assert m.pass_to_pybind11_str(b"Bytes") == 5
    else:
        with pytest.raises(TypeError):
            m.pass_to_pybind11_str(b"Bytes")
    assert m.pass_to_pybind11_str("Str") == 3

    assert m.pass_to_std_string(b"Bytes") == 5
    assert m.pass_to_std_string("Str") == 3

    malformed_utf8 = b"\x80"
    if hasattr(m, "PYBIND11_STR_LEGACY_PERMISSIVE"):
        assert m.pass_to_pybind11_str(malformed_utf8) == 1
    else:
        with pytest.raises(TypeError):
            m.pass_to_pybind11_str(malformed_utf8)


@pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC")
@pytest.mark.parametrize(
    ("create_weakref", "create_weakref_with_callback"),
    [
        (m.weakref_from_handle, m.weakref_from_handle_and_function),
        (m.weakref_from_object, m.weakref_from_object_and_function),
    ],
)
def test_weakref(create_weakref, create_weakref_with_callback):
    from weakref import getweakrefcount

    # Apparently, you cannot weakly reference an object()
    class WeaklyReferenced:
        pass

    callback_called = False

    def callback(_):
        nonlocal callback_called
        callback_called = True

    obj = WeaklyReferenced()
    assert getweakrefcount(obj) == 0
    wr = create_weakref(obj)
    assert getweakrefcount(obj) == 1

    obj = WeaklyReferenced()
    assert getweakrefcount(obj) == 0
    wr = create_weakref_with_callback(obj, callback)  # noqa: F841
    assert getweakrefcount(obj) == 1
    assert not callback_called
    del obj
    pytest.gc_collect()
    assert callback_called


@pytest.mark.parametrize(
    ("create_weakref", "has_callback"),
    [
        (m.weakref_from_handle, False),
        (m.weakref_from_object, False),
        (m.weakref_from_handle_and_function, True),
        (m.weakref_from_object_and_function, True),
    ],
)
def test_weakref_err(create_weakref, has_callback):
    class C:
        __slots__ = []

    def callback(_):
        pass

    ob = C()
    # Should raise TypeError on CPython
    cm = pytest.raises(TypeError)
    if env.PYPY or env.GRAALPY:
        cm = contextlib.nullcontext()
    with cm:
        _ = create_weakref(ob, callback) if has_callback else create_weakref(ob)


def test_cpp_iterators():
    assert m.tuple_iterator() == 12
    assert m.dict_iterator() == 305 + 711
    assert m.passed_iterator(iter((-7, 3))) == -4


def test_implementation_details():
    lst = [39, 43, 92, 49, 22, 29, 93, 98, 26, 57, 8]
    tup = tuple(lst)
    assert m.sequence_item_get_ssize_t(lst) == 43
    assert m.sequence_item_set_ssize_t(lst) is None
    assert lst[1] == "peppa"
    assert m.sequence_item_get_size_t(lst) == 92
    assert m.sequence_item_set_size_t(lst) is None
    assert lst[2] == "george"
    assert m.list_item_get_ssize_t(lst) == 49
    assert m.list_item_set_ssize_t(lst) is None
    assert lst[3] == "rebecca"
    assert m.list_item_get_size_t(lst) == 22
    assert m.list_item_set_size_t(lst) is None
    assert lst[4] == "richard"
    assert m.tuple_item_get_ssize_t(tup) == 29
    assert m.tuple_item_set_ssize_t() == ("emely", "edmond")
    assert m.tuple_item_get_size_t(tup) == 93
    assert m.tuple_item_set_size_t() == ("candy", "cat")


def test_external_float_():
    r1 = m.square_float_(2.0)
    assert r1 == 4.0


def test_tuple_rvalue_getter():
    pop = 1000
    tup = tuple(range(pop))
    m.tuple_rvalue_getter(tup)


def test_list_rvalue_getter():
    pop = 1000
    my_list = list(range(pop))
    m.list_rvalue_getter(my_list)


def test_populate_dict_rvalue():
    pop = 1000
    my_dict = {i: i for i in range(pop)}
    assert m.populate_dict_rvalue(pop) == my_dict


def test_populate_obj_str_attrs():
    pop = 1000
    o = types.SimpleNamespace(**{str(i): i for i in range(pop)})
    new_o = m.populate_obj_str_attrs(o, pop)
    new_attrs = {k: v for k, v in new_o.__dict__.items() if not k.startswith("_")}
    assert all(isinstance(v, str) for v in new_attrs.values())
    assert len(new_attrs) == pop


@pytest.mark.parametrize(
    ("a", "b"),
    [("foo", "bar"), (1, 2), (1.0, 2.0), (list(range(3)), list(range(3, 6)))],
)
def test_inplace_append(a, b):
    expected = a + b
    assert m.inplace_append(a, b) == expected


@pytest.mark.parametrize(
    ("a", "b"), [(3, 2), (3.0, 2.0), (set(range(3)), set(range(2)))]
)
def test_inplace_subtract(a, b):
    expected = a - b
    assert m.inplace_subtract(a, b) == expected


@pytest.mark.parametrize(("a", "b"), [(3, 2), (3.0, 2.0), ([1], 3)])
def test_inplace_multiply(a, b):
    expected = a * b
    assert m.inplace_multiply(a, b) == expected


@pytest.mark.parametrize(("a", "b"), [(6, 3), (6.0, 3.0)])
def test_inplace_divide(a, b):
    expected = a / b
    assert m.inplace_divide(a, b) == expected


@pytest.mark.parametrize(
    ("a", "b"),
    [
        (False, True),
        (
            set(),
            {
                1,
            },
        ),
    ],
)
def test_inplace_or(a, b):
    expected = a | b
    assert m.inplace_or(a, b) == expected


@pytest.mark.parametrize(
    ("a", "b"),
    [
        (True, False),
        (
            {1, 2, 3},
            {
                1,
            },
        ),
    ],
)
def test_inplace_and(a, b):
    expected = a & b
    assert m.inplace_and(a, b) == expected


@pytest.mark.parametrize(("a", "b"), [(8, 1), (-3, 2)])
def test_inplace_lshift(a, b):
    expected = a << b
    assert m.inplace_lshift(a, b) == expected


@pytest.mark.parametrize(("a", "b"), [(8, 1), (-2, 2)])
def test_inplace_rshift(a, b):
    expected = a >> b
    assert m.inplace_rshift(a, b) == expected


def test_tuple_nonempty_annotations(doc):
    assert (
        doc(m.annotate_tuple_float_str)
        == "annotate_tuple_float_str(arg0: tuple[float, str]) -> None"
    )


def test_tuple_empty_annotations(doc):
    assert (
        doc(m.annotate_tuple_empty) == "annotate_tuple_empty(arg0: tuple[()]) -> None"
    )


def test_tuple_variable_length_annotations(doc):
    assert (
        doc(m.annotate_tuple_variable_length)
        == "annotate_tuple_variable_length(arg0: tuple[float, ...]) -> None"
    )


def test_dict_annotations(doc):
    assert (
        doc(m.annotate_dict_str_int)
        == "annotate_dict_str_int(arg0: dict[str, typing.SupportsInt | typing.SupportsIndex]) -> None"
    )


def test_list_annotations(doc):
    assert (
        doc(m.annotate_list_int)
        == "annotate_list_int(arg0: list[typing.SupportsInt | typing.SupportsIndex]) -> None"
    )


def test_set_annotations(doc):
    assert doc(m.annotate_set_str) == "annotate_set_str(arg0: set[str]) -> None"


def test_iterable_annotations(doc):
    assert (
        doc(m.annotate_iterable_str)
        == "annotate_iterable_str(arg0: collections.abc.Iterable[str]) -> None"
    )


def test_iterator_annotations(doc):
    assert (
        doc(m.annotate_iterator_int)
        == "annotate_iterator_int(arg0: collections.abc.Iterator[typing.SupportsInt | typing.SupportsIndex]) -> None"
    )


def test_fn_annotations(doc):
    assert (
        doc(m.annotate_fn)
        == "annotate_fn(arg0: collections.abc.Callable[[list[str], str], int]) -> None"
    )


def test_fn_return_only(doc):
    assert (
        doc(m.annotate_fn_only_return)
        == "annotate_fn_only_return(arg0: collections.abc.Callable[..., int]) -> None"
    )


def test_type_annotation(doc):
    assert (
        doc(m.annotate_type)
        == "annotate_type(arg0: type[typing.SupportsInt | typing.SupportsIndex]) -> type"
    )


def test_union_annotations(doc):
    assert (
        doc(m.annotate_union)
        == "annotate_union(arg0: list[str | int | object], arg1: str, arg2: int, arg3: object) -> list[str | int | object]"
    )


def test_union_typing_only(doc):
    assert doc(m.union_typing_only) == "union_typing_only(arg0: list[str]) -> list[int]"


def test_union_object_annotations(doc):
    assert (
        doc(m.annotate_union_to_object)
        == "annotate_union_to_object(arg0: typing.SupportsInt | typing.SupportsIndex | str) -> object"
    )


def test_optional_annotations(doc):
    assert (
        doc(m.annotate_optional) == "annotate_optional(arg0: list) -> list[str | None]"
    )


def test_type_guard_annotations(doc, backport_typehints):
    assert (
        backport_typehints(doc(m.annotate_type_guard))
        == "annotate_type_guard(arg0: object) -> typing.TypeGuard[str]"
    )


def test_type_is_annotations(doc, backport_typehints):
    assert (
        backport_typehints(doc(m.annotate_type_is))
        == "annotate_type_is(arg0: object) -> typing.TypeIs[str]"
    )


def test_no_return_annotation(doc):
    assert doc(m.annotate_no_return) == "annotate_no_return() -> typing.NoReturn"


def test_never_annotation(doc, backport_typehints):
    assert (
        backport_typehints(doc(m.annotate_never)) == "annotate_never() -> typing.Never"
    )


def test_optional_object_annotations(doc):
    assert (
        doc(m.annotate_optional_to_object)
        == "annotate_optional_to_object(arg0: typing.SupportsInt | typing.SupportsIndex | None) -> object"
    )


@pytest.mark.skipif(
    not m.defined_PYBIND11_TYPING_H_HAS_STRING_LITERAL,
    reason="C++20 non-type template args feature not available.",
)
def test_literal(doc):
    assert (
        doc(m.annotate_literal)
        == 'annotate_literal(arg0: typing.Literal[26, 0x1A, "hello world", b"hello world", u"hello world", True, Color.RED, None]) -> object'
    )
    # The characters !, @, %, {, } and -> are used in the signature parser as special characters, but Literal should escape those for the parser to work.
    assert (
        doc(m.identity_literal_exclamation)
        == 'identity_literal_exclamation(arg0: typing.Literal["!"]) -> typing.Literal["!"]'
    )
    assert (
        doc(m.identity_literal_at)
        == 'identity_literal_at(arg0: typing.Literal["@"]) -> typing.Literal["@"]'
    )
    assert (
        doc(m.identity_literal_percent)
        == 'identity_literal_percent(arg0: typing.Literal["%"]) -> typing.Literal["%"]'
    )
    assert (
        doc(m.identity_literal_curly_open)
        == 'identity_literal_curly_open(arg0: typing.Literal["{"]) -> typing.Literal["{"]'
    )
    assert (
        doc(m.identity_literal_curly_close)
        == 'identity_literal_curly_close(arg0: typing.Literal["}"]) -> typing.Literal["}"]'
    )
    assert (
        doc(m.identity_literal_arrow_with_io_name)
        == 'identity_literal_arrow_with_io_name(arg0: typing.Literal["->"], arg1: float | int) -> typing.Literal["->"]'
    )
    assert (
        doc(m.identity_literal_arrow_with_callable)
        == 'identity_literal_arrow_with_callable(arg0: collections.abc.Callable[[typing.Literal["->"], float | int], float]) -> collections.abc.Callable[[typing.Literal["->"], float | int], float]'
    )
    assert (
        doc(m.identity_literal_all_special_chars)
        == 'identity_literal_all_special_chars(arg0: typing.Literal["!@!!->{%}"]) -> typing.Literal["!@!!->{%}"]'
    )


@pytest.mark.skipif(
    not m.defined_PYBIND11_TYPING_H_HAS_STRING_LITERAL,
    reason="C++20 non-type template args feature not available.",
)
def test_typevar(doc):
    assert (
        doc(m.annotate_generic_containers)
        == "annotate_generic_containers(arg0: list[T]) -> list[V]"
    )

    assert doc(m.annotate_listT_to_T) == "annotate_listT_to_T(arg0: list[T]) -> T"

    assert doc(m.annotate_object_to_T) == "annotate_object_to_T(arg0: object) -> T"


@pytest.mark.skipif(
    not m.defined_PYBIND11_TEST_PYTYPES_HAS_RANGES,
    reason="<ranges> not available.",
)
@pytest.mark.parametrize(
    ("tested_tuple", "expected"),
    [((1,), [2]), ((3, 4), [4, 5]), ((7, 8, 9), [8, 9, 10])],
)
def test_tuple_ranges(tested_tuple, expected):
    assert m.tuple_iterator_default_initialization()
    assert m.transform_tuple_plus_one(tested_tuple) == expected


@pytest.mark.skipif(
    not m.defined_PYBIND11_TEST_PYTYPES_HAS_RANGES,
    reason="<ranges> not available.",
)
@pytest.mark.parametrize(
    ("tested_list", "expected"), [([1], [2]), ([3, 4], [4, 5]), ([7, 8, 9], [8, 9, 10])]
)
def test_list_ranges(tested_list, expected):
    assert m.list_iterator_default_initialization()
    assert m.transform_list_plus_one(tested_list) == expected


@pytest.mark.skipif(
    not m.defined_PYBIND11_TEST_PYTYPES_HAS_RANGES,
    reason="<ranges> not available.",
)
@pytest.mark.parametrize(
    ("tested_dict", "expected"),
    [
        ({1: 2}, [(2, 3)]),
        ({3: 4, 5: 6}, [(4, 5), (6, 7)]),
        ({7: 8, 9: 10, 11: 12}, [(8, 9), (10, 11), (12, 13)]),
    ],
)
def test_dict_ranges(tested_dict, expected):
    assert m.dict_iterator_default_initialization()
    assert m.transform_dict_plus_one(tested_dict) == expected


# https://docs.python.org/3/howto/annotations.html#accessing-the-annotations-dict-of-an-object-in-python-3-9-and-older
def get_annotations_helper(o):
    if sys.version_info >= (3, 14):
        import annotationlib

        return annotationlib.get_annotations(o) or None
    if isinstance(o, type):
        return o.__dict__.get("__annotations__", None)
    return getattr(o, "__annotations__", None)


@pytest.mark.skipif(
    not m.defined___cpp_inline_variables,
    reason="C++17 feature __cpp_inline_variables not available.",
)
def test_module_attribute_types() -> None:
    module_annotations = get_annotations_helper(m)

    assert (
        module_annotations["list_int"]
        == "list[typing.SupportsInt | typing.SupportsIndex]"
    )
    assert module_annotations["set_str"] == "set[str]"
    assert module_annotations["foo"] == "pybind11_tests.pytypes.foo"

    assert (
        module_annotations["foo_union"]
        == "pybind11_tests.pytypes.foo | pybind11_tests.pytypes.foo2 | pybind11_tests.pytypes.foo3"
    )


@pytest.mark.skipif(
    not m.defined___cpp_inline_variables,
    reason="C++17 feature __cpp_inline_variables not available.",
)
@pytest.mark.skipif(
    sys.version_info < (3, 10),
    reason="get_annotations function does not exist until Python3.10",
)
def test_get_annotations_compliance() -> None:
    from inspect import get_annotations

    module_annotations = get_annotations(m)

    assert (
        module_annotations["list_int"]
        == "list[typing.SupportsInt | typing.SupportsIndex]"
    )
    assert module_annotations["set_str"] == "set[str]"


@pytest.mark.skipif(
    not m.defined___cpp_inline_variables,
    reason="C++17 feature __cpp_inline_variables not available.",
)
def test_class_attribute_types() -> None:
    empty_annotations = get_annotations_helper(m.EmptyAnnotationClass)
    static_annotations = get_annotations_helper(m.Static)
    instance_annotations = get_annotations_helper(m.Instance)

    assert empty_annotations is None
    assert (
        static_annotations["x"]
        == "typing.ClassVar[typing.SupportsFloat | typing.SupportsIndex]"
    )
    assert (
        static_annotations["dict_str_int"]
        == "typing.ClassVar[dict[str, typing.SupportsInt | typing.SupportsIndex]]"
    )

    assert m.Static.x == 1.0

    m.Static.x = 3.0
    static = m.Static()
    assert static.x == 3.0

    static.dict_str_int["hi"] = 3
    assert m.Static().dict_str_int == {"hi": 3}

    assert instance_annotations["y"] == "typing.SupportsFloat | typing.SupportsIndex"
    instance1 = m.Instance()
    instance1.y = 4.0

    instance2 = m.Instance()
    instance2.y = 5.0

    assert instance1.y != instance2.y


@pytest.mark.skipif(
    not m.defined___cpp_inline_variables,
    reason="C++17 feature __cpp_inline_variables not available.",
)
def test_redeclaration_attr_with_type_hint() -> None:
    obj = m.Instance()
    m.attr_with_type_hint_float_x(obj)
    assert (
        get_annotations_helper(obj)["x"]
        == "typing.SupportsFloat | typing.SupportsIndex"
    )
    with pytest.raises(
        RuntimeError, match=r'^__annotations__\["x"\] was set already\.$'
    ):
        m.attr_with_type_hint_float_x(obj)


@pytest.mark.skipif(
    not m.defined___cpp_inline_variables,
    reason="C++17 feature __cpp_inline_variables not available.",
)
def test_final_annotation() -> None:
    module_annotations = get_annotations_helper(m)
    assert module_annotations["CONST_INT"] == "typing.Final[int]"


def test_arg_return_type_hints(doc, backport_typehints):
    assert doc(m.half_of_number) == "half_of_number(arg0: float | int) -> float"
    assert (
        doc(m.half_of_number_convert)
        == "half_of_number_convert(x: float | int) -> float"
    )
    assert (
        doc(m.half_of_number_noconvert) == "half_of_number_noconvert(x: float) -> float"
    )
    assert m.half_of_number(2.0) == 1.0
    assert m.half_of_number(2) == 1.0
    assert m.half_of_number(0) == 0
    assert isinstance(m.half_of_number(0), float)
    assert not isinstance(m.half_of_number(0), int)

    # std::vector<T>
    assert (
        doc(m.half_of_number_vector)
        == "half_of_number_vector(arg0: collections.abc.Sequence[float | int]) -> list[float]"
    )
    # Tuple<T, T>
    assert (
        doc(m.half_of_number_tuple)
        == "half_of_number_tuple(arg0: tuple[float | int, float | int]) -> tuple[float, float]"
    )
    # Tuple<T, ...>
    assert (
        doc(m.half_of_number_tuple_ellipsis)
        == "half_of_number_tuple_ellipsis(arg0: tuple[float | int, ...]) -> tuple[float, ...]"
    )
    # Dict<K, V>
    assert (
        doc(m.half_of_number_dict)
        == "half_of_number_dict(arg0: dict[str, float | int]) -> dict[str, float]"
    )
    # List<T>
    assert (
        doc(m.half_of_number_list)
        == "half_of_number_list(arg0: list[float | int]) -> list[float]"
    )
    # List<List<T>>
    assert (
        doc(m.half_of_number_nested_list)
        == "half_of_number_nested_list(arg0: list[list[float | int]]) -> list[list[float]]"
    )
    # Set<T>
    assert doc(m.identity_set) == "identity_set(arg0: set[float | int]) -> set[float]"
    # Iterable<T>
    assert (
        doc(m.identity_iterable)
        == "identity_iterable(arg0: collections.abc.Iterable[float | int]) -> collections.abc.Iterable[float]"
    )
    # Iterator<T>
    assert (
        doc(m.identity_iterator)
        == "identity_iterator(arg0: collections.abc.Iterator[float | int]) -> collections.abc.Iterator[float]"
    )
    # Callable<R(A)> identity
    assert (
        doc(m.identity_callable)
        == "identity_callable(arg0: collections.abc.Callable[[float | int], float]) -> collections.abc.Callable[[float | int], float]"
    )
    # Callable<R(...)> identity
    assert (
        doc(m.identity_callable_ellipsis)
        == "identity_callable_ellipsis(arg0: collections.abc.Callable[..., float]) -> collections.abc.Callable[..., float]"
    )
    # Nested Callable<R(A)> identity
    assert (
        doc(m.identity_nested_callable)
        == "identity_nested_callable(arg0: collections.abc.Callable[[collections.abc.Callable[[float | int], float]], collections.abc.Callable[[float | int], float]]) -> collections.abc.Callable[[collections.abc.Callable[[float | int], float]], collections.abc.Callable[[float | int], float]]"
    )
    # Callable<R(A)>
    assert (
        doc(m.apply_callable)
        == "apply_callable(arg0: float | int, arg1: collections.abc.Callable[[float | int], float]) -> float"
    )
    # Callable<R(...)>
    assert (
        doc(m.apply_callable_ellipsis)
        == "apply_callable_ellipsis(arg0: float | int, arg1: collections.abc.Callable[..., float]) -> float"
    )
    # Union<T1, T2>
    assert (
        doc(m.identity_union)
        == "identity_union(arg0: float | int | str) -> float | str"
    )
    # Optional<T>
    assert (
        doc(m.identity_optional)
        == "identity_optional(arg0: float | int | None) -> float | None"
    )
    # TypeIs<T>
    assert (
        backport_typehints(doc(m.check_type_is))
        == "check_type_is(arg0: object) -> typing.TypeIs[float]"
    )
    # TypeGuard<T>
    assert (
        backport_typehints(doc(m.check_type_guard))
        == "check_type_guard(arg0: list[object]) -> typing.TypeGuard[list[float]]"
    )


def test_const_kwargs_ref_to_str():
    assert m.const_kwargs_ref_to_str() == "{}"
    assert m.const_kwargs_ref_to_str(a=1) == "{'a': 1}"
