"""
An exhaustive list of pandas methods exercising NDFrame.__finalize__.
"""

from copy import deepcopy
from datetime import time
import operator
import re

import numpy as np
import pytest

from pandas._typing import MergeHow

import pandas as pd

# TODO:
# * Binary methods (mul, div, etc.)
# * Binary outputs (align, etc.)
# * top-level methods (concat, merge, get_dummies, etc.)
# * window
# * cumulative reductions

not_implemented_mark = pytest.mark.xfail(reason="not implemented")

mi = pd.MultiIndex.from_product([["a", "b"], [0, 1]], names=["A", "B"])

frame_data = ({"A": [1]},)
frame_mi_data = ({"A": [1, 2, 3, 4]}, mi)


# Tuple of
# - Callable: Constructor (Series, DataFrame)
# - Tuple: Constructor args
# - Callable: pass the constructed value with attrs set to this.

_all_methods = [
    (pd.Series, ([0],), operator.methodcaller("take", [])),
    (pd.Series, ([0],), operator.methodcaller("__getitem__", [True])),
    (pd.Series, ([0],), operator.methodcaller("repeat", 2)),
    (pd.Series, ([0],), operator.methodcaller("reset_index")),
    (pd.Series, ([0],), operator.methodcaller("reset_index", drop=True)),
    (pd.Series, ([0],), operator.methodcaller("to_frame")),
    (pd.Series, ([0, 0],), operator.methodcaller("drop_duplicates")),
    (pd.Series, ([0, 0],), operator.methodcaller("duplicated")),
    (pd.Series, ([0, 0],), operator.methodcaller("round")),
    (pd.Series, ([0, 0],), operator.methodcaller("rename", lambda x: x + 1)),
    (pd.Series, ([0, 0],), operator.methodcaller("rename", "name")),
    (pd.Series, ([0, 0],), operator.methodcaller("set_axis", ["a", "b"])),
    (pd.Series, ([0, 0],), operator.methodcaller("reindex", [1, 0])),
    (pd.Series, ([0, 0],), operator.methodcaller("drop", [0])),
    (pd.Series, (pd.array([0, pd.NA]),), operator.methodcaller("fillna", 0)),
    (pd.Series, ([0, 0],), operator.methodcaller("replace", {0: 1})),
    (pd.Series, ([0, 0],), operator.methodcaller("shift")),
    (pd.Series, ([0, 0],), operator.methodcaller("isin", [0, 1])),
    (pd.Series, ([0, 0],), operator.methodcaller("between", 0, 2)),
    (pd.Series, ([0, 0],), operator.methodcaller("isna")),
    (pd.Series, ([0, 0],), operator.methodcaller("isnull")),
    (pd.Series, ([0, 0],), operator.methodcaller("notna")),
    (pd.Series, ([0, 0],), operator.methodcaller("notnull")),
    (pd.Series, ([1],), operator.methodcaller("add", pd.Series([1]))),
    # TODO: mul, div, etc.
    (
        pd.Series,
        ([0], pd.period_range("2000", periods=1)),
        operator.methodcaller("to_timestamp"),
    ),
    (
        pd.Series,
        ([0], pd.date_range("2000", periods=1)),
        operator.methodcaller("to_period"),
    ),
    pytest.param(
        (
            pd.DataFrame,
            frame_data,
            operator.methodcaller("dot", pd.DataFrame(index=["A"])),
        ),
        marks=pytest.mark.xfail(reason="Implement binary finalize"),
    ),
    (pd.DataFrame, frame_data, operator.methodcaller("transpose")),
    (pd.DataFrame, frame_data, operator.methodcaller("__getitem__", "A")),
    (pd.DataFrame, frame_data, operator.methodcaller("__getitem__", ["A"])),
    (pd.DataFrame, frame_data, operator.methodcaller("__getitem__", np.array([True]))),
    (pd.DataFrame, ({("A", "a"): [1]},), operator.methodcaller("__getitem__", ["A"])),
    (pd.DataFrame, frame_data, operator.methodcaller("query", "A == 1")),
    (pd.DataFrame, frame_data, operator.methodcaller("eval", "A + 1", engine="python")),
    (pd.DataFrame, frame_data, operator.methodcaller("select_dtypes", include="int")),
    (pd.DataFrame, frame_data, operator.methodcaller("assign", b=1)),
    (pd.DataFrame, frame_data, operator.methodcaller("set_axis", ["A"])),
    (pd.DataFrame, frame_data, operator.methodcaller("reindex", [0, 1])),
    (pd.DataFrame, frame_data, operator.methodcaller("drop", columns=["A"])),
    (pd.DataFrame, frame_data, operator.methodcaller("drop", index=[0])),
    (pd.DataFrame, frame_data, operator.methodcaller("rename", columns={"A": "a"})),
    (pd.DataFrame, frame_data, operator.methodcaller("rename", index=lambda x: x)),
    (pd.DataFrame, frame_data, operator.methodcaller("fillna", "A")),
    (pd.DataFrame, frame_data, operator.methodcaller("set_index", "A")),
    (pd.DataFrame, frame_data, operator.methodcaller("reset_index")),
    (pd.DataFrame, frame_data, operator.methodcaller("isna")),
    (pd.DataFrame, frame_data, operator.methodcaller("isnull")),
    (pd.DataFrame, frame_data, operator.methodcaller("notna")),
    (pd.DataFrame, frame_data, operator.methodcaller("notnull")),
    (pd.DataFrame, frame_data, operator.methodcaller("dropna")),
    (pd.DataFrame, frame_data, operator.methodcaller("drop_duplicates")),
    (pd.DataFrame, frame_data, operator.methodcaller("duplicated")),
    (pd.DataFrame, frame_data, operator.methodcaller("sort_values", by="A")),
    (pd.DataFrame, frame_data, operator.methodcaller("sort_index")),
    (pd.DataFrame, frame_data, operator.methodcaller("nlargest", 1, "A")),
    (pd.DataFrame, frame_data, operator.methodcaller("nsmallest", 1, "A")),
    (pd.DataFrame, frame_mi_data, operator.methodcaller("swaplevel")),
    (
        pd.DataFrame,
        frame_data,
        operator.methodcaller("add", pd.DataFrame(*frame_data)),
    ),
    # TODO: div, mul, etc.
    (
        pd.DataFrame,
        frame_data,
        operator.methodcaller("combine", pd.DataFrame(*frame_data), operator.add),
    ),
    (
        pd.DataFrame,
        frame_data,
        operator.methodcaller("combine_first", pd.DataFrame(*frame_data)),
    ),
    pytest.param(
        (
            pd.DataFrame,
            frame_data,
            operator.methodcaller("update", pd.DataFrame(*frame_data)),
        ),
        marks=not_implemented_mark,
    ),
    (pd.DataFrame, frame_data, operator.methodcaller("pivot", columns="A")),
    (
        pd.DataFrame,
        ({"A": [1], "B": [1]},),
        operator.methodcaller("pivot_table", columns="A"),
    ),
    (
        pd.DataFrame,
        ({"A": [1], "B": [1]},),
        operator.methodcaller("pivot_table", columns="A", aggfunc=["mean", "sum"]),
    ),
    (pd.DataFrame, frame_data, operator.methodcaller("stack")),
    (pd.DataFrame, frame_data, operator.methodcaller("explode", "A")),
    (pd.DataFrame, frame_mi_data, operator.methodcaller("unstack")),
    (
        pd.DataFrame,
        ({"A": ["a", "b", "c"], "B": [1, 3, 5], "C": [2, 4, 6]},),
        operator.methodcaller("melt", id_vars=["A"], value_vars=["B"]),
    ),
    (pd.DataFrame, frame_data, operator.methodcaller("map", lambda x: x)),
    (pd.DataFrame, frame_data, operator.methodcaller("round", 2)),
    (pd.DataFrame, frame_data, operator.methodcaller("corr")),
    pytest.param(
        (pd.DataFrame, frame_data, operator.methodcaller("cov")),
        marks=[
            pytest.mark.filterwarnings("ignore::RuntimeWarning"),
        ],
    ),
    (
        pd.DataFrame,
        frame_data,
        operator.methodcaller("corrwith", pd.DataFrame(*frame_data)),
    ),
    (pd.DataFrame, frame_data, operator.methodcaller("count")),
    (pd.DataFrame, frame_data, operator.methodcaller("nunique")),
    (pd.DataFrame, frame_data, operator.methodcaller("idxmin")),
    (pd.DataFrame, frame_data, operator.methodcaller("idxmax")),
    (pd.DataFrame, frame_data, operator.methodcaller("mode")),
    (pd.Series, [0], operator.methodcaller("mode")),
    (pd.DataFrame, frame_data, operator.methodcaller("median")),
    (
        pd.DataFrame,
        frame_data,
        operator.methodcaller("quantile", numeric_only=True),
    ),
    (
        pd.DataFrame,
        frame_data,
        operator.methodcaller("quantile", q=[0.25, 0.75], numeric_only=True),
    ),
    (
        pd.DataFrame,
        ({"A": [pd.Timedelta(days=1), pd.Timedelta(days=2)]},),
        operator.methodcaller("quantile", numeric_only=False),
    ),
    (
        pd.DataFrame,
        ({"A": [np.datetime64("2022-01-01"), np.datetime64("2022-01-02")]},),
        operator.methodcaller("quantile", numeric_only=True),
    ),
    (
        pd.DataFrame,
        ({"A": [1]}, [pd.Period("2000", "D")]),
        operator.methodcaller("to_timestamp"),
    ),
    (
        pd.DataFrame,
        ({"A": [1]}, [pd.Timestamp("2000")]),
        operator.methodcaller("to_period", freq="D"),
    ),
    (pd.DataFrame, frame_mi_data, operator.methodcaller("isin", [1])),
    (pd.DataFrame, frame_mi_data, operator.methodcaller("isin", pd.Series([1]))),
    (
        pd.DataFrame,
        frame_mi_data,
        operator.methodcaller("isin", pd.DataFrame({"A": [1]})),
    ),
    (pd.DataFrame, frame_mi_data, operator.methodcaller("droplevel", "A")),
    (pd.DataFrame, frame_data, operator.methodcaller("pop", "A")),
    # Squeeze on columns, otherwise we'll end up with a scalar
    (pd.DataFrame, frame_data, operator.methodcaller("squeeze", axis="columns")),
    (pd.Series, ([1, 2],), operator.methodcaller("squeeze")),
    (pd.Series, ([1, 2],), operator.methodcaller("rename_axis", index="a")),
    (pd.DataFrame, frame_data, operator.methodcaller("rename_axis", columns="a")),
    # Unary ops
    (pd.DataFrame, frame_data, operator.neg),
    (pd.Series, [1], operator.neg),
    (pd.DataFrame, frame_data, operator.pos),
    (pd.Series, [1], operator.pos),
    (pd.DataFrame, frame_data, operator.inv),
    (pd.Series, [1], operator.inv),
    (pd.DataFrame, frame_data, abs),
    (pd.Series, [1], abs),
    (pd.DataFrame, frame_data, round),
    (pd.Series, [1], round),
    (pd.DataFrame, frame_data, operator.methodcaller("take", [0, 0])),
    (pd.DataFrame, frame_mi_data, operator.methodcaller("xs", "a")),
    (pd.Series, (1, mi), operator.methodcaller("xs", "a")),
    (pd.DataFrame, frame_data, operator.methodcaller("get", "A")),
    (
        pd.DataFrame,
        frame_data,
        operator.methodcaller("reindex_like", pd.DataFrame({"A": [1, 2, 3]})),
    ),
    (
        pd.Series,
        frame_data,
        operator.methodcaller("reindex_like", pd.Series([0, 1, 2])),
    ),
    (pd.DataFrame, frame_data, operator.methodcaller("add_prefix", "_")),
    (pd.DataFrame, frame_data, operator.methodcaller("add_suffix", "_")),
    (pd.Series, (1, ["a", "b"]), operator.methodcaller("add_prefix", "_")),
    (pd.Series, (1, ["a", "b"]), operator.methodcaller("add_suffix", "_")),
    (pd.Series, ([3, 2],), operator.methodcaller("sort_values")),
    (pd.Series, ([1] * 10,), operator.methodcaller("head")),
    (pd.DataFrame, ({"A": [1] * 10},), operator.methodcaller("head")),
    (pd.Series, ([1] * 10,), operator.methodcaller("tail")),
    (pd.DataFrame, ({"A": [1] * 10},), operator.methodcaller("tail")),
    (pd.Series, ([1, 2],), operator.methodcaller("sample", n=2, replace=True)),
    (pd.DataFrame, (frame_data,), operator.methodcaller("sample", n=2, replace=True)),
    (pd.Series, ([1, 2],), operator.methodcaller("astype", float)),
    (pd.DataFrame, frame_data, operator.methodcaller("astype", float)),
    (pd.Series, ([1, 2],), operator.methodcaller("copy")),
    (pd.DataFrame, frame_data, operator.methodcaller("copy")),
    (pd.Series, ([1, 2], None, object), operator.methodcaller("infer_objects")),
    (
        pd.DataFrame,
        ({"A": np.array([1, 2], dtype=object)},),
        operator.methodcaller("infer_objects"),
    ),
    (pd.Series, ([1, 2],), operator.methodcaller("convert_dtypes")),
    (pd.DataFrame, frame_data, operator.methodcaller("convert_dtypes")),
    (pd.Series, ([1, None, 3],), operator.methodcaller("interpolate")),
    (pd.DataFrame, ({"A": [1, None, 3]},), operator.methodcaller("interpolate")),
    (pd.Series, ([1, 2],), operator.methodcaller("clip", lower=1)),
    (pd.DataFrame, frame_data, operator.methodcaller("clip", lower=1)),
    (
        pd.Series,
        (1, pd.date_range("2000", periods=4)),
        operator.methodcaller("asfreq", "h"),
    ),
    (
        pd.DataFrame,
        ({"A": [1, 1, 1, 1]}, pd.date_range("2000", periods=4)),
        operator.methodcaller("asfreq", "h"),
    ),
    (
        pd.Series,
        (1, pd.date_range("2000", periods=4)),
        operator.methodcaller("at_time", time(12)),
    ),
    (
        pd.DataFrame,
        ({"A": [1, 1, 1, 1]}, pd.date_range("2000", periods=4)),
        operator.methodcaller("at_time", time(12)),
    ),
    (
        pd.Series,
        (1, pd.date_range("2000", periods=4)),
        operator.methodcaller("between_time", "12:00", "13:00"),
    ),
    (
        pd.DataFrame,
        ({"A": [1, 1, 1, 1]}, pd.date_range("2000", periods=4)),
        operator.methodcaller("between_time", "12:00", "13:00"),
    ),
    (pd.Series, ([1, 2],), operator.methodcaller("rank")),
    (pd.DataFrame, frame_data, operator.methodcaller("rank")),
    (pd.Series, ([1, 2],), operator.methodcaller("where", np.array([True, False]))),
    (pd.DataFrame, frame_data, operator.methodcaller("where", np.array([[True]]))),
    (pd.Series, ([1, 2],), operator.methodcaller("mask", np.array([True, False]))),
    (pd.DataFrame, frame_data, operator.methodcaller("mask", np.array([[True]]))),
    (pd.Series, ([1, 2],), operator.methodcaller("truncate", before=0)),
    (pd.DataFrame, frame_data, operator.methodcaller("truncate", before=0)),
    (
        pd.Series,
        (1, pd.date_range("2000", periods=4, tz="UTC")),
        operator.methodcaller("tz_convert", "CET"),
    ),
    (
        pd.DataFrame,
        ({"A": [1, 1, 1, 1]}, pd.date_range("2000", periods=4, tz="UTC")),
        operator.methodcaller("tz_convert", "CET"),
    ),
    (
        pd.Series,
        (1, pd.date_range("2000", periods=4)),
        operator.methodcaller("tz_localize", "CET"),
    ),
    (
        pd.DataFrame,
        ({"A": [1, 1, 1, 1]}, pd.date_range("2000", periods=4)),
        operator.methodcaller("tz_localize", "CET"),
    ),
    (pd.Series, ([1, 2],), operator.methodcaller("describe")),
    (pd.DataFrame, frame_data, operator.methodcaller("describe")),
    (pd.Series, ([1, 2],), operator.methodcaller("pct_change")),
    (pd.DataFrame, frame_data, operator.methodcaller("pct_change")),
    (pd.Series, ([1],), operator.methodcaller("transform", lambda x: x - x.min())),
    (
        pd.DataFrame,
        frame_mi_data,
        operator.methodcaller("transform", lambda x: x - x.min()),
    ),
    (pd.Series, ([1],), operator.methodcaller("apply", lambda x: x)),
    (pd.DataFrame, frame_mi_data, operator.methodcaller("apply", lambda x: x)),
    # Cumulative reductions
    (pd.Series, ([1],), operator.methodcaller("cumsum")),
    (pd.DataFrame, frame_data, operator.methodcaller("cumsum")),
    (pd.Series, ([1],), operator.methodcaller("cummin")),
    (pd.DataFrame, frame_data, operator.methodcaller("cummin")),
    (pd.Series, ([1],), operator.methodcaller("cummax")),
    (pd.DataFrame, frame_data, operator.methodcaller("cummax")),
    (pd.Series, ([1],), operator.methodcaller("cumprod")),
    (pd.DataFrame, frame_data, operator.methodcaller("cumprod")),
    # Reductions
    (pd.DataFrame, frame_data, operator.methodcaller("any")),
    (pd.DataFrame, frame_data, operator.methodcaller("all")),
    (pd.DataFrame, frame_data, operator.methodcaller("min")),
    (pd.DataFrame, frame_data, operator.methodcaller("max")),
    (pd.DataFrame, frame_data, operator.methodcaller("sum")),
    (pd.DataFrame, frame_data, operator.methodcaller("std")),
    (pd.DataFrame, frame_data, operator.methodcaller("mean")),
    (pd.DataFrame, frame_data, operator.methodcaller("prod")),
    (pd.DataFrame, frame_data, operator.methodcaller("sem")),
    (pd.DataFrame, frame_data, operator.methodcaller("skew")),
    (pd.DataFrame, frame_data, operator.methodcaller("kurt")),
]


