import re

import numpy as np
import pytest

import pandas as pd
import pandas._testing as tm


def _assert_almost_equal_both(a, b, **kwargs):
    """
    Check that two objects are approximately equal.

    This check is performed commutatively.

    Parameters
    ----------
    a : object
        The first object to compare.
    b : object
        The second object to compare.
    **kwargs
        The arguments passed to `tm.assert_almost_equal`.
    """
    tm.assert_almost_equal(a, b, **kwargs)
    tm.assert_almost_equal(b, a, **kwargs)


def _assert_not_almost_equal(a, b, **kwargs):
    """
    Check that two objects are not approximately equal.

    Parameters
    ----------
    a : object
        The first object to compare.
    b : object
        The second object to compare.
    **kwargs
        The arguments passed to `tm.assert_almost_equal`.
    """
    try:
        tm.assert_almost_equal(a, b, **kwargs)
        msg = f"{a} and {b} were approximately equal when they shouldn't have been"
        pytest.fail(reason=msg)
    except AssertionError:
        pass


def _assert_not_almost_equal_both(a, b, **kwargs):
    """
    Check that two objects are not approximately equal.

    This check is performed commutatively.

    Parameters
    ----------
    a : object
        The first object to compare.
    b : object
        The second object to compare.
    **kwargs
        The arguments passed to `tm.assert_almost_equal`.
    """
    _assert_not_almost_equal(a, b, **kwargs)
    _assert_not_almost_equal(b, a, **kwargs)


@pytest.mark.parametrize(
    "a,b",
    [
        (1.1, 1.1),
        (1.1, 1.100001),
        (np.int16(1), 1.000001),
        (np.float64(1.1), 1.1),
        (np.uint32(5), 5),
    ],
)
def test_assert_almost_equal_numbers(a, b):
    _assert_almost_equal_both(a, b)


@pytest.mark.parametrize(
    "a,b",
    [
        (1.1, 1),
        (1.1, True),
        (1, 2),
        (1.0001, np.int16(1)),
        # The following two examples are not "almost equal" due to tol.
        (0.1, 0.1001),
        (0.0011, 0.0012),
    ],
)
def test_assert_not_almost_equal_numbers(a, b):
    _assert_not_almost_equal_both(a, b)


@pytest.mark.parametrize(
    "a,b",
    [
        (1.1, 1.1),
        (1.1, 1.100001),
        (1.1, 1.1001),
        (0.000001, 0.000005),
        (1000.0, 1000.0005),
        # Testing this example, as per #13357
        (0.000011, 0.000012),
    ],
)
def test_assert_almost_equal_numbers_atol(a, b):
    # Equivalent to the deprecated check_less_precise=True, enforced in 2.0
    _assert_almost_equal_both(a, b, rtol=0.5e-3, atol=0.5e-3)


@pytest.mark.parametrize("a,b", [(1.1, 1.11), (0.1, 0.101), (0.000011, 0.001012)])
def test_assert_not_almost_equal_numbers_atol(a, b):
    _assert_not_almost_equal_both(a, b, atol=1e-3)


@pytest.mark.parametrize(
    "a,b",
    [
        (1.1, 1.1),
        (1.1, 1.100001),
        (1.1, 1.1001),
        (1000.0, 1000.0005),
        (1.1, 1.11),
        (0.1, 0.101),
    ],
)
def test_assert_almost_equal_numbers_rtol(a, b):
    _assert_almost_equal_both(a, b, rtol=0.05)


@pytest.mark.parametrize("a,b", [(0.000011, 0.000012), (0.000001, 0.000005)])
def test_assert_not_almost_equal_numbers_rtol(a, b):
    _assert_not_almost_equal_both(a, b, rtol=0.05)


@pytest.mark.parametrize("dtype", [int, np.int64, np.uint64])
def test_assert_almost_equal_large_integers_within_atol(dtype):
    # GH#66400 these land on float64s 256 apart, so the comparison used to
    #  report a difference that is not there
    a = 1450804465901089690
    b = 1450804465901089614  # 76 apart

    _assert_almost_equal_both(dtype(a), dtype(b), rtol=0, atol=100)