def idfn(x):
    xpr = re.compile(r"'(.*)?'")
    m = xpr.search(str(x))
    if m:
        return m.group(1)
    else:
        return str(x)


@pytest.mark.parametrize("ndframe_method", _all_methods, ids=lambda x: idfn(x[-1]))
def test_finalize_called(ndframe_method):
    cls, init_args, method = ndframe_method
    ndframe = cls(*init_args)

    ndframe.attrs = {"a": 1}
    result = method(ndframe)

    assert result.attrs == {"a": 1}


@not_implemented_mark
def test_finalize_called_eval_numexpr():
    pytest.importorskip("numexpr")
    df = pd.DataFrame({"A": [1, 2]})
    df.attrs["A"] = 1
    result = df.eval("A + 1", engine="numexpr")
    assert result.attrs == {"A": 1}


# ----------------------------------------------------------------------------
# Binary operations


@pytest.mark.parametrize("annotate", ["left", "right", "both"])
@pytest.mark.parametrize(
    "args",
    [
        (1, pd.Series([1])),
        (1, pd.DataFrame({"A": [1]})),
        (pd.Series([1]), 1),
        (pd.DataFrame({"A": [1]}), 1),
        (pd.Series([1]), pd.Series([1])),
        (pd.DataFrame({"A": [1]}), pd.DataFrame({"A": [1]})),
        (pd.Series([1]), pd.DataFrame({"A": [1]})),
        (pd.DataFrame({"A": [1]}), pd.Series([1])),
    ],
    ids=lambda x: f"({type(x[0]).__name__},{type(x[1]).__name__})",
)
def test_binops(request, args, annotate, all_binary_operators):
    # This generates 624 tests... Is that needed?
    left, right = args
    if isinstance(left, (pd.DataFrame, pd.Series)):
        left.attrs = {}
    if isinstance(right, (pd.DataFrame, pd.Series)):
        right.attrs = {}

    if annotate == "left" and isinstance(left, int):
        pytest.skip("left is an int and doesn't support .attrs")
    if annotate == "right" and isinstance(right, int):
        pytest.skip("right is an int and doesn't support .attrs")

    if annotate in {"left", "both"} and not isinstance(left, int):
        left.attrs = {"a": 1}
    if annotate in {"right", "both"} and not isinstance(right, int):
        right.attrs = {"a": 1}

    is_cmp = all_binary_operators in [
        operator.eq,
        operator.ne,
        operator.gt,
        operator.ge,
        operator.lt,
        operator.le,
    ]
    if is_cmp and isinstance(left, pd.DataFrame) and isinstance(right, pd.Series):
        # in 2.0 silent alignment on comparisons was removed xref GH#28759
        left, right = left.align(right, axis=1)
    elif is_cmp and isinstance(left, pd.Series) and isinstance(right, pd.DataFrame):
        right, left = right.align(left, axis=1)

    result = all_binary_operators(left, right)
    assert result.attrs == {"a": 1}


@pytest.mark.parametrize("left", [pd.Series, pd.DataFrame])
@pytest.mark.parametrize("right", [pd.Series, pd.DataFrame])
def test_attrs_binary_operations(all_binary_operators, left, right):
    # GH 51607
    attrs = {"a": 1}
    left = left([1])
    left.attrs = attrs
    right = right([2])
    assert all_binary_operators(left, right).attrs == attrs
    assert all_binary_operators(right, left).attrs == attrs


# ----------------------------------------------------------------------------
# Accessors


@pytest.mark.parametrize(
    "method",
    [
        operator.methodcaller("capitalize"),
        operator.methodcaller("casefold"),
        operator.methodcaller("cat", ["a"]),
        operator.methodcaller("contains", "a"),
        operator.methodcaller("count", "a"),
        operator.methodcaller("encode", "utf-8"),
        operator.methodcaller("endswith", "a"),
        operator.methodcaller("extract", r"(\w)(\d)"),
        operator.methodcaller("extract", r"(\w)(\d)", expand=False),
        operator.methodcaller("find", "a"),
        operator.methodcaller("findall", "a"),
        operator.methodcaller("get", 0),
        operator.methodcaller("index", "a"),
        operator.methodcaller("len"),
        operator.methodcaller("ljust", 4),
        operator.methodcaller("lower"),
        operator.methodcaller("lstrip"),
        operator.methodcaller("match", r"\w"),
        operator.methodcaller("normalize", "NFC"),
        operator.methodcaller("pad", 4),
        operator.methodcaller("partition", "a"),
        operator.methodcaller("repeat", 2),
        operator.methodcaller("replace", "a", "b"),
        operator.methodcaller("rfind", "a"),
        operator.methodcaller("rindex", "a"),
        operator.methodcaller("rjust", 4),
        operator.methodcaller("rpartition", "a"),
        operator.methodcaller("rstrip"),
        operator.methodcaller("slice", 4),
        operator.methodcaller("slice_replace", 1, repl="a"),
        operator.methodcaller("startswith", "a"),
        operator.methodcaller("strip"),
        operator.methodcaller("swapcase"),
        operator.methodcaller("translate", {"a": "b"}),
        operator.methodcaller("upper"),
        operator.methodcaller("wrap", 4),
        operator.methodcaller("zfill", 4),
        operator.methodcaller("isalnum"),
        operator.methodcaller("isalpha"),
        operator.methodcaller("isdigit"),
        operator.methodcaller("isspace"),
        operator.methodcaller("islower"),
        operator.methodcaller("isupper"),
        operator.methodcaller("istitle"),
        operator.methodcaller("isnumeric"),
        operator.methodcaller("isdecimal"),
        operator.methodcaller("get_dummies"),
    ],
    ids=idfn,
)
def test_string_method(method):
    s = pd.Series(["a1"])
    s.attrs = {"a": 1}
    result = method(s.str)
    assert result.attrs == {"a": 1}