@pytest.mark.parametrize("dtype", [int, np.int64, np.uint64])
def test_assert_not_almost_equal_large_integers_outside_atol(dtype):
    # GH#66400 these land on the same float64, so the comparison used to pass
    a = 1450804465901089690
    b = a + 100

    _assert_not_almost_equal_both(dtype(a), dtype(b), rtol=0, atol=10)


@pytest.mark.parametrize("kwargs", [{"rtol": -1}, {"atol": -1}])
def test_assert_almost_equal_integers_negative_tolerance(kwargs):
    # GH#66400 the integer path must not swallow the math.isclose validation
    with pytest.raises(ValueError, match="tolerances must be non-negative"):
        tm.assert_almost_equal(1, 2, **kwargs)


@pytest.mark.parametrize(
    "a,b",
    [
        (2**60 + 1, float(2**60)),
        (np.int64(2**60 + 1), np.float64(2**60)),
        (np.uint64(2**63 + 1), np.float64(2**63)),
        (
            np.array([2**60 + 1], dtype="int64"),
            np.array([float(2**60)], dtype="float64"),
        ),
        (
            np.array([2**53 + 1], dtype="int64"),
            np.array([float(2**53)], dtype="float64"),
        ),
        (
            np.array([-(2**53) - 1], dtype="int64"),
            np.array([float(-(2**53))], dtype="float64"),
        ),
    ],
)
def test_assert_almost_equal_large_mixed_integer_float_atol(a, b):
    # GH#66699 float64 cannot represent the one-integer difference.
    _assert_not_almost_equal_both(a, b, check_dtype=False, rtol=0, atol=0.5)
    _assert_almost_equal_both(a, b, check_dtype=False, rtol=0, atol=1)


def test_assert_almost_equal_large_mixed_integer_float_rtol():
    a = 2**60 + 1
    b = float(2**60)

    _assert_not_almost_equal_both(a, b, check_dtype=False, rtol=0.5 / 2**60, atol=0)
    _assert_almost_equal_both(a, b, check_dtype=False, rtol=1 / 2**60, atol=0)


@pytest.mark.parametrize(
    "left,right",
    [
        (
            np.array([2**60], dtype="int64"),
            np.array([float(2**60)], dtype="float64"),
        ),
        (
            np.array([-(2**60)], dtype="int64"),
            np.array([float(-(2**60))], dtype="float64"),
        ),
        (
            np.array([2**64 - 2048], dtype="uint64"),  # 2048 = float64 ULP here
            np.array([float(2**64 - 2048)], dtype="float64"),
        ),
    ],
)
def test_assert_almost_equal_large_mixed_integer_float_equal(left, right):
    # GH#66699 magnitudes above 2**53 bypass the array_equivalent fast path, so
    #  the equal case has to be settled at full integer precision instead.
    _assert_almost_equal_both(left, right, check_dtype=False, rtol=0, atol=0)


@pytest.mark.parametrize(
    "left,right",
    [
        (
            np.array([2**63 - 1], dtype="int64"),
            np.array([float(2**63)], dtype="float64"),
        ),
        (
            np.array([2**64 - 1], dtype="uint64"),
            np.array([float(2**64)], dtype="float64"),
        ),
    ],
)
def test_assert_almost_equal_mixed_integer_float_out_of_range(left, right):
    # GH#68568 the float rounds past the end of the integer dtype, so it has no
    #  exact integer cast and the one-integer difference must still be reported
    _assert_not_almost_equal_both(left, right, check_dtype=False, rtol=0, atol=0)


def test_assert_almost_equal_large_mixed_integer_float_message():
    integer = 2**60 + 1
    floating = float(2**60)

    msg = re.escape(
        "expected 1152921504606846976.00000 but got 1152921504606846977.00000, "
        "with rtol=0, atol=0.5"
    )
    # \Z not $, so a trailing newline cannot slip past
    with pytest.raises(AssertionError, match=rf"^{msg}\Z"):
        tm.assert_almost_equal(integer, floating, rtol=0, atol=0.5)


def test_assert_almost_equal_2d_large_mixed_integer_float():
    # GH#68366 the element loop must honour check_dtype in 2-D as well as 1-D;
    #  the one-integer difference keeps this off the exact-match fast path
    big = np.array([[2**60 + 1, 1], [2, 3]], dtype="int64")
    flt = np.array([[float(2**60), 1.0], [2.0, 3.0]])

    _assert_almost_equal_both(big, flt, check_dtype=False, rtol=0, atol=1)
    _assert_not_almost_equal_both(big, flt, check_dtype=False, rtol=0, atol=0)