@pytest.mark.parametrize(
    "method",
    [
        operator.methodcaller("to_period"),
        operator.methodcaller("tz_localize", "CET"),
        operator.methodcaller("normalize"),
        operator.methodcaller("strftime", "%Y"),
        operator.methodcaller("round", "h"),
        operator.methodcaller("floor", "h"),
        operator.methodcaller("ceil", "h"),
        operator.methodcaller("month_name"),
        operator.methodcaller("day_name"),
    ],
    ids=idfn,
)
def test_datetime_method(method):
    s = pd.Series(pd.date_range("2000", periods=4))
    s.attrs = {"a": 1}
    result = method(s.dt)
    assert result.attrs == {"a": 1}


@pytest.mark.parametrize(
    "attr",
    [
        "date",
        "time",
        "timetz",
        "year",
        "month",
        "day",
        "hour",
        "minute",
        "second",
        "microsecond",
        "nanosecond",
        "dayofweek",
        "day_of_week",
        "dayofyear",
        "day_of_year",
        "quarter",
        "is_month_start",
        "is_month_end",
        "is_quarter_start",
        "is_quarter_end",
        "is_year_start",
        "is_year_end",
        "is_leap_year",
        "daysinmonth",
        "days_in_month",
    ],
)
def test_datetime_property(attr):
    s = pd.Series(pd.date_range("2000", periods=4))
    s.attrs = {"a": 1}
    result = getattr(s.dt, attr)
    assert result.attrs == {"a": 1}