def test_assert_almost_equal_nested_arrays_check_dtype():
    # GH#68366 the element loop forwards check_dtype, so nested arrays compare
    #  by value rather than tripping on their dtypes
    left = [np.array([1, 2]), np.array([3, 4])]
    right = [np.array([1.0, 2.0]), np.array([3.0, 4.0])]

    _assert_almost_equal_both(left, right, check_dtype=False)


@pytest.mark.parametrize(
    "a,b,rtol",
    [
        (1.00001, 1.00005, 0.001),
        (-0.908356 + 0.2j, -0.908358 + 0.2j, 1e-3),
        (0.1 + 1.009j, 0.1 + 1.006j, 0.1),
        (0.1001 + 2.0j, 0.1 + 2.001j, 0.01),
    ],
)
def test_assert_almost_equal_complex_numbers(a, b, rtol):
    _assert_almost_equal_both(a, b, rtol=rtol)
    _assert_almost_equal_both(np.complex64(a), np.complex64(b), rtol=rtol)
    _assert_almost_equal_both(np.complex128(a), np.complex128(b), rtol=rtol)


@pytest.mark.parametrize(
    "a,b,rtol",
    [
        (0.58310768, 0.58330768, 1e-7),
        (-0.908 + 0.2j, -0.978 + 0.2j, 0.001),
        (0.1 + 1j, 0.1 + 2j, 0.01),
        (-0.132 + 1.001j, -0.132 + 1.005j, 1e-5),
        (0.58310768j, 0.58330768j, 1e-9),
    ],
)
def test_assert_not_almost_equal_complex_numbers(a, b, rtol):
    _assert_not_almost_equal_both(a, b, rtol=rtol)
    _assert_not_almost_equal_both(np.complex64(a), np.complex64(b), rtol=rtol)
    _assert_not_almost_equal_both(np.complex128(a), np.complex128(b), rtol=rtol)


@pytest.mark.parametrize("a,b", [(0, 0), (0, 0.0), (0, np.float64(0)), (0.00000001, 0)])
def test_assert_almost_equal_numbers_with_zeros(a, b):
    _assert_almost_equal_both(a, b)


@pytest.mark.parametrize("a,b", [(0.001, 0), (1, 0)])
def test_assert_not_almost_equal_numbers_with_zeros(a, b):
    _assert_not_almost_equal_both(a, b)


@pytest.mark.parametrize("a,b", [(1, "abc"), (1, [1]), (1, object())])
def test_assert_not_almost_equal_numbers_with_mixed(a, b):
    _assert_not_almost_equal_both(a, b)


@pytest.mark.parametrize(
    "left_dtype", ["M8[ns]", "m8[ns]", "float64", "int64", "object"]
)
@pytest.mark.parametrize(
    "right_dtype", ["M8[ns]", "m8[ns]", "float64", "int64", "object"]
)
def test_assert_almost_equal_edge_case_ndarrays(left_dtype, right_dtype):
    # Empty compare.
    _assert_almost_equal_both(
        np.array([], dtype=left_dtype),
        np.array([], dtype=right_dtype),
        check_dtype=False,
    )


def test_assert_almost_equal_sets():
    # GH#51727
    _assert_almost_equal_both({1, 2, 3}, {1, 2, 3})


def test_assert_almost_not_equal_sets():
    # GH#51727
    msg = r"{1, 2, 3} != {1, 2, 4}"
    with pytest.raises(AssertionError, match=msg):
        _assert_almost_equal_both({1, 2, 3}, {1, 2, 4})


def test_assert_almost_equal_dicts():
    _assert_almost_equal_both({"a": 1, "b": 2}, {"a": 1, "b": 2})


@pytest.mark.parametrize(
    "a,b",
    [
        ({"a": 1, "b": 2}, {"a": 1, "b": 3}),
        ({"a": 1, "b": 2}, {"a": 1, "b": 2, "c": 3}),
        ({"a": 1}, 1),
        ({"a": 1}, "abc"),
        ({"a": 1}, [1]),
    ],
)
def test_assert_not_almost_equal_dicts(a, b):
    _assert_not_almost_equal_both(a, b)


@pytest.mark.parametrize("val", [1, 2])
def test_assert_almost_equal_dict_like_object(val):
    dict_val = 1
    real_dict = {"a": val}

    class DictLikeObj:
        def keys(self):
            return ("a",)

        def __getitem__(self, item):
            if item == "a":
                return dict_val

    func = (
        _assert_almost_equal_both if val == dict_val else _assert_not_almost_equal_both
    )
    func(real_dict, DictLikeObj(), check_dtype=False)


def test_assert_almost_equal_strings():
    _assert_almost_equal_both("abc", "abc")


@pytest.mark.parametrize("b", ["abcd", "abd", 1, [1]])
def test_assert_not_almost_equal_strings(b):
    _assert_not_almost_equal_both("abc", b)


@pytest.mark.parametrize("box", [list, np.array])
def test_assert_almost_equal_iterables(box):
    _assert_almost_equal_both(box([1, 2, 3]), box([1, 2, 3]))


@pytest.mark.parametrize(
    "a,b",
    [
        # Class is different.
        (np.array([1, 2, 3]), [1, 2, 3]),
        # Dtype is different.
        (np.array([1, 2, 3]), np.array([1.0, 2.0, 3.0])),
        # Can't compare generators.
        (iter([1, 2, 3]), [1, 2, 3]),
        ([1, 2, 3], [1, 2, 4]),
        ([1, 2, 3], [1, 2, 3, 4]),
        ([1, 2, 3], 1),
    ],
)
def test_assert_not_almost_equal_iterables(a, b):
    _assert_not_almost_equal(a, b)


def test_assert_almost_equal_null():
    _assert_almost_equal_both(None, None)


@pytest.mark.parametrize("a,b", [(None, np.nan), (None, 0), (np.nan, 0)])
def test_assert_not_almost_equal_null(a, b):
    _assert_not_almost_equal(a, b)


@pytest.mark.parametrize(
    "a,b",
    [
        (np.inf, np.inf),
        (np.inf, float("inf")),
        (np.array([np.inf, np.nan, -np.inf]), np.array([np.inf, np.nan, -np.inf])),
    ],
)
def test_assert_almost_equal_inf(a, b):
    _assert_almost_equal_both(a, b)


objs = [
    pd.NA,
    np.nan,
    pd.NaT,
    None,
    np.datetime64("NaT", "ns"),
    np.timedelta64("NaT", "ns"),
]


@pytest.mark.parametrize("left", objs)
@pytest.mark.parametrize("right", objs)
def test_mismatched_na_assert_almost_equal(left, right):
    left_arr = np.array([left], dtype=object)
    right_arr = np.array([right], dtype=object)

    msg = "Mismatched null-like values"

    if left is right:
        _assert_almost_equal_both(left, right, check_dtype=False)
        tm.assert_numpy_array_equal(left_arr, right_arr)
        tm.assert_index_equal(
            pd.Index(left_arr, dtype=object), pd.Index(right_arr, dtype=object)
        )
        tm.assert_series_equal(
            pd.Series(left_arr, dtype=object), pd.Series(right_arr, dtype=object)
        )
        tm.assert_frame_equal(
            pd.DataFrame(left_arr, dtype=object), pd.DataFrame(right_arr, dtype=object)
        )

    else:
        with pytest.raises(AssertionError, match=msg):
            _assert_almost_equal_both(left, right, check_dtype=False)

        # TODO: to get the same deprecation in assert_numpy_array_equal we need
        #  to change/deprecate the default for strict_nan to become True
        # TODO: to get the same deprecation in assert_index_equal we need to
        #  change/deprecate array_equivalent_object to be stricter, as
        #  assert_index_equal uses Index.equal which uses array_equivalent.
        with pytest.raises(AssertionError, match="Series are different"):
            tm.assert_series_equal(
                pd.Series(left_arr, dtype=object), pd.Series(right_arr, dtype=object)
            )
        with pytest.raises(AssertionError, match="DataFrame.iloc.* are different"):
            tm.assert_frame_equal(
                pd.DataFrame(left_arr, dtype=object),
                pd.DataFrame(right_arr, dtype=object),
            )


def test_assert_not_almost_equal_inf():
    _assert_not_almost_equal_both(np.inf, 0)