@pytest.mark.parametrize(
    "attr", ["days", "seconds", "microseconds", "nanoseconds", "components"]
)
def test_timedelta_property(attr):
    s = pd.Series(pd.timedelta_range("2000", periods=4))
    s.attrs = {"a": 1}
    result = getattr(s.dt, attr)
    assert result.attrs == {"a": 1}


@pytest.mark.parametrize("method", [operator.methodcaller("total_seconds")])
def test_timedelta_methods(method):
    s = pd.Series(pd.timedelta_range("2000", periods=4))
    s.attrs = {"a": 1}
    result = method(s.dt)
    assert result.attrs == {"a": 1}


@pytest.mark.parametrize(
    "method",
    [
        operator.methodcaller("add_categories", ["c"]),
        operator.methodcaller("as_ordered"),
        operator.methodcaller("as_unordered"),
        lambda x: x.codes,
        operator.methodcaller("remove_categories", "a"),
        operator.methodcaller("remove_unused_categories"),
        operator.methodcaller("rename_categories", {"a": "A", "b": "B"}),
        operator.methodcaller("reorder_categories", ["b", "a"]),
        operator.methodcaller("set_categories", ["A", "B"]),
    ],
)
@not_implemented_mark
def test_categorical_accessor(method):
    s = pd.Series(["a", "b"], dtype="category")
    s.attrs = {"a": 1}
    result = method(s.cat)
    assert result.attrs == {"a": 1}


# ----------------------------------------------------------------------------
# Groupby


@pytest.mark.parametrize(
    "obj", [pd.Series([0, 0]), pd.DataFrame({"A": [0, 1], "B": [1, 2]})]
)
@pytest.mark.parametrize(
    "method",
    [
        operator.methodcaller("sum"),
        lambda x: x.apply(lambda y: y),
        lambda x: x.agg("sum"),
        lambda x: x.agg("mean"),
        lambda x: x.agg("median"),
    ],
)
def test_groupby_finalize(obj, method):
    obj.attrs = {"a": 1}
    result = method(obj.groupby([0, 0], group_keys=False))
    assert result.attrs == {"a": 1}


@pytest.mark.parametrize(
    "obj", [pd.Series([0, 0]), pd.DataFrame({"A": [0, 1], "B": [1, 2]})]
)
@pytest.mark.parametrize(
    "method",
    [
        lambda x: x.agg(["sum", "count"]),
        lambda x: x.agg("std"),
        lambda x: x.agg("var"),
        lambda x: x.agg("sem"),
        lambda x: x.agg("size"),
        lambda x: x.agg("ohlc"),
    ],
)
@not_implemented_mark
def test_groupby_finalize_not_implemented(obj, method):
    obj.attrs = {"a": 1}
    result = method(obj.groupby([0, 0]))
    assert result.attrs == {"a": 1}