@pytest.mark.parametrize(
    "a,b",
    [
        (pd.Index([1.0, 1.1]), pd.Index([1.0, 1.100001])),
        (pd.Series([1.0, 1.1]), pd.Series([1.0, 1.100001])),
        (np.array([1.1, 2.000001]), np.array([1.1, 2.0])),
        (pd.DataFrame({"a": [1.0, 1.1]}), pd.DataFrame({"a": [1.0, 1.100001]})),
    ],
)
def test_assert_almost_equal_pandas(a, b):
    _assert_almost_equal_both(a, b)


def test_assert_almost_equal_object():
    a = [pd.Timestamp("2011-01-01"), pd.Timestamp("2011-01-01")]
    b = [pd.Timestamp("2011-01-01"), pd.Timestamp("2011-01-01")]
    _assert_almost_equal_both(a, b)


def test_assert_almost_equal_value_mismatch():
    msg = "expected 2\\.00000 but got 1\\.00000, with rtol=1e-05, atol=1e-08"

    with pytest.raises(AssertionError, match=msg):
        tm.assert_almost_equal(1, 2)


@pytest.mark.parametrize(
    "a,b,klass1,klass2",
    [
        (np.array([1]), 1, "ndarray", "int"),
        (1, np.array([1]), "int", "ndarray"),
        (np.array(1), 1, "ndarray", "int"),
    ],
)
def test_assert_almost_equal_class_mismatch(a, b, klass1, klass2):
    msg = f"""numpy array are different

numpy array classes are different
\\[left\\]:  {klass1}
\\[right\\]: {klass2}"""

    with pytest.raises(AssertionError, match=msg):
        tm.assert_almost_equal(a, b)


def test_assert_almost_equal_value_mismatch1():
    msg = """numpy array are different

numpy array values are different \\(66\\.66667 %\\)
\\[left\\]:  \\[nan, 2\\.0, 3\\.0\\]
\\[right\\]: \\[1\\.0, nan, 3\\.0\\]"""

    with pytest.raises(AssertionError, match=msg):
        tm.assert_almost_equal(np.array([np.nan, 2, 3]), np.array([1, np.nan, 3]))


def test_assert_almost_equal_value_mismatch2():
    msg = """numpy array are different

numpy array values are different \\(50\\.0 %\\)
\\[left\\]:  \\[1, 2\\]
\\[right\\]: \\[1, 3\\]"""

    with pytest.raises(AssertionError, match=msg):
        tm.assert_almost_equal(np.array([1, 2]), np.array([1, 3]))


def test_assert_almost_equal_value_mismatch3():
    msg = """numpy array are different

numpy array values are different \\(16\\.66667 %\\)
\\[left\\]:  \\[\\[1, 2\\], \\[3, 4\\], \\[5, 6\\]\\]
\\[right\\]: \\[\\[1, 3\\], \\[3, 4\\], \\[5, 6\\]\\]"""

    with pytest.raises(AssertionError, match=msg):
        tm.assert_almost_equal(
            np.array([[1, 2], [3, 4], [5, 6]]), np.array([[1, 3], [3, 4], [5, 6]])
        )


def test_assert_almost_equal_value_mismatch4():
    msg = """numpy array are different

numpy array values are different \\(25\\.0 %\\)
\\[left\\]:  \\[\\[1, 2\\], \\[3, 4\\]\\]
\\[right\\]: \\[\\[1, 3\\], \\[3, 4\\]\\]"""

    with pytest.raises(AssertionError, match=msg):
        tm.assert_almost_equal(np.array([[1, 2], [3, 4]]), np.array([[1, 3], [3, 4]]))


def test_assert_almost_equal_value_mismatch_2d_percentage():
    # GH#68366 the percentage counts differing values, not differing rows
    msg = """numpy array are different

numpy array values are different \\(100\\.0 %\\)
\\[left\\]:  \\[\\[1, 2\\], \\[3, 4\\]\\]
\\[right\\]: \\[\\[9, 9\\], \\[9, 9\\]\\]
At positional index 0, first diff: 1 != 9"""

    with pytest.raises(AssertionError, match=msg):
        tm.assert_almost_equal(np.array([[1, 2], [3, 4]]), np.array([[9, 9], [9, 9]]))