def test_finalize_frame_series_name():
    # https://github.com/pandas-dev/pandas/pull/37186/files#r506978889
    # ensure we don't copy the column `name` to the Series.
    df = pd.DataFrame({"name": [1, 2]})
    result = pd.Series([1, 2]).__finalize__(df)
    assert result.name is None


# ----------------------------------------------------------------------------
# Merge


@pytest.mark.parametrize(
    ["allow_on_left", "allow_on_right"],
    [(False, False), (False, True), (True, False), (True, True)],
)
@pytest.mark.parametrize(
    "how",
    [
        "left",
        "right",
        "inner",
        "outer",
        "left_anti",
        "right_anti",
        "cross",
    ],
)
def test_merge_correctly_sets_duplication_allowance_flag(
    how: MergeHow,
    allow_on_left: bool,
    allow_on_right: bool,
):
    left = pd.DataFrame({"test": [1]}).set_flags(allows_duplicate_labels=allow_on_left)
    right = pd.DataFrame({"test": [1]}).set_flags(
        allows_duplicate_labels=allow_on_right,
    )

    if not how == "cross":
        result = left.merge(right, how=how, on="test")
    else:
        result = left.merge(right, how=how)

    expected_duplication_allowance = allow_on_left and allow_on_right
    assert result.flags.allows_duplicate_labels == expected_duplication_allowance