def test_assert_almost_equal_shape_mismatch_override():
    msg = """Index are different

Index shapes are different
\\[left\\]:  \\(2L*,\\)
\\[right\\]: \\(3L*,\\)"""
    with pytest.raises(AssertionError, match=msg):
        tm.assert_almost_equal(np.array([1, 2]), np.array([3, 4, 5]), obj="Index")


def test_assert_almost_equal_unicode():
    # see gh-20503
    msg = """numpy array are different

numpy array values are different \\(33\\.33333 %\\)
\\[left\\]:  \\[á, à, ä\\]
\\[right\\]: \\[á, à, å\\]"""

    with pytest.raises(AssertionError, match=msg):
        tm.assert_almost_equal(np.array(["á", "à", "ä"]), np.array(["á", "à", "å"]))


def test_assert_almost_equal_timestamp():
    a = np.array([pd.Timestamp("2011-01-01"), pd.Timestamp("2011-01-01")])
    b = np.array([pd.Timestamp("2011-01-01"), pd.Timestamp("2011-01-02")])

    msg = """numpy array are different

numpy array values are different \\(50\\.0 %\\)
\\[left\\]:  \\[2011-01-01 00:00:00, 2011-01-01 00:00:00\\]
\\[right\\]: \\[2011-01-01 00:00:00, 2011-01-02 00:00:00\\]"""

    with pytest.raises(AssertionError, match=msg):
        tm.assert_almost_equal(a, b)


def test_assert_almost_equal_iterable_length_mismatch():
    msg = """Iterable are different

Iterable length are different
\\[left\\]:  2
\\[right\\]: 3"""

    with pytest.raises(AssertionError, match=msg):
        tm.assert_almost_equal([1, 2], [3, 4, 5])


@pytest.mark.parametrize(
    "value",
    [[1, 2], ([1, 2],), np.array([1, 2]), {1: None, 2: None}],
    ids=["list", "tuple-with-list", "array", "dict"],
)
@pytest.mark.parametrize("reverse", [False, True])
def test_assert_almost_equal_iterable_length_mismatch_unhashable(value, reverse):
    # GH#69014
    left = [1, 2]
    right = [value]
    if reverse:
        left, right = right, left

    msg = (
        "Iterable are different\n\nIterable length are different\n"
        f"[left]:  {len(left)}\n[right]: {len(right)}"
    )
    with pytest.raises(AssertionError, match=f"^{re.escape(msg)}$"):
        tm.assert_almost_equal(left, right)


def test_assert_almost_equal_iterable_length_mismatch_diff():
    # GH#69014: retain the extra diagnostic for hashable elements.
    msg = "[diff]: [3]"
    with pytest.raises(AssertionError, match=re.escape(msg)):
        tm.assert_almost_equal([1, 2], [1, 2, 3])


def test_assert_almost_equal_iterable_values_mismatch():
    msg = """Iterable are different

Iterable values are different \\(50\\.0 %\\)
\\[left\\]:  \\[1, 2\\]
\\[right\\]: \\[1, 3\\]"""

    with pytest.raises(AssertionError, match=msg):
        tm.assert_almost_equal([1, 2], [1, 3])


subarr = np.empty(2, dtype=object)
subarr[:] = [np.array([None, "b"], dtype=object), np.array(["c", "d"], dtype=object)]