@pytest.mark.parametrize(
    ["allow_on_left", "allow_on_right"],
    [(False, False), (False, True), (True, False), (True, True)],
)
def test_merge_asof_correctly_sets_duplication_allowance_flag(
    allow_on_left: bool,
    allow_on_right: bool,
):
    left = pd.DataFrame({"test": [1]}).set_flags(allows_duplicate_labels=allow_on_left)
    right = pd.DataFrame({"test": [1]}).set_flags(
        allows_duplicate_labels=allow_on_right,
    )

    result = pd.merge_asof(left, right)

    expected_duplication_allowance = allow_on_left and allow_on_right
    assert result.flags.allows_duplicate_labels == expected_duplication_allowance


def test_merge_propagates_metadata_from_equal_input_metadata():
    metadata = {"a": [1, 2]}
    left = pd.DataFrame({"test": [1]})
    left.attrs = metadata
    right = pd.DataFrame({"test": [1]})
    right.attrs = deepcopy(metadata)

    result = left.merge(right, how="inner", on="test")

    assert result.attrs == metadata

    # Verify that merge deep-copies the attr dictionary.
    assert result.attrs is not left.attrs
    assert result.attrs is not right.attrs
    assert result.attrs["a"] is not left.attrs["a"]
    assert result.attrs["a"] is not right.attrs["a"]


def test_merge_does_not_propagate_metadata_from_unequal_input_metadata():
    left = pd.DataFrame({"test": [1]})
    left.attrs = {"a": 2}
    right = pd.DataFrame({"test": [1]})
    right.attrs = {"b": 3}

    result = left.merge(right, how="inner", on="test")

    assert result.attrs == {}


@pytest.mark.parametrize(
    ["left_has_metadata", "right_has_metadata", "expected"],
    [
        (False, True, {}),
        (True, False, {}),
        (False, False, {}),
    ],
    ids=["left-empty", "right-empty", "both-empty"],
)
def test_merge_does_not_propagate_metadata_if_one_input_has_no_metadata(
    left_has_metadata: bool,
    right_has_metadata: bool,
    expected: dict,
):
    left = pd.DataFrame({"test": [1]})
    right = pd.DataFrame({"test": [1]})

    if left_has_metadata:
        left.attrs = {"a": [1, 2]}
    else:
        left.attrs = {}

    if right_has_metadata:
        right.attrs = {"a": [1, 2]}
    else:
        right.attrs = {}

    result = left.merge(right, how="inner", on="test")

    assert result.attrs == expected