NESTED_CASES = [
    # nested array
    (
        np.array([np.array([50, 70, 90]), np.array([20, 30])], dtype=object),
        np.array([np.array([50, 70, 90]), np.array([20, 30])], dtype=object),
    ),
    # >1 level of nesting
    (
        np.array(
            [
                np.array([np.array([50, 70]), np.array([90])], dtype=object),
                np.array([np.array([20, 30])], dtype=object),
            ],
            dtype=object,
        ),
        np.array(
            [
                np.array([np.array([50, 70]), np.array([90])], dtype=object),
                np.array([np.array([20, 30])], dtype=object),
            ],
            dtype=object,
        ),
    ),
    # lists
    (
        np.array([[50, 70, 90], [20, 30]], dtype=object),
        np.array([[50, 70, 90], [20, 30]], dtype=object),
    ),
    # mixed array/list
    (
        np.array([np.array([1, 2, 3]), np.array([4, 5])], dtype=object),
        np.array([[1, 2, 3], [4, 5]], dtype=object),
    ),
    (
        np.array([np.array([], dtype=object), None], dtype=object),
        np.array([[], None], dtype=object),
    ),
    (
        np.array(
            [
                np.array([np.array([1, 2, 3]), np.array([4, 5])], dtype=object),
                np.array(
                    [np.array([6]), np.array([7, 8]), np.array([9])], dtype=object
                ),
            ],
            dtype=object,
        ),
        np.array([[[1, 2, 3], [4, 5]], [[6], [7, 8], [9]]], dtype=object),
    ),
    # same-length lists
    (
        np.array([subarr, None], dtype=object),
        np.array([[[None, "b"], ["c", "d"]], None], dtype=object),
    ),
    # dicts
    (
        np.array([{"f1": 1, "f2": np.array(["a", "b"], dtype=object)}], dtype=object),
        np.array([{"f1": 1, "f2": np.array(["a", "b"], dtype=object)}], dtype=object),
    ),
    (
        np.array([{"f1": 1, "f2": np.array(["a", "b"], dtype=object)}], dtype=object),
        np.array([{"f1": 1, "f2": ["a", "b"]}], dtype=object),
    ),
    # array/list of dicts
    (
        np.array(
            [
                np.array(
                    [{"f1": 1, "f2": np.array(["a", "b"], dtype=object)}], dtype=object
                ),
                np.array([], dtype=object),
            ],
            dtype=object,
        ),
        np.array([[{"f1": 1, "f2": ["a", "b"]}], []], dtype=object),
    ),
]


@pytest.mark.parametrize("a,b", NESTED_CASES)
def test_assert_almost_equal_array_nested(a, b):
    _assert_almost_equal_both(a, b)


def test_assert_almost_equal_zero_dim_duck_array():
    # GH#45240 a scalar that defines __iter__/__len__ delegating to a scalar
    #  payload (as pint's Quantity does) is compared as a scalar
    class PintQuantity:
        # mimics pint's Quantity: __iter__/__len__ delegate to the magnitude,
        #  and ndim reports 0 when that magnitude is a scalar
        ndim = 0

        def __init__(self, magnitude):
            self.magnitude = magnitude

        def __iter__(self):
            return iter(self.magnitude)

        def __len__(self):
            return len(self.magnitude)

        def __eq__(self, other):
            return self.magnitude == other.magnitude

    left = np.array([PintQuantity(1), PintQuantity(2)], dtype=object)
    right = np.array([PintQuantity(1), PintQuantity(3)], dtype=object)

    _assert_almost_equal_both(left, left.copy())
    _assert_not_almost_equal_both(left, right)


class ZeroDimSubclass(np.ndarray):
    # GH#68927 mimics astropy's Quantity: indexing returns a 0-dim instance of the
    #  subclass rather than a numpy scalar
    def __new__(cls, values):
        return np.asarray(values).view(cls)

    def __getitem__(self, key):
        out = super().__getitem__(key)
        if not isinstance(out, np.ndarray):
            # Wrap scalars in a 0-dim array of the same subclass
            out = type(self)(out)
        return out


SHAPE = "shapes are different"
DTYPE = 'Attribute "dtype" are different'
CLASS = "classes are different"
NE = ""  # any AssertionError

dt_ns = "datetime64[ns]"
td_ns = "timedelta64[ns]"


def _check(a, b, expected, **kwargs):
    for left, right in [(a, b), (b, a)]:
        if expected is None:
            tm.assert_almost_equal(left, right, **kwargs)
        elif expected == NE:
            with pytest.raises(AssertionError):
                tm.assert_almost_equal(left, right, **kwargs)
        else:
            with pytest.raises(AssertionError, match=expected):
                tm.assert_almost_equal(left, right, **kwargs)


@pytest.mark.parametrize(
    "a, b, kwargs, expected",
    [
        # plain 0-dim ndarrays
        pytest.param(np.array(5), np.array(5), {}, None, id="int-eq"),
        pytest.param(np.array(5), np.array(6), {}, NE, id="int-ne"),
        pytest.param(np.array(5), np.array(5.0), {}, DTYPE, id="dtype-mismatch"),
        pytest.param(
            np.array(5),
            np.array(5.0),
            {"check_dtype": False},
            None,
            id="dtype-unchecked",
        ),
        pytest.param(np.array(5.0), np.array(5.1), {"atol": 0.2}, None, id="atol-pass"),
        pytest.param(np.array(5.0), np.array(5.1), {"atol": 0.01}, NE, id="atol-fail"),
        pytest.param(
            np.array(100.0), np.array(101.0), {"rtol": 0.02}, None, id="rtol-pass"
        ),
        pytest.param(np.array(np.nan), np.array(np.nan), {}, None, id="nan-nan"),
        pytest.param(np.array(np.nan), np.array(1.0), {}, NE, id="nan-num"),
        pytest.param(np.array(True), np.array(False), {}, NE, id="bool-ne"),
        pytest.param(np.array("abc"), np.array("abc"), {}, None, id="str-eq"),
        pytest.param(np.array("abc"), np.array("abd"), {}, NE, id="str-ne"),
        pytest.param(
            np.array(1, dtype=object), np.array(2, dtype=object), {}, NE, id="object-ne"
        ),
        pytest.param(
            np.array("2020-01-01", dtype=dt_ns),
            np.array("2020-01-01", dtype=dt_ns),
            {},
            None,
            id="dt64-eq",
        ),
        pytest.param(
            np.array("2020-01-01T00:00", dtype=dt_ns),
            np.array("2020-01-01T01:00", dtype=dt_ns),
            {},
            NE,
            id="dt64-ne",
        ),
        pytest.param(
            np.array(1, dtype=td_ns), np.array(1, dtype=td_ns), {}, None, id="td64-eq"
        ),
        pytest.param(
            np.array(10**15, dtype=td_ns),
            np.array(10**15 + 1, dtype=td_ns),
            {},
            NE,
            id="td64-ne",
        ),
        # 0-dim vs other shapes and scalars
        pytest.param(np.array(5), np.array([5]), {}, SHAPE, id="vs-1d"),
        pytest.param(np.array(5), np.array([5, 5]), {}, SHAPE, id="vs-size2"),
        pytest.param(np.array(5), 5, {}, CLASS, id="vs-scalar"),
        # ndarray subclass whose elements are 0-dim instances of itself
        pytest.param(ZeroDimSubclass(5), ZeroDimSubclass(5), {}, None, id="sub-0d-eq"),
        pytest.param(ZeroDimSubclass(5), ZeroDimSubclass(6), {}, NE, id="sub-0d-ne"),
        pytest.param(
            ZeroDimSubclass(5.0),
            ZeroDimSubclass(5.1),
            {"atol": 0.2},
            None,
            id="sub-atol-pass",
        ),
        pytest.param(
            ZeroDimSubclass(5.0),
            ZeroDimSubclass(5.1),
            {"atol": 0.01},
            NE,
            id="sub-atol-fail",
        ),
        pytest.param(
            ZeroDimSubclass(100.0),
            ZeroDimSubclass(101.0),
            {"rtol": 0.02},
            None,
            id="sub-rtol-pass",
        ),
        pytest.param(
            ZeroDimSubclass([1, 2]), ZeroDimSubclass([1, 2]), {}, None, id="sub-1d-eq"
        ),
        pytest.param(
            ZeroDimSubclass([1, 2]), ZeroDimSubclass([1, 3]), {}, NE, id="sub-1d-ne"
        ),
        pytest.param(
            ZeroDimSubclass(5), ZeroDimSubclass([5]), {}, SHAPE, id="sub-vs-1d"
        ),
        pytest.param(
            ZeroDimSubclass("abc"), ZeroDimSubclass("abd"), {}, NE, id="sub-str-ne"
        ),
        pytest.param(
            ZeroDimSubclass(np.array("2020-01-01T00:00", dtype=dt_ns)),
            ZeroDimSubclass(np.array("2020-01-01T01:00", dtype=dt_ns)),
            {},
            NE,
            id="sub-dt64-ne",
        ),
        pytest.param(
            ZeroDimSubclass(np.array([1, "a"], dtype=object)),
            ZeroDimSubclass(np.array([1, "b"], dtype=object)),
            {},
            NE,
            id="sub-object-ne",
        ),
    ],
)
def test_assert_almost_equal_zero_dim(a, b, kwargs, expected):
    # GH#68927
    _check(a, b, expected, **kwargs)
