"""test positional based indexing with iloc"""

from array import array as std_array
from collections import deque
from datetime import datetime
import re

import numpy as np
import pytest

from pandas.errors import IndexingError
import pandas.util._test_decorators as td

import pandas as pd
import pandas._testing as tm
from pandas.api.types import is_scalar
from pandas.tests.indexing.common import check_indexing_smoketest_or_raises

# We pass through the error message from numpy
_slice_iloc_msg = re.escape(
    "only integers, slices (`:`), ellipsis (`...`), numpy.newaxis (`None`) "
    "and integer or boolean arrays are valid indices"
)


class DuckSequence:
    """A sequence implementing only __len__/__getitem__, so not is_list_like."""

    def __init__(self, data) -> None:
        self.data = list(data)

    def __len__(self) -> int:
        return len(self.data)

    def __getitem__(self, key):
        return self.data[key]


class GetitemOnly:
    """Subscriptable but unsized, so np.asarray treats it as a scalar."""

    def __getitem__(self, key):
        return key


class TestiLoc:
    @pytest.mark.parametrize("key", [2, -1, [0, 1, 2]])
    @pytest.mark.parametrize(
        "index",
        [
            pd.Index(list("abcd"), dtype=object),
            pd.Index([2, 4, "null", 8], dtype=object),
            pd.date_range("20130101", periods=4),
            pd.Index(range(0, 8, 2), dtype=np.float64),
            pd.Index([]),
        ],
    )
    def test_iloc_getitem_int_and_list_int(self, key, frame_or_series, index, request):
        obj = frame_or_series(range(len(index)), index=index)
        check_indexing_smoketest_or_raises(
            obj,
            "iloc",
            key,
            fails=IndexError,
        )

        # array of ints (GH5006), make sure that a single indexer is returning
        # the correct type


class TestiLocBaseIndependent:
    """Tests Independent Of Base Class"""

    @pytest.mark.parametrize(
        "key",
        [
            slice(None),
            slice(3),
            range(3),
            [0, 1, 2],
            pd.Index(range(3)),
            np.asarray([0, 1, 2]),
        ],
    )
    def test_iloc_setitem_fullcol_categorical(self, indexer_li, key):
        frame = pd.DataFrame({0: range(3)}, dtype=object)

        cat = pd.Categorical(["alpha", "beta", "gamma"])

        assert frame._mgr.blocks[0]._can_hold_element(cat)

        df = frame.copy()
        orig_vals = df.values

        indexer_li(df)[key, 0] = cat

        expected = pd.DataFrame({0: cat}).astype(object)
        assert np.shares_memory(df[0].values, orig_vals)

        tm.assert_frame_equal(df, expected)

        # check we dont have a view on cat (may be undesired GH#39986)
        df.iloc[0, 0] = "gamma"
        assert cat[0] != "gamma"

        # pre-2.0 with mixed dataframe ("split" path) we always overwrote the
        #  column.  as of 2.0 we correctly write "into" the column, so
        #  we retain the object dtype.
        frame = pd.DataFrame({0: np.array([0, 1, 2], dtype=object), 1: range(3)})
        df = frame.copy()
        indexer_li(df)[key, 0] = cat
        expected = pd.DataFrame(
            {0: pd.Series(cat.astype(object), dtype=object), 1: range(3)}
        )
        tm.assert_frame_equal(df, expected)

    @pytest.mark.parametrize("has_ref", [True, False])
    def test_iloc_setitem_ea_inplace(
        self, frame_or_series, index_or_series_or_array, has_ref
    ):
        # GH#38952 Case with not setting a full column
        #  IntegerArray without NAs
        arr = pd.array([1, 2, 3, 4])
        obj = frame_or_series(arr.to_numpy("i8"))
        if has_ref:
            view = obj[:]  # noqa: F841

        if frame_or_series is pd.Series:
            values = obj.values
        else:
            values = obj._mgr.blocks[0].values

        if frame_or_series is pd.Series:
            obj.iloc[:2] = index_or_series_or_array(arr[2:])
        else:
            obj.iloc[:2, 0] = index_or_series_or_array(arr[2:])

        expected = frame_or_series(np.array([3, 4, 3, 4], dtype="i8"))
        tm.assert_equal(obj, expected)

        # Check that we are actually in-place
        if not has_ref:
            if frame_or_series is pd.Series:
                assert obj.values is not values
                assert np.shares_memory(obj.values, values)
            else:
                assert np.shares_memory(obj[0].values, values)

    def test_is_scalar_access(self):
        # GH#32085 index with duplicates doesn't matter for _is_scalar_access
        index = pd.Index([1, 2, 1])
        ser = pd.Series(range(3), index=index)

        assert ser.iloc._is_scalar_access((1,))

        df = ser.to_frame()
        assert df.iloc._is_scalar_access((1, 0))

    def test_iloc_exceeds_bounds(self):
        # GH6296
        # iloc should allow indexers that exceed the bounds
        df = pd.DataFrame(
            np.random.default_rng(2).random((20, 5)), columns=list("ABCDE")
        )

        # lists of positions should raise IndexError!
        msg = "positional indexers are out-of-bounds"
        with pytest.raises(IndexError, match=msg):
            df.iloc[:, [0, 1, 2, 3, 4, 5]]
        with pytest.raises(IndexError, match=msg):
            df.iloc[[1, 30]]
        with pytest.raises(IndexError, match=msg):
            df.iloc[[1, -30]]
        with pytest.raises(IndexError, match=msg):
            df.iloc[[100]]

        s = df["A"]
        with pytest.raises(IndexError, match=msg):
            s.iloc[[100]]
        with pytest.raises(IndexError, match=msg):
            s.iloc[[-100]]

        # still raise on a single indexer
        msg = "single positional indexer is out-of-bounds"
        with pytest.raises(IndexError, match=msg):
            df.iloc[30]
        with pytest.raises(IndexError, match=msg):
            df.iloc[-30]

        # GH10779
        # single positive/negative indexer exceeding Series bounds should raise
        # an IndexError
        with pytest.raises(IndexError, match=msg):
            s.iloc[30]
        with pytest.raises(IndexError, match=msg):
            s.iloc[-30]

        # slices are ok
        result = df.iloc[:, 4:10]  # 0 < start < len < stop
        expected = df.iloc[:, 4:]
        tm.assert_frame_equal(result, expected)

        result = df.iloc[:, -4:-10]  # stop < 0 < start < len
        expected = df.iloc[:, :0]
        tm.assert_frame_equal(result, expected)

        result = df.iloc[:, 10:4:-1]  # 0 < stop < len < start (down)
        expected = df.iloc[:, :4:-1]
        tm.assert_frame_equal(result, expected)

        result = df.iloc[:, 4:-10:-1]  # stop < 0 < start < len (down)
        expected = df.iloc[:, 4::-1]
        tm.assert_frame_equal(result, expected)

        result = df.iloc[:, -10:4]  # start < 0 < stop < len
        expected = df.iloc[:, :4]
        tm.assert_frame_equal(result, expected)

        result = df.iloc[:, 10:4]  # 0 < stop < len < start
        expected = df.iloc[:, :0]
        tm.assert_frame_equal(result, expected)

        result = df.iloc[:, -10:-11:-1]  # stop < start < 0 < len (down)
        expected = df.iloc[:, :0]
        tm.assert_frame_equal(result, expected)

        result = df.iloc[:, 10:11]  # 0 < len < start < stop
        expected = df.iloc[:, :0]
        tm.assert_frame_equal(result, expected)

        # slice bounds exceeding is ok
        result = s.iloc[18:30]
        expected = s.iloc[18:]
        tm.assert_series_equal(result, expected)

        result = s.iloc[30:]
        expected = s.iloc[:0]
        tm.assert_series_equal(result, expected)

        result = s.iloc[30::-1]
        expected = s.iloc[::-1]
        tm.assert_series_equal(result, expected)

        # doc example
        dfl = pd.DataFrame(
            np.random.default_rng(2).standard_normal((5, 2)), columns=list("AB")
        )
        tm.assert_frame_equal(
            dfl.iloc[:, 2:3],
            pd.DataFrame(
                index=dfl.index, columns=pd.Index([], dtype=dfl.columns.dtype)
            ),
        )
        tm.assert_frame_equal(dfl.iloc[:, 1:3], dfl.iloc[:, [1]])
        tm.assert_frame_equal(dfl.iloc[4:6], dfl.iloc[[4]])

        msg = "positional indexers are out-of-bounds"
        with pytest.raises(IndexError, match=msg):
            dfl.iloc[[4, 5, 6]]
        msg = "single positional indexer is out-of-bounds"
        with pytest.raises(IndexError, match=msg):
            dfl.iloc[:, 4]

    @pytest.mark.parametrize("index,columns", [(np.arange(20), list("ABCDE"))])
    @pytest.mark.parametrize(
        "index_vals,column_vals",
        [
            ([slice(None), ["A", "D"]]),
            (["1", "2"], slice(None)),
            ([datetime(2019, 1, 1)], slice(None)),
        ],
    )
    def test_iloc_non_integer_raises(self, index, columns, index_vals, column_vals):
        # GH 25753
        df = pd.DataFrame(
            np.random.default_rng(2).standard_normal((len(index), len(columns))),
            index=index,
            columns=columns,
        )
        msg = ".iloc requires numeric indexers, got"
        with pytest.raises(IndexError, match=msg):
            df.iloc[index_vals, column_vals]

    def test_iloc_getitem_invalid_scalar(self, frame_or_series):
        # GH 21982

        obj = pd.DataFrame(np.arange(100).reshape(10, 10))
        obj = tm.get_obj(obj, frame_or_series)

        with pytest.raises(TypeError, match="Cannot index by location index"):
            obj.iloc["a"]

    def test_iloc_array_not_mutating_negative_indices(self):
        # GH 21867
        array_with_neg_numbers = np.array([1, 2, -1])
        array_copy = array_with_neg_numbers.copy()
        df = pd.DataFrame(
            {"A": [100, 101, 102], "B": [103, 104, 105], "C": [106, 107, 108]},
            index=[1, 2, 3],
        )
        df.iloc[array_with_neg_numbers]
        tm.assert_numpy_array_equal(array_with_neg_numbers, array_copy)
        df.iloc[:, array_with_neg_numbers]
        tm.assert_numpy_array_equal(array_with_neg_numbers, array_copy)

    def test_iloc_getitem_neg_int_can_reach_first_index(self):
        # GH10547 and GH10779
        # negative integers should be able to reach index 0
        df = pd.DataFrame({"A": [2, 3, 5], "B": [7, 11, 13]})
        s = df["A"]

        expected = df.iloc[0]
        result = df.iloc[-3]
        tm.assert_series_equal(result, expected)

        expected = df.iloc[[0]]
        result = df.iloc[[-3]]
        tm.assert_frame_equal(result, expected)

        expected = s.iloc[0]
        result = s.iloc[-3]
        assert result == expected

        expected = s.iloc[[0]]
        result = s.iloc[[-3]]
        tm.assert_series_equal(result, expected)

        # check the length 1 Series case highlighted in GH10547
        expected = pd.Series(["a"], index=["A"])
        result = expected.iloc[[-1]]
        tm.assert_series_equal(result, expected)

    def test_iloc_getitem_dups(self):
        # GH 6766
        df1 = pd.DataFrame([{"A": None, "B": 1}, {"A": 2, "B": 2}])
        df2 = pd.DataFrame([{"A": 3, "B": 3}, {"A": 4, "B": 4}])
        df = pd.concat([df1, df2], axis=1)

        # cross-sectional indexing
        result = df.iloc[0, 0]
        assert pd.isna(result)

        result = df.iloc[0, :]
        expected = pd.Series([np.nan, 1, 3, 3], index=["A", "B", "A", "B"], name=0)
        tm.assert_series_equal(result, expected)

    def test_iloc_getitem_array(self):
        df = pd.DataFrame(
            [
                {"A": 1, "B": 2, "C": 3},
                {"A": 100, "B": 200, "C": 300},
                {"A": 1000, "B": 2000, "C": 3000},
            ]
        )

        expected = pd.DataFrame([{"A": 1, "B": 2, "C": 3}])
        tm.assert_frame_equal(df.iloc[[0]], expected)

        expected = pd.DataFrame(
            [{"A": 1, "B": 2, "C": 3}, {"A": 100, "B": 200, "C": 300}]
        )
        tm.assert_frame_equal(df.iloc[[0, 1]], expected)

        expected = pd.DataFrame(
            [{"B": 2, "C": 3}, {"B": 2000, "C": 3000}], index=[0, 2]
        )
        result = df.iloc[[0, 2], [1, 2]]
        tm.assert_frame_equal(result, expected)

    def test_iloc_getitem_bool(self):
        df = pd.DataFrame(
            [
                {"A": 1, "B": 2, "C": 3},
                {"A": 100, "B": 200, "C": 300},
                {"A": 1000, "B": 2000, "C": 3000},
            ]
        )

        expected = pd.DataFrame(
            [{"A": 1, "B": 2, "C": 3}, {"A": 100, "B": 200, "C": 300}]
        )
        result = df.iloc[[True, True, False]]
        tm.assert_frame_equal(result, expected)

        expected = pd.DataFrame(
            [{"A": 1, "B": 2, "C": 3}, {"A": 1000, "B": 2000, "C": 3000}], index=[0, 2]
        )
        result = df.iloc[lambda x: x.index % 2 == 0]
        tm.assert_frame_equal(result, expected)

    @pytest.mark.parametrize("index", [[True, False], [True, False, True, False]])
    def test_iloc_getitem_bool_diff_len(self, index):
        # GH26658
        s = pd.Series([1, 2, 3])
        msg = f"Boolean index has wrong length: {len(index)} instead of {len(s)}"
        with pytest.raises(IndexError, match=msg):
            s.iloc[index]

    def test_iloc_getitem_slice(self):
        df = pd.DataFrame(
            [
                {"A": 1, "B": 2, "C": 3},
                {"A": 100, "B": 200, "C": 300},
                {"A": 1000, "B": 2000, "C": 3000},
            ]
        )

        expected = pd.DataFrame(
            [{"A": 1, "B": 2, "C": 3}, {"A": 100, "B": 200, "C": 300}]
        )
        result = df.iloc[:2]
        tm.assert_frame_equal(result, expected)

        expected = pd.DataFrame([{"A": 100, "B": 200}], index=[1])
        result = df.iloc[1:2, 0:2]
        tm.assert_frame_equal(result, expected)

        expected = pd.DataFrame(
            [{"A": 1, "C": 3}, {"A": 100, "C": 300}, {"A": 1000, "C": 3000}]
        )
        result = df.iloc[:, lambda df: [0, 2]]
        tm.assert_frame_equal(result, expected)

    def test_iloc_getitem_slice_dups(self):
        df1 = pd.DataFrame(
            np.random.default_rng(2).standard_normal((10, 4)),
            columns=["A", "A", "B", "B"],
        )
        df2 = pd.DataFrame(
            np.random.default_rng(2).integers(0, 10, size=20).reshape(10, 2),
            columns=["A", "C"],
        )

        # axis=1
        df = pd.concat([df1, df2], axis=1)
        tm.assert_frame_equal(df.iloc[:, :4], df1)
        tm.assert_frame_equal(df.iloc[:, 4:], df2)

        df = pd.concat([df2, df1], axis=1)
        tm.assert_frame_equal(df.iloc[:, :2], df2)
        tm.assert_frame_equal(df.iloc[:, 2:], df1)

        exp = pd.concat([df2, df1.iloc[:, [0]]], axis=1)
        tm.assert_frame_equal(df.iloc[:, 0:3], exp)

        # axis=0
        df = pd.concat([df, df], axis=0)
        tm.assert_frame_equal(df.iloc[0:10, :2], df2)
        tm.assert_frame_equal(df.iloc[0:10, 2:], df1)
        tm.assert_frame_equal(df.iloc[10:, :2], df2)
        tm.assert_frame_equal(df.iloc[10:, 2:], df1)

    @pytest.mark.parametrize("has_ref", [True, False])
    def test_iloc_setitem(sel, has_ref):
        df = pd.DataFrame(
            np.random.default_rng(2).standard_normal((4, 4)),
            index=np.arange(0, 8, 2),
            columns=np.arange(0, 12, 3),
        )
        if has_ref:
            view = df[:]  # noqa: F841

        df.iloc[1, 1] = 1
        result = df.iloc[1, 1]
        assert result == 1

        df.iloc[:, 2:3] = 0
        expected = df.iloc[:, 2:3]
        result = df.iloc[:, 2:3]
        tm.assert_frame_equal(result, expected)

        # GH5771
        s = pd.Series(0, index=[4, 5, 6])
        s.iloc[1:2] += 1
        expected = pd.Series([0, 1, 0], index=[4, 5, 6])
        tm.assert_series_equal(s, expected)

    @pytest.mark.parametrize("has_ref", [True, False])
    def test_iloc_setitem_axis_argument(self, has_ref):
        # GH45032
        df = pd.DataFrame([[6, "c", 10], [7, "d", 11], [8, "e", 12]])
        df[1] = df[1].astype(object)
        if has_ref:
            view = df[:]
        expected = pd.DataFrame([[6, "c", 10], [7, "d", 11], [5, 5, 5]])
        expected[1] = expected[1].astype(object)
        df.iloc(axis=0)[2] = 5
        tm.assert_frame_equal(df, expected)

        df = pd.DataFrame([[6, "c", 10], [7, "d", 11], [8, "e", 12]])
        df[1] = df[1].astype(object)
        if has_ref:
            view = df[:]  # noqa: F841
        expected = pd.DataFrame([[6, "c", 5], [7, "d", 5], [8, "e", 5]])
        expected[1] = expected[1].astype(object)
        df.iloc(axis=1)[2] = 5
        tm.assert_frame_equal(df, expected)

    @pytest.mark.parametrize("has_ref", [True, False])
    def test_iloc_setitem_list(self, has_ref):
        # setitem with an iloc list
        df = pd.DataFrame(
            np.arange(9).reshape((3, 3)), index=["A", "B", "C"], columns=["A", "B", "C"]
        )
        if has_ref:
            view = df[:]  # noqa: F841
        df.iloc[[0, 1], [1, 2]]
        df.iloc[[0, 1], [1, 2]] += 100

        expected = pd.DataFrame(
            np.array([0, 101, 102, 3, 104, 105, 6, 7, 8]).reshape((3, 3)),
            index=["A", "B", "C"],
            columns=["A", "B", "C"],
        )
        tm.assert_frame_equal(df, expected)

    def test_iloc_setitem_pandas_object(self):
        # GH 17193
        s_orig = pd.Series([0, 1, 2, 3])
        expected = pd.Series([0, -1, -2, 3])

        s = s_orig.copy()
        s.iloc[pd.Series([1, 2])] = [-1, -2]
        tm.assert_series_equal(s, expected)

        s = s_orig.copy()
        s.iloc[pd.Index([1, 2])] = [-1, -2]
        tm.assert_series_equal(s, expected)

    def test_iloc_setitem_dups(self):
        # GH 6766
        # iloc with a mask aligning from another iloc
        df1 = pd.DataFrame([{"A": None, "B": 1}, {"A": 2, "B": 2}])
        df2 = pd.DataFrame([{"A": 3, "B": 3}, {"A": 4, "B": 4}])
        df = pd.concat([df1, df2], axis=1)

        expected = df.fillna(3)
        inds = np.isnan(df.iloc[:, 0])
        mask = inds[inds].index
        df.iloc[mask, 0] = df.iloc[mask, 2]
        tm.assert_frame_equal(df, expected)

        # del a dup column across blocks
        expected = pd.DataFrame({0: [1, 2], 1: [3, 4]})
        expected.columns = ["B", "B"]
        del df["A"]
        tm.assert_frame_equal(df, expected)

        # assign back to self
        df.iloc[[0, 1], [0, 1]] = df.iloc[[0, 1], [0, 1]]
        tm.assert_frame_equal(df, expected)

        # reversed x 2
        df.iloc[[1, 0], [0, 1]] = df.iloc[[1, 0], [0, 1]].reset_index(drop=True)
        df.iloc[[1, 0], [0, 1]] = df.iloc[[1, 0], [0, 1]].reset_index(drop=True)
        tm.assert_frame_equal(df, expected)

    def test_iloc_setitem_frame_duplicate_columns_multiple_blocks(self):
        # Same as the "assign back to self" check in test_iloc_setitem_dups
        #  but on a DataFrame with multiple blocks
        df = pd.DataFrame([[0, 1], [2, 3]], columns=["B", "B"])

        # setting float values that can be held by existing integer arrays
        #  is inplace
        df.iloc[:, 0] = df.iloc[:, 0].astype("f8")
        assert len(df._mgr.blocks) == 1

        # if the assigned values cannot be held by existing integer arrays,
        #  we raise
        with pytest.raises(TypeError, match="Invalid value"):
            df.iloc[:, 0] = df.iloc[:, 0] + 0.5

        expected = df.copy()

        # assign back to self
        df.iloc[[0, 1], [0, 1]] = df.iloc[[0, 1], [0, 1]]

        tm.assert_frame_equal(df, expected)

    def test_iloc_setitem_unordered_column_indexer_referenced_block(self):
        # GH#65446 assignment was silently lost when the block was referenced
        #  by another object and the column indexer was not sorted
        df = pd.DataFrame({"A": [1, 2], "B": [3, 4]})
        df_orig = df.copy()
        ref = df[["A", "B"]]
        df.iloc[:, [1, 0]] = pd.DataFrame({"A": [10, 20], "B": [30, 40]})
        df._mgr._verify_integrity()
        expected = pd.DataFrame({"A": [30, 40], "B": [10, 20]})
        tm.assert_frame_equal(df, expected)
        tm.assert_frame_equal(ref, df_orig)

    def test_iloc_setitem_duplicate_column_indexer_referenced_block(self):
        # GH#65446
        df = pd.DataFrame({"A": [1, 2], "B": [3, 4]})
        df_orig = df.copy()
        ref = df[["A", "B"]]
        df.iloc[:, [0, 0]] = np.array([[10, 30], [20, 40]])
        df._mgr._verify_integrity()
        expected = pd.DataFrame({"A": [30, 40], "B": [3, 4]})
        tm.assert_frame_equal(df, expected)
        tm.assert_frame_equal(ref, df_orig)

    def test_iloc_setitem_frame_swap_columns(self):
        # GH#65446 positional swap when the RHS shares data with the target
        df = pd.DataFrame({"A": [1, 2], "B": [3, 4]})
        df.iloc[:, [1, 0]] = df[["A", "B"]]
        expected = pd.DataFrame({"A": [3, 4], "B": [1, 2]})
        tm.assert_frame_equal(df, expected)

    def test_iloc_setitem_unordered_row_and_column_indexer_referenced_block(self):
        # GH#65446 a list row indexer combined with an unsorted column indexer
        #  must set the full cross product
        df = pd.DataFrame({"A": [1, 2], "B": [3, 4]})
        df_orig = df.copy()
        ref = df[["A", "B"]]
        df.iloc[[1, 0], [1, 0]] = np.array([[10, 20], [30, 40]])
        df._mgr._verify_integrity()
        expected = pd.DataFrame({"A": [40, 20], "B": [30, 10]})
        tm.assert_frame_equal(df, expected)
        tm.assert_frame_equal(ref, df_orig)

    @pytest.mark.parametrize("dtype", ["float64", "M8[s]"])
    def test_iloc_setitem_reversed_slice_column_indexer_referenced_block(self, dtype):
        # GH#65446 a slice column indexer takes the cross product with the
        #  row indexer; on a referenced frame it became an array instead
        df = pd.DataFrame(
            np.arange(12, dtype="i8").reshape(4, 3).astype(dtype), columns=list("abc")
        )
        df_orig = df.copy()
        ref = df[["a", "b", "c"]]
        value = np.arange(100, 106, dtype="i8").reshape(2, 3).astype(dtype)
        df.iloc[[2, 0], ::-1] = value
        df._mgr._verify_integrity()
        expected = df_orig.copy()
        expected.iloc[2] = value[0, ::-1]
        expected.iloc[0] = value[1, ::-1]
        tm.assert_frame_equal(df, expected)
        tm.assert_frame_equal(ref, df_orig)

    @pytest.mark.parametrize("dtype", ["float64", "M8[s]"])
    @pytest.mark.parametrize(
        "key, shape",
        [
            (([2, 0], slice(0, 2)), (2, 2)),
            (([2, 0], slice(2, None, -2)), (2, 2)),
            # these keys reject a 2-D value even on an unreferenced frame
            ((range(2, 0, -1), [0, 2]), None),
            (((2, 1), [0, 2]), None),
            ((pd.array([2, 1], dtype="Int64"), [0, 2]), None),
            ((np.array([2, 1]), range(0, 3, 2)), None),
            ((np.array([2, 1]), pd.array([0, 2], dtype="Int64")), None),
            ((np.array([0, 2]), None), None),
            (([2, 1, 0], Ellipsis), (3, 3)),
            ((Ellipsis, [2, 0, 1]), (4, 3)),
            ((np.array([True, False, True, False]), slice(None, None, -1)), (2, 3)),
            ((np.array([[1, 0]]), slice(None, None, -1)), (2, 3)),
        ],
    )
    def test_iloc_setitem_referenced_block_matches_unreferenced(
        self, key, shape, dtype
    ):
        # GH#65446 a distinct value per cell, so a transposed or reordered
        #  write shows up; a scalar still tells a cross product from a
        #  pointwise write
        df = pd.DataFrame(
            np.arange(12, dtype="i8").reshape(4, 3).astype(dtype), columns=list("abc")
        )
        df_orig = df.copy()
        if shape is None:
            value = df_orig.iloc[3, 1]
        else:
            value = np.arange(100, 100 + np.prod(shape), dtype="i8").reshape(shape)
            value = value.astype(dtype)

        expected = df.copy()
        expected.iloc[key] = value
        ref = df[["a", "b", "c"]]
        df.iloc[key] = value
        df._mgr._verify_integrity()
        tm.assert_frame_equal(df, expected)
        tm.assert_frame_equal(ref, df_orig)

    def test_loc_setitem_boolean_row_mask_reversed_slice_referenced_block(self):
        # GH#65446 a reversed label slice reaches the same path through loc
        df = pd.DataFrame(
            np.arange(12).reshape(4, 3).astype("float64"), columns=list("abc")
        )
        df_orig = df.copy()
        ref = df[["a", "b", "c"]]
        df.loc[[True, False, True, False], "c":"a":-1] = 99.0
        df._mgr._verify_integrity()
        expected = df_orig.copy()
        expected.iloc[[0, 2]] = 99.0
        tm.assert_frame_equal(df, expected)
        tm.assert_frame_equal(ref, df_orig)

    # TODO: GH#27620 this test used to compare iloc against ix; check if this
    #  is redundant with another test comparing iloc against loc
    def test_iloc_getitem_frame(self):
        df = pd.DataFrame(
            np.random.default_rng(2).standard_normal((10, 4)),
            index=range(0, 20, 2),
            columns=range(0, 8, 2),
        )

        result = df.iloc[2]
        exp = df.loc[4]
        tm.assert_series_equal(result, exp)

        result = df.iloc[2, 2]
        exp = df.loc[4, 4]
        assert result == exp

        # slice
        result = df.iloc[4:8]
        expected = df.loc[8:14]
        tm.assert_frame_equal(result, expected)

        result = df.iloc[:, 2:3]
        expected = df.loc[:, 4:5]
        tm.assert_frame_equal(result, expected)

        # list of integers
        result = df.iloc[[0, 1, 3]]
        expected = df.loc[[0, 2, 6]]
        tm.assert_frame_equal(result, expected)

        result = df.iloc[[0, 1, 3], [0, 1]]
        expected = df.loc[[0, 2, 6], [0, 2]]
        tm.assert_frame_equal(result, expected)

        # neg indices
        result = df.iloc[[-1, 1, 3], [-1, 1]]
        expected = df.loc[[18, 2, 6], [6, 2]]
        tm.assert_frame_equal(result, expected)

        # dups indices
        result = df.iloc[[-1, -1, 1, 3], [-1, 1]]
        expected = df.loc[[18, 18, 2, 6], [6, 2]]
        tm.assert_frame_equal(result, expected)

        # with index-like
        s = pd.Series(index=range(1, 5), dtype=object)
        result = df.iloc[s.index]
        expected = df.loc[[2, 4, 6, 8]]
        tm.assert_frame_equal(result, expected)

    def test_iloc_getitem_labelled_frame(self):
        # try with labelled frame
        df = pd.DataFrame(
            np.random.default_rng(2).standard_normal((10, 4)),
            index=list("abcdefghij"),
            columns=list("ABCD"),
        )

        result = df.iloc[1, 1]
        exp = df.loc["b", "B"]
        assert result == exp

        result = df.iloc[:, 2:3]
        expected = df.loc[:, ["C"]]
        tm.assert_frame_equal(result, expected)

        # negative indexing
        result = df.iloc[-1, -1]
        exp = df.loc["j", "D"]
        assert result == exp

        # out-of-bounds exception
        msg = "index 5 is out of bounds for axis 0 with size 4"
        with pytest.raises(IndexError, match=msg):
            df.iloc[10, 5]

        # trying to use a label
        msg = (
            r"Location based indexing can only have \[integer, integer "
            r"slice \(START point is INCLUDED, END point is EXCLUDED\), "
            r"listlike of integers, boolean array\] types"
        )
        with pytest.raises(ValueError, match=msg):
            df.iloc["j", "D"]

    def test_iloc_getitem_doc_issue(self):
        # multi axis slicing issue with single block
        # surfaced in GH 6059

        arr = np.random.default_rng(2).standard_normal((6, 4))
        index = pd.date_range("20130101", periods=6)
        columns = list("ABCD")
        df = pd.DataFrame(arr, index=index, columns=columns)

        # defines ref_locs
        df.describe()

        result = df.iloc[3:5, 0:2]

        expected = pd.DataFrame(arr[3:5, 0:2], index=index[3:5], columns=columns[0:2])
        tm.assert_frame_equal(result, expected)

        # for dups
        df.columns = list("aaaa")
        result = df.iloc[3:5, 0:2]

        expected = pd.DataFrame(arr[3:5, 0:2], index=index[3:5], columns=list("aa"))
        tm.assert_frame_equal(result, expected)

        # related
        arr = np.random.default_rng(2).standard_normal((6, 4))
        index = list(range(0, 12, 2))
        columns = list(range(0, 8, 2))
        df = pd.DataFrame(arr, index=index, columns=columns)

        df._mgr.blocks[0].mgr_locs
        result = df.iloc[1:5, 2:4]
        expected = pd.DataFrame(arr[1:5, 2:4], index=index[1:5], columns=columns[2:4])
        tm.assert_frame_equal(result, expected)

    @pytest.mark.parametrize("has_ref", [True, False])
    def test_iloc_setitem_series(self, has_ref):
        df = pd.DataFrame(
            np.random.default_rng(2).standard_normal((10, 4)),
            index=list("abcdefghij"),
            columns=list("ABCD"),
        )
        if has_ref:
            view = df[:]  # noqa: F841

        df.iloc[1, 1] = 1
        result = df.iloc[1, 1]
        assert result == 1

        df.iloc[:, 2:3] = 0
        expected = df.iloc[:, 2:3]
        result = df.iloc[:, 2:3]
        tm.assert_frame_equal(result, expected)

        s = pd.Series(
            np.random.default_rng(2).standard_normal(10), index=range(0, 20, 2)
        )

        s.iloc[1] = 1
        result = s.iloc[1]
        assert result == 1

        s.iloc[:4] = 0
        expected = s.iloc[:4]
        result = s.iloc[:4]
        tm.assert_series_equal(result, expected)

        s = pd.Series([-1] * 6)
        s.iloc[0::2] = [0, 2, 4]
        s.iloc[1::2] = [1, 3, 5]
        result = s
        expected = pd.Series([0, 1, 2, 3, 4, 5])
        tm.assert_series_equal(result, expected)

    @pytest.mark.parametrize("has_ref", [True, False])
    def test_iloc_setitem_list_of_lists(self, has_ref):
        # GH 7551
        # list-of-list is set incorrectly in mixed vs. single dtyped frames
        df = pd.DataFrame(
            {"A": np.arange(5, dtype="int64"), "B": np.arange(5, 10, dtype="int64")}
        )
        if has_ref:
            view = df[:]
        df.iloc[2:4] = [[10, 11], [12, 13]]
        expected = pd.DataFrame({"A": [0, 1, 10, 12, 4], "B": [5, 6, 11, 13, 9]})
        tm.assert_frame_equal(df, expected)

        df = pd.DataFrame(
            {"A": ["a", "b", "c", "d", "e"], "B": np.arange(5, 10, dtype="int64")}
        )
        if has_ref:
            view = df[:]  # noqa: F841
        df.iloc[2:4] = [["x", 11], ["y", 13]]
        expected = pd.DataFrame(
            {"A": ["a", "b", "x", "y", "e"], "B": [5, 6, 11, 13, 9]}
        )
        tm.assert_frame_equal(df, expected)

    @pytest.mark.parametrize("indexer", ["loc", "iloc"])
    @pytest.mark.parametrize("value", [[[1], [2, 3]], [[2, 3], [1]]])
    def test_setitem_ragged_list_of_lists_raises(self, indexer, value):
        # GH#64229
        df = pd.DataFrame({"a": [0.0, 0.0], "b": [0, 0], "c": [0.0, 0.0]})
        with pytest.raises(ValueError, match="Must have equal len keys"):
            if indexer == "loc":
                df.loc[:, ["a", "c"]] = value
            else:
                df.iloc[:, [0, 2]] = value

    @pytest.mark.parametrize(
        "box",
        [np.array, pd.Series, pd.Index, pd.array],
        ids=["ndarray", "Series", "Index", "pd.array"],
    )
    @pytest.mark.parametrize("indexer", ["loc", "iloc"])
    @pytest.mark.parametrize("nrows", [2, 3])
    def test_setitem_split_path_list_of_arrays_is_2d(self, indexer, nrows, box):
        # GH#64230 a list of 1-D array-likes is a 2D value (a list of rows),
        # just like a list of lists / an ndarray; the split path used to
        # transpose it into columns instead.
        df = pd.DataFrame(
            {
                "a": np.zeros(nrows),
                "b": np.zeros(nrows, dtype="int64"),
                "c": np.zeros(nrows),
            }
        )
        rows = [[2 * i + 1, 2 * i + 2] for i in range(nrows)]
        value = [box(row) for row in rows]
        if indexer == "loc":
            df.loc[:, ["a", "c"]] = value
        else:
            df.iloc[:, [0, 2]] = value

        arr = np.array(rows, dtype="float64")
        expected = pd.DataFrame(
            {"a": arr[:, 0], "b": np.zeros(nrows, dtype="int64"), "c": arr[:, 1]}
        )
        tm.assert_frame_equal(df, expected)

    @pytest.mark.parametrize(
        "value",
        [
            [pd.Series([1, 2], index=[5, 6]), pd.Series([3, 4], index=["p", "q"])],
            # only value[0] decides that this is 2D, but every row gets unwrapped
            [[1, 2], pd.Series([3, 4], index=["p", "q"])],
            [pd.Index([1, 2]), pd.Series([3, 4], index=["p", "q"])],
            # labels that DO resolve positionally, to a different answer: this
            #  catches a label-based read by the value, not by an exception
            [pd.Series([1, 2], index=[1, 0]), pd.Series([3, 4], index=[1, 0])],
        ],
        ids=["all-series", "list-first", "index-first", "reversed-labels"],
    )
    @pytest.mark.parametrize("indexer", ["loc", "iloc"])
    def test_setitem_split_path_list_of_series_not_aligned(self, indexer, value):
        # GH#64230 Series rows are read positionally, not by label
        df = pd.DataFrame({"a": [0.0, 0.0], "b": np.zeros(2, dtype="int64")})
        if indexer == "loc":
            df.loc[:, ["a", "b"]] = value
        else:
            df.iloc[:, [0, 1]] = value

        expected = pd.DataFrame({"a": [1.0, 3.0], "b": [2, 4]})
        tm.assert_frame_equal(df, expected)

    @pytest.mark.parametrize(
        "box",
        [np.array, pd.Series, pd.Index, pd.array],
        ids=["ndarray", "Series", "Index", "pd.array"],
    )
    @pytest.mark.parametrize("indexer", ["loc", "iloc"])
    def test_setitem_split_path_list_of_arrays_single_column(self, indexer, box):
        # GH#64230 a list of 1-D array-likes targeting a single column is a
        # shape mismatch, same as a list of lists; unlike mismatched-length
        # tuples (GH#65264) arrays are not scalar-like, so no per-cell storage
        df = pd.DataFrame({"a": np.zeros(2, dtype=object), "b": [0, 0]})
        value = [box([1, 2]), box([3, 4])]
        msg = "Must have equal len keys and value when setting with an ndarray"
        with pytest.raises(ValueError, match=msg):
            if indexer == "loc":
                df.loc[:, ["a"]] = value
            else:
                df.iloc[:, [0]] = value

    @pytest.mark.parametrize("indexer", ["loc", "iloc"])
    def test_setitem_split_path_mismatched_tuples_single_column(self, indexer):
        # GH#37629 mismatched-length tuples into a single column are cell values,
        # even when the column key is a list rather than a scalar label
        df = pd.DataFrame({"a": np.zeros(2, dtype=object), "b": [0, 0]})
        value = [(1, 2), (3, 4)]
        if indexer == "loc":
            df.loc[:, ["a"]] = value
        else:
            df.iloc[:, [0]] = value

        expected = pd.DataFrame(
            {"a": pd.Series([(1, 2), (3, 4)], dtype=object), "b": [0, 0]}
        )
        tm.assert_frame_equal(df, expected)

    @pytest.mark.parametrize("indexer", ["loc", "iloc"])
    def test_setitem_split_path_mismatched_tuples_arrow_list_column(self, indexer):
        # GH#65264 the per-cell hatch keys on the target being a single column,
        # not on it being object dtype
        pa = pytest.importorskip("pyarrow")
        dtype = pd.ArrowDtype(pa.list_(pa.int64()))
        df = pd.DataFrame({"a": pd.array([[9], [9]], dtype=dtype), "b": [0, 0]})
        value = [(1, 2), (3, 4)]
        if indexer == "loc":
            df.loc[:, ["a"]] = value
        else:
            df.iloc[:, [0]] = value

        expected = pd.DataFrame(
            {"a": pd.array([[1, 2], [3, 4]], dtype=dtype), "b": [0, 0]}
        )
        tm.assert_frame_equal(df, expected)

    @pytest.mark.parametrize(
        "box",
        [np.array, pd.Series, pd.Index, pd.array, tuple],
        ids=["ndarray", "Series", "Index", "pd.array", "tuple"],
    )
    @pytest.mark.parametrize("indexer", ["loc", "iloc"])
    def test_setitem_split_path_list_of_len1_arrays_single_column(self, indexer, box):
        # GH#64230 len-1 rows into a single column is the shape that *does*
        # match, the counterpart to the mismatch above. Tuples go this route
        # too, like every other box (GH#65264)
        df = pd.DataFrame({"a": np.zeros(2), "b": [0, 0]})
        value = [box([1]), box([2])]
        if indexer == "loc":
            df.loc[:, ["a"]] = value
        else:
            df.iloc[:, [0]] = value

        expected = pd.DataFrame({"a": [1.0, 2.0], "b": [0, 0]})
        tm.assert_frame_equal(df, expected)

    @pytest.mark.parametrize(
        "box",
        [np.array, pd.Series, pd.Index, pd.array],
        ids=["ndarray", "Series", "Index", "pd.array"],
    )
    def test_setitem_split_path_list_of_arrays_expansion(self, box):
        # GH#64230 rows are split across the target columns when one of them is
        # created by the setitem
        df = pd.DataFrame({"a": [0.0, 0.0]})
        # spell the rows int64: the row dtype carries into the created column,
        # and a bare np.array would be int32 on 32-bit platforms
        df.loc[:, ["a", "new"]] = [
            box(np.array([1, 2], dtype=np.int64)),
            box(np.array([3, 4], dtype=np.int64)),
        ]

        expected = pd.DataFrame({"a": [1.0, 3.0], "new": [2, 4]})
        tm.assert_frame_equal(df, expected)

    @pytest.mark.parametrize(
        "box", [list, deque, tuple], ids=["list", "deque", "tuple"]
    )
    def test_setitem_split_path_heterogeneous_rows(self, box):
        # GH#64230 mixed element types survive the row unwrap, and do so
        # identically whichever container the row arrives in
        df = pd.DataFrame(
            {
                "a": np.zeros(2, dtype=object),
                "b": np.zeros(2, dtype=object),
                "z": np.zeros(2, dtype="int64"),
            }
        )
        df.iloc[:, [0, 1]] = [box([1, "x"]), box([2, "y"])]

        expected = pd.DataFrame(
            {
                "a": pd.Series([1, 2], dtype=object),
                "b": pd.Series(["x", "y"], dtype=object),
                "z": np.zeros(2, dtype="int64"),
            }
        )
        tm.assert_frame_equal(df, expected)

    @pytest.mark.parametrize("indexer", ["loc", "iloc"])
    def test_setitem_split_path_list_of_masked_arrays(self, indexer):
        # GH#64230 a masked row keeps pd.NA; np.asarray would make it nan
        df = pd.DataFrame(
            {
                "a": pd.Series([0, 0], dtype="Int64"),
                "b": pd.Series([0, 0], dtype="Int64"),
            }
        )
        value = [
            pd.array([1, pd.NA], dtype="Int64"),
            pd.array([pd.NA, 4], dtype="Int64"),
        ]
        if indexer == "loc":
            df.loc[:, ["a", "b"]] = value
        else:
            df.iloc[:, [0, 1]] = value

        expected = pd.DataFrame(
            {
                "a": pd.array([1, pd.NA], dtype="Int64"),
                "b": pd.array([pd.NA, 4], dtype="Int64"),
            }
        )
        tm.assert_frame_equal(df, expected)

    @pytest.mark.parametrize("chunked", [False, True])
    @pytest.mark.parametrize("indexer", ["loc", "iloc"])
    def test_setitem_split_path_list_of_pyarrow_arrays(self, indexer, chunked):
        # GH#64230 a foreign array indexes positionally but hands back a boxed
        # scalar, so rows go through np.asarray instead of row[i]; with an
        # object dtype target the boxed scalars would be stored silently
        pa = pytest.importorskip("pyarrow")
        if chunked:
            value = [pa.chunked_array([["p", "q"]]), pa.chunked_array([["r", "s"]])]
        else:
            value = [pa.array(["p", "q"]), pa.array(["r", "s"])]
        # "z" keeps the frame multi-block, so this takes the split path
        df = pd.DataFrame(
            {
                "a": np.zeros(2, dtype=object),
                "b": np.zeros(2, dtype=object),
                "z": np.zeros(2, dtype="int64"),
            }
        )
        if indexer == "loc":
            df.loc[:, ["a", "b"]] = value
        else:
            df.iloc[:, [0, 1]] = value

        expected = pd.DataFrame(
            {
                "a": pd.Series(["p", "r"], dtype=object),
                "b": pd.Series(["q", "s"], dtype=object),
                "z": np.zeros(2, dtype="int64"),
            }
        )
        tm.assert_frame_equal(df, expected)

    @pytest.mark.parametrize(
        "box",
        [np.array, pd.Series, pd.Index, pd.array],
        ids=["ndarray", "Series", "Index", "pd.array"],
    )
    @pytest.mark.parametrize("indexer", ["loc", "iloc"])
    def test_setitem_split_path_list_of_arrays_object_cell(self, indexer, box):
        # GH#64230 being a 2D value takes precedence over the nested-data-into-
        # a-single-object-cell branch, so this is a shape mismatch rather than
        # a list stored in df.iloc[0, 0]
        df = pd.DataFrame({"a": np.zeros(2, dtype=object), "b": [0, 0]})
        value = [box([1, 2]), box([3, 4])]
        msg = "Must have equal len keys and value when setting with an ndarray"
        with pytest.raises(ValueError, match=msg):
            if indexer == "loc":
                df.loc[0, "a"] = value
            else:
                df.iloc[0, 0] = value

    @pytest.mark.parametrize(
        "box",
        [deque, lambda data: std_array("q", data), DuckSequence],
        ids=["deque", "array.array", "duck"],
    )
    @pytest.mark.parametrize("indexer", ["loc", "iloc"])
    def test_setitem_split_path_list_of_sequences(self, indexer, box):
        # GH#64230 rows need not be array-likes; any sized, subscriptable,
        # non-mapping sequence is a row, as it was when this went through
        # np.asarray
        df = pd.DataFrame({"a": [0.0, 0.0], "b": np.zeros(2, dtype="int64")})
        value = [box([1, 2]), box([3, 4])]
        if indexer == "loc":
            df.loc[:, ["a", "b"]] = value
        else:
            df.iloc[:, [0, 1]] = value

        expected = pd.DataFrame({"a": [1.0, 3.0], "b": [2, 4]})
        tm.assert_frame_equal(df, expected)

    @pytest.mark.parametrize("indexer", ["loc", "iloc"])
    def test_setitem_split_path_list_of_ranges(self, indexer):
        # GH#64230 range is a sequence, so a list of them is a list of rows
        df = pd.DataFrame({"a": [0.0, 0.0], "b": np.zeros(2, dtype="int64")})
        if indexer == "loc":
            df.loc[:, ["a", "b"]] = [range(1, 3), range(3, 5)]
        else:
            df.iloc[:, [0, 1]] = [range(1, 3), range(3, 5)]

        expected = pd.DataFrame({"a": [1.0, 3.0], "b": [2, 4]})
        tm.assert_frame_equal(df, expected)

    @pytest.mark.parametrize(
        "value",
        [
            [{1, 2}, {3, 4}],
            [dict.fromkeys([1, 2]), dict.fromkeys([3, 4])],
            # bytes and classes are sized and subscriptable, but np.asarray
            #  never treated them as rows either
            [b"ab", b"cd"],
            [list, dict],
            [GetitemOnly(), GetitemOnly()],
        ],
        ids=["set", "dict", "bytes", "type", "unsized"],
    )
    @pytest.mark.parametrize("indexer", ["loc", "iloc"])
    def test_setitem_split_path_list_of_non_sequences(self, indexer, value):
        # GH#64230 these are not subscriptable non-mapping sequences, so they
        # are values for one column each rather than rows
        df = pd.DataFrame(
            {"a": np.zeros(2, dtype=object), "b": np.zeros(2, dtype=object)}
        )
        if indexer == "loc":
            df.loc[0, ["a", "b"]] = value
        else:
            df.iloc[0, [0, 1]] = value

        expected = pd.DataFrame(
            {
                "a": np.array([value[0], 0], dtype=object),
                "b": np.array([value[1], 0], dtype=object),
            }
        )
        tm.assert_frame_equal(df, expected)

    @pytest.mark.parametrize("indexer", ["loc", "iloc"])
    def test_setitem_split_path_list_of_0d_arrays(self, indexer):
        # GH#64230 0-d array elements are scalars, not rows, so a list of
        # them is not a 2D value; set one scalar per column
        df = pd.DataFrame({"a": [0.0, 0.0], "b": [0, 0], "c": [0.0, 0.0]})
        value = [np.array(1.0), np.array(2.0)]
        if indexer == "loc":
            df.loc[0, ["a", "c"]] = value
        else:
            df.iloc[0, [0, 2]] = value

        expected = pd.DataFrame({"a": [1.0, 0.0], "b": [0, 0], "c": [2.0, 0.0]})
        tm.assert_frame_equal(df, expected)

    @pytest.mark.parametrize("indexer", ["loc", "iloc"])
    def test_setitem_split_path_list_of_2d_arrays(self, indexer):
        # GH#64230 2-D array elements make the value 3D, not 2D; they get
        # stored per-cell in an object column
        df = pd.DataFrame({"a": np.zeros(2, dtype=object), "b": [0, 0]})
        value = [np.ones((2, 2)), np.zeros((2, 2))]
        if indexer == "loc":
            df.loc[:, ["a"]] = value
        else:
            df.iloc[:, [0]] = value

        result = df["a"].tolist()
        assert len(result) == 2
        tm.assert_numpy_array_equal(result[0], value[0])
        tm.assert_numpy_array_equal(result[1], value[1])

    @pytest.mark.parametrize("has_ref", [True, False])
    @pytest.mark.parametrize("indexer", [[0], slice(None, 1, None), np.array([0])])
    @pytest.mark.parametrize("value", [["Z"], np.array(["Z"])])
    def test_iloc_setitem_with_scalar_index(self, has_ref, indexer, value):
        # GH #19474
        # assigning like "df.iloc[0, [0]] = ['Z']" should be evaluated
        # elementwisely, not using "setter('A', ['Z'])".

        # Set object type to avoid upcast when setting "Z"
        df = pd.DataFrame([[1, 2], [3, 4]], columns=["A", "B"]).astype({"A": object})
        if has_ref:
            view = df[:]  # noqa: F841
        df.iloc[0, indexer] = value
        result = df.iloc[0, 0]

        assert is_scalar(result) and result == "Z"

    @pytest.mark.filterwarnings("ignore::UserWarning")
    def test_iloc_mask(self):
        # GH 60994, iloc with a mask (of a series) should return accordingly
        df = pd.DataFrame(list(range(5)), index=list("ABCDE"), columns=["a"])
        mask = df.a % 2 == 0
        msg = "iLocation based boolean indexing cannot use an indexable as a mask"
        with pytest.raises(ValueError, match=msg):
            df.iloc[mask]

        mask.index = range(len(mask))
        msg = "Unalignable boolean Series provided as indexer"
        with pytest.raises(IndexingError, match=msg):
            df.iloc[mask]

        # ndarray ok
        result = df.iloc[np.array([True] * len(mask), dtype=bool)]
        tm.assert_frame_equal(result, df)

        # the possibilities
        locs = np.arange(4)
        nums = 2**locs
        reps = [bin(num) for num in nums]
        df = pd.DataFrame({"locs": locs, "nums": nums}, reps)

        expected = {
            (None, ""): "0b1100",
            (None, ".loc"): "0b1100",
            (None, ".iloc"): "0b1100",
            ("index", ""): "0b11",
            ("index", ".loc"): "0b11",
            (
                "index",
                ".iloc",
            ): "iLocation based boolean indexing cannot use an indexable as a mask",
            ("locs", ""): "Unalignable boolean Series provided as indexer",
            ("locs", ".loc"): "Unalignable boolean Series provided as indexer",
            ("locs", ".iloc"): "Unalignable boolean Series provided as indexer",
        }

        # UserWarnings from reindex of a boolean mask
        for idx in [None, "index", "locs"]:
            mask = (df.nums > 2).values
            if idx:
                mask_index = getattr(df, idx)[::-1]
                mask = pd.Series(mask, list(mask_index))
            for method in ["", ".loc", ".iloc"]:
                try:
                    if method:
                        accessor = getattr(df, method[1:])
                    else:
                        accessor = df
                    answer = str(bin(accessor[mask]["nums"].sum()))
                except (ValueError, IndexingError) as err:
                    answer = str(err)

                key = (
                    idx,
                    method,
                )
                expected_result = expected.get(key)

                # Fix the assertion to check for substring match
                if (
                    idx is None or (idx == "index" and method != ".iloc")
                ) and "0b" in expected_result:
                    # For successful numeric results, exact match is needed
                    assert expected_result == answer, (
                        f"[{key}] does not match [{answer}]"
                    )
                else:
                    # For error messages, substring match is sufficient
                    assert expected_result in answer, f"[{key}] not found in [{answer}]"

    def test_iloc_with_numpy_bool_array(self):
        df = pd.DataFrame(list(range(5)), index=list("ABCDE"), columns=["a"])
        result = df.iloc[np.array([True, False, True, False, True], dtype=bool)]
        expected = pd.DataFrame({"a": [0, 2, 4]}, index=["A", "C", "E"])
        tm.assert_frame_equal(result, expected)

    def test_iloc_series_mask_with_index_mismatch_raises(self):
        df = pd.DataFrame(list(range(5)), index=list("ABCDE"), columns=["a"])
        mask = df.a % 2 == 0
        msg = "Unalignable boolean Series provided as indexer"
        with pytest.raises(IndexingError, match=msg):
            df.iloc[pd.Series([True] * len(mask), dtype=bool)]

    def test_iloc_series_mask_all_true(self):
        df = pd.DataFrame(list(range(5)), columns=["a"])
        mask = pd.Series([True] * len(df), dtype=bool)
        result = df.iloc[mask]
        tm.assert_frame_equal(result, df)

    def test_iloc_series_mask_alternate_true(self):
        df = pd.DataFrame(list(range(5)), columns=["a"])
        mask = pd.Series([True, False, True, False, True], dtype=bool)
        result = df.iloc[mask]
        expected = pd.DataFrame({"a": [0, 2, 4]}, index=[0, 2, 4])
        tm.assert_frame_equal(result, expected)

    def test_iloc_non_unique_indexing(self):
        # GH 4017, non-unique indexing (on the axis)
        df = pd.DataFrame({"A": [0.1] * 3000, "B": [1] * 3000})
        idx = np.arange(30) * 99
        expected = df.iloc[idx]

        df3 = pd.concat([df, 2 * df, 3 * df])
        result = df3.iloc[idx]

        tm.assert_frame_equal(result, expected)

        df2 = pd.DataFrame({"A": [0.1] * 1000, "B": [1] * 1000})
        df2 = pd.concat([df2, 2 * df2, 3 * df2])

        with pytest.raises(KeyError, match="not in index"):
            df2.loc[idx]

    def test_iloc_empty_list_indexer_is_ok(self):
        df = pd.DataFrame(
            np.ones((5, 2)),
            index=pd.Index([f"i-{i}" for i in range(5)], name="a"),
            columns=pd.Index([f"i-{i}" for i in range(2)], name="a"),
        )
        # vertical empty
        tm.assert_frame_equal(
            df.iloc[:, []],
            df.iloc[:, :0],
            check_index_type=True,
            check_column_type=True,
        )
        # horizontal empty
        tm.assert_frame_equal(
            df.iloc[[], :],
            df.iloc[:0, :],
            check_index_type=True,
            check_column_type=True,
        )
        # horizontal empty
        tm.assert_frame_equal(
            df.iloc[[]], df.iloc[:0, :], check_index_type=True, check_column_type=True
        )

    def test_identity_slice_returns_new_object(self):
        # GH13873
        original_df = pd.DataFrame({"a": [1, 2, 3]})
        sliced_df = original_df.iloc[:]
        assert sliced_df is not original_df

        # should be a shallow copy
        assert np.shares_memory(original_df["a"], sliced_df["a"])

        # Setting using .loc[:, "a"] sets inplace so alters both sliced and orig
        # depending on CoW
        original_df.loc[:, "a"] = [4, 4, 4]
        assert (sliced_df["a"] == [1, 2, 3]).all()

        original_series = pd.Series([1, 2, 3, 4, 5, 6])
        sliced_series = original_series.iloc[:]
        assert sliced_series is not original_series

        # should also be a shallow copy
        original_series[:3] = [7, 8, 9]
        # shallow copy not updated (CoW)
        assert all(sliced_series[:3] == [1, 2, 3])

    def test_indexing_zerodim_np_array(self):
        # GH24919
        df = pd.DataFrame([[1, 2], [3, 4]])
        result = df.iloc[np.array(0)]
        s = pd.Series([1, 2], name=0)
        tm.assert_series_equal(result, s)

    def test_series_indexing_zerodim_np_array(self):
        # GH24919
        s = pd.Series([1, 2])
        result = s.iloc[np.array(0)]
        assert result == 1

    def test_iloc_setitem_categorical_updates_inplace(self):
        # Mixed dtype ensures we go through take_split_path in setitem_with_indexer
        cat = pd.Categorical(["A", "B", "C"])
        df = pd.DataFrame({1: cat, 2: [1, 2, 3]}, copy=False)

        assert tm.shares_memory(df[1], cat)

        # With the enforcement of GH#45333 in 2.0, this modifies original
        #  values inplace
        df.iloc[:, 0] = cat[::-1]

        assert tm.shares_memory(df[1], cat)
        expected = pd.Categorical(["C", "B", "A"], categories=["A", "B", "C"])
        tm.assert_categorical_equal(cat, expected)

    def test_iloc_with_boolean_operation(self):
        # GH 20627
        result = pd.DataFrame([[0, 1], [2, 3], [4, 5], [6, np.nan]])
        result.iloc[result.index <= 2] *= 2
        expected = pd.DataFrame([[0, 2], [4, 6], [8, 10], [6, np.nan]])
        tm.assert_frame_equal(result, expected)

        result.iloc[result.index > 2] *= 2
        expected = pd.DataFrame([[0, 2], [4, 6], [8, 10], [12, np.nan]])
        tm.assert_frame_equal(result, expected)

        result.iloc[[True, True, False, False]] *= 2
        expected = pd.DataFrame([[0, 4], [8, 12], [8, 10], [12, np.nan]])
        tm.assert_frame_equal(result, expected)

        result.iloc[[False, False, True, True]] /= 2
        expected = pd.DataFrame([[0, 4.0], [8, 12.0], [4, 5.0], [6, np.nan]])
        tm.assert_frame_equal(result, expected)

    def test_iloc_getitem_singlerow_slice_categoricaldtype_gives_series(self):
        # GH#29521
        df = pd.DataFrame({"x": pd.Categorical("a b c d e".split())})
        result = df.iloc[0]
        raw_cat = pd.Categorical(["a"], categories=["a", "b", "c", "d", "e"])
        expected = pd.Series(raw_cat, index=["x"], name=0, dtype="category")

        tm.assert_series_equal(result, expected)

    def test_iloc_getitem_categorical_values(self):
        # GH#14580
        # test iloc() on Series with Categorical data

        ser = pd.Series([1, 2, 3]).astype("category")

        # get slice
        result = ser.iloc[0:2]
        expected = pd.Series([1, 2]).astype(pd.CategoricalDtype([1, 2, 3]))
        tm.assert_series_equal(result, expected)

        # get list of indexes
        result = ser.iloc[[0, 1]]
        expected = pd.Series([1, 2]).astype(pd.CategoricalDtype([1, 2, 3]))
        tm.assert_series_equal(result, expected)

        # get boolean array
        result = ser.iloc[[True, False, False]]
        expected = pd.Series([1]).astype(pd.CategoricalDtype([1, 2, 3]))
        tm.assert_series_equal(result, expected)

    @pytest.mark.parametrize("value", [None, pd.NaT, np.nan])
    def test_iloc_setitem_td64_values_cast_na(self, value):
        # GH#18586
        series = pd.Series([0, 1, 2], dtype="timedelta64[ns]")
        series.iloc[0] = value
        expected = pd.Series([pd.NaT, 1, 2], dtype="timedelta64[ns]")
        tm.assert_series_equal(series, expected)

    @pytest.mark.parametrize("not_na", [pd.Interval(0, 1), "a", 1.0])
    def test_setitem_mix_of_nan_and_interval(self, not_na, nulls_fixture):
        # GH#27937
        dtype = pd.CategoricalDtype(categories=[not_na])
        ser = pd.Series(
            [nulls_fixture, nulls_fixture, nulls_fixture, nulls_fixture], dtype=dtype
        )
        ser.iloc[:3] = [nulls_fixture, not_na, nulls_fixture]
        exp = pd.Series(
            [nulls_fixture, not_na, nulls_fixture, nulls_fixture], dtype=dtype
        )
        tm.assert_series_equal(ser, exp)

    def test_iloc_setitem_empty_frame_raises_with_3d_ndarray(self):
        idx = pd.Index([])
        obj = pd.DataFrame(
            np.random.default_rng(2).standard_normal((len(idx), len(idx))),
            index=idx,
            columns=idx,
        )
        nd3 = np.random.default_rng(2).integers(5, size=(2, 2, 2))

        msg = f"Cannot set values with ndim > {obj.ndim}"
        with pytest.raises(ValueError, match=msg):
            obj.iloc[nd3] = 0

    def test_iloc_getitem_read_only_values(self, indexer_li):
        # GH#10043 this is fundamentally a test for iloc, but test loc while
        #  we're here
        rw_array = np.eye(10)
        rw_df = pd.DataFrame(rw_array)

        ro_array = np.eye(10)
        ro_array.setflags(write=False)
        ro_df = pd.DataFrame(ro_array)

        tm.assert_frame_equal(
            indexer_li(rw_df)[[1, 2, 3]], indexer_li(ro_df)[[1, 2, 3]]
        )
        tm.assert_frame_equal(indexer_li(rw_df)[[1]], indexer_li(ro_df)[[1]])
        tm.assert_series_equal(indexer_li(rw_df)[1], indexer_li(ro_df)[1])
        tm.assert_frame_equal(indexer_li(rw_df)[1:3], indexer_li(ro_df)[1:3])

    def test_iloc_getitem_readonly_key(self):
        # GH#17192 iloc with read-only array raising TypeError
        df = pd.DataFrame({"data": np.ones(100, dtype="float64")})
        indices = np.array([1, 3, 6])
        indices.flags.writeable = False

        result = df.iloc[indices]
        expected = df.loc[[1, 3, 6]]
        tm.assert_frame_equal(result, expected)

        result = df["data"].iloc[indices]
        expected = df["data"].loc[[1, 3, 6]]
        tm.assert_series_equal(result, expected)

    def test_iloc_assign_series_to_df_cell(self):
        # GH 37593
        df = pd.DataFrame(columns=["a"], index=[0])
        df.iloc[0, 0] = pd.Series([1, 2, 3])
        expected = pd.DataFrame({"a": [pd.Series([1, 2, 3])]}, columns=["a"], index=[0])
        tm.assert_frame_equal(df, expected)

    @pytest.mark.parametrize("klass", [list, np.array])
    def test_iloc_setitem_bool_indexer(self, klass):
        # GH#36741
        df = pd.DataFrame({"flag": ["x", "y", "z"], "value": [1, 3, 4]})
        indexer = klass([True, False, False])
        df.iloc[indexer, 1] = df.iloc[indexer, 1] * 2
        expected = pd.DataFrame({"flag": ["x", "y", "z"], "value": [2, 3, 4]})
        tm.assert_frame_equal(df, expected)

    @pytest.mark.parametrize("has_ref", [True, False])
    @pytest.mark.parametrize("indexer", [[1], slice(1, 2)])
    def test_iloc_setitem_pure_position_based(self, indexer, has_ref):
        # GH#22046
        df1 = pd.DataFrame({"a2": [11, 12, 13], "b2": [14, 15, 16]})
        df2 = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]})
        if has_ref:
            view = df2[:]  # noqa: F841
        df2.iloc[:, indexer] = df1.iloc[:, [0]]
        expected = pd.DataFrame({"a": [1, 2, 3], "b": [11, 12, 13], "c": [7, 8, 9]})
        tm.assert_frame_equal(df2, expected)

    @pytest.mark.parametrize("has_ref", [True, False])
    def test_iloc_setitem_dictionary_value(self, has_ref):
        # GH#37728
        df = pd.DataFrame({"x": [1, 2], "y": [2, 2]})
        if has_ref:
            view = df[:]
        rhs = {"x": 9, "y": 99}
        df.iloc[1] = rhs
        expected = pd.DataFrame({"x": [1, 9], "y": [2, 99]})
        tm.assert_frame_equal(df, expected)

        # GH#38335 same thing, mixed dtypes
        df = pd.DataFrame({"x": [1, 2], "y": [2.0, 2.0]})
        if has_ref:
            view = df[:]  # noqa: F841
        df.iloc[1] = rhs
        expected = pd.DataFrame({"x": [1, 9], "y": [2.0, 99.0]})
        tm.assert_frame_equal(df, expected)

    def test_iloc_getitem_float_duplicates(self):
        df = pd.DataFrame(
            np.random.default_rng(2).standard_normal((3, 3)),
            index=[0.1, 0.2, 0.2],
            columns=list("abc"),
        )
        expect = df.iloc[1:]
        tm.assert_frame_equal(df.loc[0.2], expect)

        expect = df.iloc[1:, 0]
        tm.assert_series_equal(df.loc[0.2, "a"], expect)

        df.index = [1, 0.2, 0.2]
        expect = df.iloc[1:]
        tm.assert_frame_equal(df.loc[0.2], expect)

        expect = df.iloc[1:, 0]
        tm.assert_series_equal(df.loc[0.2, "a"], expect)

        df = pd.DataFrame(
            np.random.default_rng(2).standard_normal((4, 3)),
            index=[1, 0.2, 0.2, 1],
            columns=list("abc"),
        )
        expect = df.iloc[1:-1]
        tm.assert_frame_equal(df.loc[0.2], expect)

        expect = df.iloc[1:-1, 0]
        tm.assert_series_equal(df.loc[0.2, "a"], expect)

        df.index = [0.1, 0.2, 2, 0.2]
        expect = df.iloc[[1, -1]]
        tm.assert_frame_equal(df.loc[0.2], expect)

        expect = df.iloc[[1, -1], 0]
        tm.assert_series_equal(df.loc[0.2, "a"], expect)

    def test_iloc_setitem_custom_object(self):
        # iloc with an object
        class TO:
            def __init__(self, value) -> None:
                self.value = value

            def __str__(self) -> str:
                return f"[{self.value}]"

            __repr__ = __str__

            def __eq__(self, other) -> bool:
                return self.value == other.value

            def view(self):
                return self

        df = pd.DataFrame(index=[0, 1], columns=[0])
        df.iloc[1, 0] = TO(1)
        df.iloc[1, 0] = TO(2)

        result = pd.DataFrame(index=[0, 1], columns=[0])
        result.iloc[1, 0] = TO(2)

        tm.assert_frame_equal(result, df)

        # remains object dtype even after setting it back
        df = pd.DataFrame(index=[0, 1], columns=[0])
        df.iloc[1, 0] = TO(1)
        df.iloc[1, 0] = np.nan
        result = pd.DataFrame(index=[0, 1], columns=[0])

        tm.assert_frame_equal(result, df)

    def test_iloc_getitem_with_duplicates(self):
        df = pd.DataFrame(
            np.random.default_rng(2).random((3, 3)),
            columns=list("ABC"),
            index=list("aab"),
        )

        result = df.iloc[0]
        assert isinstance(result, pd.Series)
        tm.assert_almost_equal(result.values, df.values[0])

        result = df.T.iloc[:, 0]
        assert isinstance(result, pd.Series)
        tm.assert_almost_equal(result.values, df.values[0])

    def test_iloc_getitem_with_duplicates2(self):
        # GH#2259
        df = pd.DataFrame([[1, 2, 3], [4, 5, 6]], columns=[1, 1, 2])
        result = df.iloc[:, [0]]
        expected = df.take([0], axis=1)
        tm.assert_frame_equal(result, expected)

    def test_iloc_interval(self):
        # GH#17130
        df = pd.DataFrame({pd.Interval(1, 2): [1, 2]})

        result = df.iloc[0]
        expected = pd.Series({pd.Interval(1, 2): 1}, name=0)
        tm.assert_series_equal(result, expected)

        result = df.iloc[:, 0]
        expected = pd.Series([1, 2], name=pd.Interval(1, 2))
        tm.assert_series_equal(result, expected)

        result = df.copy()
        result.iloc[:, 0] += 1
        expected = pd.DataFrame({pd.Interval(1, 2): [2, 3]})
        tm.assert_frame_equal(result, expected)

    @pytest.mark.parametrize("indexing_func", [list, np.array])
    @pytest.mark.parametrize("rhs_func", [list, np.array])
    def test_loc_setitem_boolean_list(self, rhs_func, indexing_func):
        # GH#20438 testing specifically list key, not arraylike
        ser = pd.Series([0, 1, 2])
        ser.iloc[indexing_func([True, False, True])] = rhs_func([5, 10])
        expected = pd.Series([5, 1, 10])
        tm.assert_series_equal(ser, expected)

        df = pd.DataFrame({"a": [0, 1, 2]})
        df.iloc[indexing_func([True, False, True])] = rhs_func([[5], [10]])
        expected = pd.DataFrame({"a": [5, 1, 10]})
        tm.assert_frame_equal(df, expected)

    def test_iloc_getitem_slice_negative_step_ea_block(self):
        # GH#44551
        df = pd.DataFrame({"A": [1, 2, 3]}, dtype="Int64")

        res = df.iloc[:, ::-1]
        tm.assert_frame_equal(res, df)

        df["B"] = "foo"
        res = df.iloc[:, ::-1]
        expected = pd.DataFrame({"B": df["B"], "A": df["A"]})
        tm.assert_frame_equal(res, expected)

    @pytest.mark.parametrize(
        "indexer, values, expected_values",
        [
            (slice(None, None, -2), [11, 5, 15], [15, 13, 5, 9, 11]),
            (slice(None, None, -1), [11, 5, 15, 9, 3], [3, 9, 15, 5, 11]),
            (slice(3, None, -1), [11, 5, 15, 9], [9, 15, 5, 11, 3]),
        ],
    )
    def test_iloc_setitem_slice_negative_step_none_bounds(
        self, indexer, values, expected_values
    ):
        # GH#66100 length_of_indexer previously returned a negative length
        # for negative-step slices with start/stop left as None, causing
        # a spurious "different length than the value" ValueError
        ser = pd.Series([2, 13, 7, 9, 3], index=["a", "b", "c", "d", "e"])
        ser.iloc[indexer] = values
        expected = pd.Series(expected_values, index=["a", "b", "c", "d", "e"])
        tm.assert_series_equal(ser, expected)

    def test_iloc_setitem_slice_negative_step_full_reverse(self):
        # GH#66100
        ser = pd.Series([2, 13, 7, 9, 3], index=["a", "b", "c", "d", "e"])
        ser.iloc[::-2] = [11, 5, 15]
        expected = pd.Series([15, 13, 5, 9, 11], index=["a", "b", "c", "d", "e"])
        tm.assert_series_equal(ser, expected)

    def test_iloc_setitem_2d_ndarray_into_ea_block(self):
        # GH#44703
        df = pd.DataFrame({"status": ["a", "b", "c"]}, dtype="category")
        df.iloc[np.array([0, 1]), np.array([0])] = np.array([["a"], ["a"]])

        expected = pd.DataFrame({"status": ["a", "a", "c"]}, dtype=df["status"].dtype)
        tm.assert_frame_equal(df, expected)

    def test_iloc_getitem_int_single_ea_block_view(self):
        # GH#45241
        # TODO: make an extension interface test for this?
        arr = pd.interval_range(1, 10.0)._values
        df = pd.DataFrame(arr, copy=False)

        # ser should be a *view* on the DataFrame data
        ser = df.iloc[2]

        # if we have a view, then changing arr[2] should also change ser[0]
        assert arr[2] != arr[-1]  # otherwise the rest isn't meaningful
        arr[2] = arr[-1]
        assert ser[0] == arr[-1]

    def test_iloc_setitem_multicolumn_to_datetime(self, using_infer_string):
        # GH#20511
        df = pd.DataFrame({"A": ["2022-01-01", "2022-01-02"], "B": ["2021", "2022"]})

        if using_infer_string:
            with pytest.raises(TypeError, match="Invalid value"):
                df.iloc[:, [0]] = pd.DataFrame({"A": pd.to_datetime(["2021", "2022"])})
        else:
            df.iloc[:, [0]] = pd.DataFrame({"A": pd.to_datetime(["2021", "2022"])})
            expected = pd.DataFrame(
                {
                    "A": [
                        pd.Timestamp("2021-01-01 00:00:00"),
                        pd.Timestamp("2022-01-01 00:00:00"),
                    ],
                    "B": ["2021", "2022"],
                }
            )
            tm.assert_frame_equal(df, expected, check_dtype=False)


class TestILocErrors:
    # NB: this test should work for _any_ Series we can pass as
    #  series_with_simple_index
    def test_iloc_float_raises(self, series_with_simple_index, frame_or_series):
        # GH#4892
        # float_indexers should raise exceptions
        # on appropriate Index types & accessors
        # this duplicates the code below
        # but is specifically testing for the error
        # message

        obj = series_with_simple_index
        if frame_or_series is pd.DataFrame:
            obj = obj.to_frame()

        msg = "Cannot index by location index with a non-integer key"
        with pytest.raises(TypeError, match=msg):
            obj.iloc[3.0]

        with pytest.raises(IndexError, match=_slice_iloc_msg):
            obj.iloc[3.0] = 0

    @pytest.mark.parametrize("has_ref", [True, False])
    def test_iloc_getitem_setitem_fancy_exceptions(self, float_frame, has_ref):
        with pytest.raises(IndexingError, match="Too many indexers"):
            float_frame.iloc[:, :, :]

        if has_ref:
            view = float_frame[:]  # noqa: F841
        with pytest.raises(IndexError, match="too many indices for array"):
            # GH#32257 we let numpy do validation, get their exception
            float_frame.iloc[:, :, :] = 1

    def test_iloc_frame_indexer(self):
        # GH#39004
        df = pd.DataFrame({"a": [1, 2, 3]})
        indexer = pd.DataFrame({"a": [True, False, True]})
        msg = "DataFrame indexer for .iloc is not supported. Consider using .loc"
        with pytest.raises(TypeError, match=msg):
            df.iloc[indexer] = 1

        msg = (
            "DataFrame indexer is not allowed for .iloc\n"
            "Consider using .loc for automatic alignment."
        )
        with pytest.raises(IndexError, match=msg):
            df.iloc[indexer]


class TestILocSetItemDuplicateColumns:
    def test_iloc_setitem_scalar_duplicate_columns(self):
        # GH#15686, duplicate columns and mixed dtype
        df1 = pd.DataFrame([{"A": None, "B": 1}, {"A": 2, "B": 2}])
        df2 = pd.DataFrame([{"A": 3, "B": 3}, {"A": 4, "B": 4}])
        df = pd.concat([df1, df2], axis=1)
        df.iloc[0, 0] = -1

        assert df.iloc[0, 0] == -1
        assert df.iloc[0, 2] == 3
        assert df.dtypes.iloc[2] == np.int64

    def test_iloc_setitem_list_duplicate_columns(self):
        # GH#22036 setting with same-sized list
        df = pd.DataFrame([[0, "str", "str2"]], columns=["a", "b", "b"])

        df.iloc[:, 2] = ["str3"]

        expected = pd.DataFrame([[0, "str", "str3"]], columns=["a", "b", "b"])
        tm.assert_frame_equal(df, expected)

    def test_iloc_setitem_series_duplicate_columns(self):
        df = pd.DataFrame(
            np.arange(8, dtype=np.int64).reshape(2, 4), columns=["A", "B", "A", "B"]
        )
        df.iloc[:, 0] = df.iloc[:, 0].astype(np.float64)
        assert df.dtypes.iloc[2] == np.int64

    @pytest.mark.parametrize(
        ["dtypes", "init_value", "expected_value"],
        [("int64", "0", 0), ("float", "1.2", 1.2)],
    )
    def test_iloc_setitem_dtypes_duplicate_columns(
        self, dtypes, init_value, expected_value
    ):
        # GH#22035
        df = pd.DataFrame(
            [[init_value, "str", "str2"]], columns=["a", "b", "b"], dtype=object
        )

        # with the enforcement of GH#45333 in 2.0, this sets values inplace,
        #  so we retain object dtype
        df.iloc[:, 0] = df.iloc[:, 0].astype(dtypes)

        expected_df = pd.DataFrame(
            [[expected_value, "str", "str2"]],
            columns=["a", "b", "b"],
            dtype=object,
        )
        tm.assert_frame_equal(df, expected_df)


class TestILocCallable:
    def test_frame_iloc_getitem_callable(self):
        # GH#11485
        df = pd.DataFrame({"X": [1, 2, 3, 4], "Y": list("aabb")}, index=list("ABCD"))

        # return location
        res = df.iloc[lambda x: [1, 3]]
        tm.assert_frame_equal(res, df.iloc[[1, 3]])

        res = df.iloc[lambda x: [1, 3], :]
        tm.assert_frame_equal(res, df.iloc[[1, 3], :])

        res = df.iloc[lambda x: [1, 3], lambda x: 0]
        tm.assert_series_equal(res, df.iloc[[1, 3], 0])

        res = df.iloc[lambda x: [1, 3], lambda x: [0]]
        tm.assert_frame_equal(res, df.iloc[[1, 3], [0]])

        # mixture
        res = df.iloc[[1, 3], lambda x: 0]
        tm.assert_series_equal(res, df.iloc[[1, 3], 0])

        res = df.iloc[[1, 3], lambda x: [0]]
        tm.assert_frame_equal(res, df.iloc[[1, 3], [0]])

        res = df.iloc[lambda x: [1, 3], 0]
        tm.assert_series_equal(res, df.iloc[[1, 3], 0])

        res = df.iloc[lambda x: [1, 3], [0]]
        tm.assert_frame_equal(res, df.iloc[[1, 3], [0]])

    def test_frame_iloc_setitem_callable(self):
        # GH#11485
        df = pd.DataFrame(
            {"X": [1, 2, 3, 4], "Y": pd.Series(list("aabb"), dtype=object)},
            index=list("ABCD"),
        )

        # return location
        res = df.copy()
        res.iloc[lambda x: [1, 3]] = 0
        exp = df.copy()
        exp.iloc[[1, 3]] = 0
        tm.assert_frame_equal(res, exp)

        res = df.copy()
        res.iloc[lambda x: [1, 3], :] = -1
        exp = df.copy()
        exp.iloc[[1, 3], :] = -1
        tm.assert_frame_equal(res, exp)

        res = df.copy()
        res.iloc[lambda x: [1, 3], lambda x: 0] = 5
        exp = df.copy()
        exp.iloc[[1, 3], 0] = 5
        tm.assert_frame_equal(res, exp)

        res = df.copy()
        res.iloc[lambda x: [1, 3], lambda x: [0]] = 25
        exp = df.copy()
        exp.iloc[[1, 3], [0]] = 25
        tm.assert_frame_equal(res, exp)

        # mixture
        res = df.copy()
        res.iloc[[1, 3], lambda x: 0] = -3
        exp = df.copy()
        exp.iloc[[1, 3], 0] = -3
        tm.assert_frame_equal(res, exp)

        res = df.copy()
        res.iloc[[1, 3], lambda x: [0]] = -5
        exp = df.copy()
        exp.iloc[[1, 3], [0]] = -5
        tm.assert_frame_equal(res, exp)

        res = df.copy()
        res.iloc[lambda x: [1, 3], 0] = 10
        exp = df.copy()
        exp.iloc[[1, 3], 0] = 10
        tm.assert_frame_equal(res, exp)

        res = df.copy()
        res.iloc[lambda x: [1, 3], [0]] = [-5, -5]
        exp = df.copy()
        exp.iloc[[1, 3], [0]] = [-5, -5]
        tm.assert_frame_equal(res, exp)


class TestILocSeries:
    def test_iloc(self):
        ser = pd.Series(
            np.random.default_rng(2).standard_normal(10), index=list(range(0, 20, 2))
        )
        ser_original = ser.copy()

        for i in range(len(ser)):
            result = ser.iloc[i]
            exp = ser[ser.index[i]]
            tm.assert_almost_equal(result, exp)

        # pass a slice
        result = ser.iloc[slice(1, 3)]
        expected = ser.loc[2:4]
        tm.assert_series_equal(result, expected)

        # test slice is a view
        with tm.assert_produces_warning(None):
            # GH#45324 make sure we aren't giving a spurious FutureWarning
            result[:] = 0
        tm.assert_series_equal(ser, ser_original)

        # list of integers
        result = ser.iloc[[0, 2, 3, 4, 5]]
        expected = ser.reindex(ser.index[[0, 2, 3, 4, 5]])
        tm.assert_series_equal(result, expected)

    def test_iloc_getitem_nonunique(self):
        ser = pd.Series([0, 1, 2], index=[0, 1, 0])
        assert ser.iloc[2] == 2

    def test_iloc_setitem_pure_position_based(self):
        # GH#22046
        ser1 = pd.Series([1, 2, 3])
        ser2 = pd.Series([4, 5, 6], index=[1, 0, 2])
        ser1.iloc[1:3] = ser2.iloc[1:3]
        expected = pd.Series([1, 5, 6])
        tm.assert_series_equal(ser1, expected)

    def test_iloc_nullable_int64_size_1_nan(self):
        # GH 31861
        result = pd.DataFrame({"a": ["test"], "b": [np.nan]})

        ser = pd.Series([pd.NA], name="b", dtype="Int64")
        with pytest.raises(TypeError, match="Invalid value"):
            result.loc[:, "b"] = ser

    def test_iloc_arrow_extension_array(self):
        # GH#61311
        pytest.importorskip("pyarrow")
        df = pd.DataFrame({"a": [1, 2], "c": [0, 2], "d": ["c", "a"]})
        df_arrow = pd.DataFrame(
            {"a": [1, 2], "c": [0, 2], "d": ["c", "a"]}
        ).convert_dtypes(dtype_backend="pyarrow")
        expected = df.iloc[:, df["c"]]
        result = df_arrow.iloc[:, df_arrow["c"]]
        tm.assert_frame_equal(result, expected, check_dtype=False)

    @td.skip_if_no("pyarrow")
    def test_setitem_pyarrow_int_series(self):
        # GH#62462
        ser = pd.Series([1, 2, 3], dtype="int64[pyarrow]")
        idx = pd.Index([0, 1])
        vals = pd.Series([7, 8], dtype="int64[pyarrow]")

        ser.iloc[idx] = vals

        expected = pd.Series([7, 8, 3], dtype="int64[pyarrow]")
        tm.assert_series_equal(ser, expected)


@pytest.mark.parametrize(
    "row_key",
    [
        [0, 1, 2],
        slice(0, 3),
        range(3),
        np.array([True, True, True, False]),
        pd.Series([0, 1, 2]),
        pd.Index([0, 1, 2]),
    ],
)
@pytest.mark.parametrize("col_key", [[1], slice(1, 2), np.array([False, True, False])])
def test_iloc_setitem_single_column_key_1d_value(row_key, col_key):
    # GH#68021 a length-1 column key makes the selection (N, 1), which a
    #  length-N 1-D value could not be broadcast into on a single-block frame
    df = pd.DataFrame(np.zeros((4, 3)), columns=list("abc"))

    df.iloc[row_key, col_key] = [1.0, 2.0, 3.0]

    expected = pd.DataFrame(np.zeros((4, 3)), columns=list("abc"))
    expected["b"] = [1.0, 2.0, 3.0, 0.0]
    tm.assert_frame_equal(df, expected)


def test_iloc_setitem_single_column_key_1d_value_datetimelike():
    # GH#68021 the 2D DatetimeArray block hit the same broadcast failure, but
    #  surfaced it as OutOfBoundsDatetime from the upcast fallback
    df = pd.DataFrame(np.zeros((4, 3)), columns=list("abc")).astype("M8[ns]")

    df.iloc[[0, 1, 2], [1]] = pd.to_datetime([1, 2, 3], unit="ns")

    expected = pd.DataFrame(np.zeros((4, 3)), columns=list("abc")).astype("M8[ns]")
    expected["b"] = pd.to_datetime([1, 2, 3, 0], unit="ns")
    tm.assert_frame_equal(df, expected)


def test_iloc_setitem_single_column_key_list_of_tuples():
    # GH#68021 a list of tuples that do not match the number of selected columns
    #  is per-cell values, and reached the same broadcast failure
    df = pd.DataFrame({col: np.array([1, 2, 3, 4], dtype=object) for col in "abc"})

    df.iloc[[0, 1, 2], [1]] = [(1, 2), (3, 4), (5, 6)]

    expected = pd.DataFrame(
        {col: np.array([1, 2, 3, 4], dtype=object) for col in "abc"}
    )
    expected["b"] = pd.array([(1, 2), (3, 4), (5, 6), 4], dtype=object)
    tm.assert_frame_equal(df, expected)


def test_iloc_setitem_single_column_key_tuples_matching_width_still_2d():
    # GH#68021 length-1 tuples do match the one selected column, so they are a
    #  2-D value and still flatten; the per-cell path must not swallow them
    df = pd.DataFrame({col: np.array([1, 2, 3, 4], dtype=object) for col in "abc"})

    df.iloc[[0, 1, 2], [1]] = [(1,), (3,), (5,)]

    expected = pd.DataFrame(
        {col: np.array([1, 2, 3, 4], dtype=object) for col in "abc"}
    )
    expected["b"] = pd.array([1, 3, 5, 4], dtype=object)
    tm.assert_frame_equal(df, expected)


def test_iloc_setitem_single_column_key_length_mismatch_still_raises():
    # GH#68021 the per-column path must not swallow a genuine length mismatch.
    #  The message is numpy's rather than pandas' because a single-block frame
    #  takes the non-split path; that divergence is deliberate, see GH#65241 and
    #  test_loc_setitem_int_row_length_mismatch_message.
    df = pd.DataFrame(np.zeros((4, 3)), columns=list("abc"))

    with pytest.raises(ValueError, match="setting an array element with a sequence"):
        df.iloc[[0, 1], [1]] = [1.0, 2.0, 3.0]


def test_iloc_setitem_single_column_key_2d_value_unchanged():
    # GH#68021 an (N, 1) value already matched the selection and must keep
    #  taking the whole-block path
    df = pd.DataFrame(np.zeros((4, 3)), columns=list("abc"))

    df.iloc[[0, 1, 2], [1]] = np.array([[1.0], [2.0], [3.0]])

    expected = pd.DataFrame(np.zeros((4, 3)), columns=list("abc"))
    expected["b"] = [1.0, 2.0, 3.0, 0.0]
    tm.assert_frame_equal(df, expected)


def test_iloc_setitem_single_column_key_length_one_value_broadcasts():
    # GH#68021 a length-1 value broadcasts to every selected row; the length
    #  check must not turn that into a per-row assignment
    df = pd.DataFrame(np.zeros((4, 3)), columns=list("abc"))

    df.iloc[[0, 1, 2], [1]] = [9.0]

    expected = pd.DataFrame(np.zeros((4, 3)), columns=list("abc"))
    expected["b"] = [9.0, 9.0, 9.0, 0.0]
    tm.assert_frame_equal(df, expected)


def test_iloc_setitem_scalar_row_multiple_columns_unchanged():
    # GH#68021 a scalar row key with several columns stays row-wise
    df = pd.DataFrame(np.zeros((4, 3)), columns=list("abc"))

    df.iloc[0, [0, 1]] = [5.0, 6.0]

    expected = pd.DataFrame(np.zeros((4, 3)), columns=list("abc"))
    expected.iloc[0, 0] = 5.0
    expected.iloc[0, 1] = 6.0
    tm.assert_frame_equal(df, expected)


def test_iloc_setitem_scalar_row_single_column_unchanged():
    # GH#68021 a scalar row key selects one cell, not a column to fill; this is
    #  the case that reaches the guard's is_scalar(indexer[0]) clause, since
    #  length_of_indexer(0) == 1 == len(value) would otherwise match
    df = pd.DataFrame(np.zeros((4, 3)), columns=list("abc"))

    df.iloc[0, [1]] = [5.0]

    expected = pd.DataFrame(np.zeros((4, 3)), columns=list("abc"))
    expected.iloc[0, 1] = 5.0
    tm.assert_frame_equal(df, expected)


@pytest.mark.parametrize(
    "row_key",
    [
        pd.array([0, 1, 2], dtype="Int64"),
        pd.array([True, True, True, False], dtype="boolean"),
        pd.Categorical([0, 1, 2]),
        (0, 1, 2),
    ],
)
def test_iloc_setitem_single_column_key_row_key_length_of_indexer_cannot_measure(
    row_key,
):
    # GH#68021 length_of_indexer does not know these row keys and raises
    #  AssertionError for them, so they have to keep taking the whole-block
    #  path instead of the per-column one
    df = pd.DataFrame(np.zeros((4, 3)), columns=list("abc"))

    df.iloc[row_key, [1]] = [1.0, 2.0, 3.0]

    expected = pd.DataFrame(np.zeros((4, 3)), columns=list("abc"))
    expected["b"] = [1.0, 2.0, 3.0, 0.0]
    tm.assert_frame_equal(df, expected)


def test_iloc_setitem_boolean_column_key_selecting_one_column():
    # GH#68021 _ensure_iterable_column_indexer leaves a list of bools alone, so
    #  the per-column path would have taken True for a position
    df = pd.DataFrame(np.zeros((5, 1)), columns=["a"])

    df.iloc[range(3), [True]] = [1.0, 2.0, 3.0]

    expected = pd.DataFrame(np.zeros((5, 1)), columns=["a"])
    expected["a"] = [1.0, 2.0, 3.0, 0.0, 0.0]
    tm.assert_frame_equal(df, expected)


@pytest.mark.parametrize("row_key", [[], np.array([False, False, False, False])])
def test_iloc_setitem_single_column_key_empty_selection_is_noop(row_key):
    # GH#68021 an empty selection with an empty value is a no-op, as it already
    #  was on the mixed-dtype split path
    df = pd.DataFrame(np.zeros((4, 3)), columns=list("abc"))

    df.iloc[row_key, [1]] = []

    tm.assert_frame_equal(df, pd.DataFrame(np.zeros((4, 3)), columns=list("abc")))


@pytest.mark.parametrize("row_key", [np.array(0), np.array([[0], [1]])])
def test_iloc_setitem_single_column_key_non_1d_row_key_keeps_clear_error(row_key):
    # GH#68021 length_of_indexer cannot measure these, so they keep taking the
    #  whole-block path and its clearer message
    df = pd.DataFrame(np.zeros((4, 3)), columns=list("abc"))

    with pytest.raises(ValueError, match="Cross index must be 1 dimensional"):
        df.iloc[row_key, [1]] = [1.0, 2.0]


def test_iloc_setitem_out_of_bounds_boolean_column_key_raises():
    # GH#68021 a too-long boolean column key whose True lands past the last
    #  column used to be silently discarded here.  Note this pins the
    #  out-of-bounds position, not the key's length: a too-long key whose True
    #  is in bounds is still accepted, as it already was on the split path.
    df = pd.DataFrame({"a": pd.array([1, 2, 3, 4], dtype="Int64")})

    with pytest.raises(IndexError, match="single positional indexer is out-of-bounds"):
        df.iloc[range(3), np.array([False, True, False])] = [1, 2, 3]


def test_iloc_setitem_null_slice_column_key_single_column_frame():
    # GH#68021 on a one-column frame a null slice also selects exactly one
    #  column, so it hits the same (N, 1) selection
    df = pd.DataFrame({"a": [0.0, 1.0, 2.0, 3.0]})

    df.iloc[[0, 1, 2], :] = [10.0, 20.0, 30.0]

    tm.assert_frame_equal(df, pd.DataFrame({"a": [10.0, 20.0, 30.0, 3.0]}))


def test_iloc_setitem_boolean_index_row_key():
    # GH#68021 on a single-block frame this raised the same
    #  "setting an array element with a sequence" as every other row key; it
    #  reaches length_of_indexer, and so the Index.sum() crash, only via the
    #  new per-column route
    df = pd.DataFrame(np.zeros((4, 3)), columns=list("abc"))

    df.iloc[pd.Index([True, True, True, False]), [1]] = [1.0, 2.0, 3.0]

    expected = pd.DataFrame(np.zeros((4, 3)), columns=list("abc"))
    expected["b"] = [1.0, 2.0, 3.0, 0.0]
    tm.assert_frame_equal(df, expected)


def test_iloc_setitem_boolean_index_row_key_mixed_dtype():
    # GH#68021 this is where the Index.sum() crash was already reachable: the
    #  split path measures the row key, so it raised AttributeError on main
    df = pd.DataFrame({"a": np.zeros(4), "b": np.zeros(4), "c": list("wxyz")})

    df.iloc[pd.Index([True, True, True, False]), [1]] = [1.0, 2.0, 3.0]

    expected = pd.DataFrame({"a": np.zeros(4), "b": np.zeros(4), "c": list("wxyz")})
    expected["b"] = [1.0, 2.0, 3.0, 0.0]
    tm.assert_frame_equal(df, expected)


def test_iloc_setitem_series_boolean_index_row_key():
    # GH#68021 an object-dtype Series reaches length_of_indexer from
    #  Block.setitem's num_set, a different call site than the DataFrame paths
    ser = pd.Series([1, 2, 3, 4], dtype=object)

    ser.iloc[pd.Index([True, True, False, False])] = ["a", "b"]

    tm.assert_series_equal(ser, pd.Series(["a", "b", 3, 4], dtype=object))


def _datetimelike_values(ints, dtype):
    # ``ints`` as the i8 values of ``dtype``, keeping the shape
    if dtype == "period[D]":
        return pd.arrays.PeriodArray(ints.ravel(), dtype=dtype).reshape(ints.shape)
    if dtype == "M8[ns, UTC]":
        values = pd.array(ints.ravel().view("M8[ns]")).tz_localize("UTC")
        return values.reshape(ints.shape)
    return ints.view(dtype)


_ROW_KEYS = [
    1,
    [2, 0],
    slice(None, None, -1),
    np.array([True, False, True, False]),
    [],
    ...,
]
_COL_KEYS = [0, [2, 0], slice(None, None, 2), [2, 1, 0], []]


class TestILocSetItem2DDatetimelikeBlock:
    @pytest.mark.parametrize("dtype", ["M8[s]", "m8[s]"])
    def test_1d_value_broadcasts(self, dtype):
        # GH#68521 the 2D datetimelike block stores columns first, so a 1-D value
        #  was broadcast down rows instead of across the selected columns
        arr = np.arange(12).reshape(4, 3).astype("i8")
        # copy so a write-through to ``arr`` can't make the comparison vacuous
        df = pd.DataFrame(arr.copy().view(dtype), columns=list("abc"))

        df.iloc[[2, 0], ::-1] = np.array([100, 101, 102], dtype="i8").view(dtype)

        arr[[2, 0], ::-1] = [100, 101, 102]
        expected = pd.DataFrame(arr.view(dtype), columns=list("abc"))
        tm.assert_frame_equal(df, expected)

    @pytest.mark.parametrize("dtype", ["M8[s]", "m8[s]", "M8[ns, UTC]", "period[D]"])
    @pytest.mark.parametrize(
        "key",
        [
            (row_key, col_key)
            for row_key in _ROW_KEYS
            for col_key in _COL_KEYS
            if not (isinstance(row_key, int) and isinstance(col_key, int))
        ]
        + [
            (range(2), range(2)),
            ([2, 0],),
            (None,),
            (None, ...),
            (np.array([[1], [2]]),),
            (np.array([[1, 2]]),),
            (slice(None), np.array([[0, 2]])),
        ],
    )
    def test_matches_numeric(self, dtype, key):
        # GH#68521 a 2D datetimelike block writes the cells a NumPy-backed frame
        #  does, and raises where it raises
        ints = np.arange(12, dtype="i8").reshape(4, 3)
        if len(key) == 2 and all(
            isinstance(entry, (list, np.ndarray)) for entry in key
        ):
            # iloc takes the outer product of two list-likes, like np.ix_
            shape = ints[np.ix_(*key)].shape
        else:
            shape = ints[key].shape
        value_shapes = {shape, shape[:1], shape[-1:], shape[::-1]}
        if len(shape) == 2:
            value_shapes |= {(shape[0], 1), (1, *shape)}

        for value_shape in value_shapes:
            numeric = pd.DataFrame(ints.copy())
            df = pd.DataFrame(_datetimelike_values(ints, dtype))
            value = np.arange(100, 100 + np.prod(value_shape), dtype="i8")
            value = value.reshape(value_shape)
            try:
                numeric.iloc[key] = value
            except (ValueError, IndexError):
                with pytest.raises((ValueError, IndexError)):
                    df.iloc[key] = _datetimelike_values(value, dtype)
            else:
                df.iloc[key] = _datetimelike_values(value, dtype)
            expected = pd.DataFrame(_datetimelike_values(numeric.to_numpy(), dtype))
            tm.assert_frame_equal(df, expected)

    @pytest.mark.parametrize(
        "box",
        [
            list,
            tuple,
            pd.Index,
            pd.Series,
            pd.array,
            pd.Categorical,
            lambda x: [str(pd.Timestamp(v)) for v in x],
        ],
    )
    def test_1d_value_not_ndarray(self, box):
        # GH#68521 these spellings of a 1-D value wrote the wrong cells silently too
        arr = np.arange(12).reshape(4, 3).astype("i8")
        df = pd.DataFrame(arr.copy().view("M8[s]"), columns=list("abc"))

        df.iloc[[2, 0], ::2] = box(np.array([100, 101], dtype="i8").view("M8[s]"))

        arr[[2, 0], ::2] = [100, 101]
        expected = pd.DataFrame(arr.view("M8[s]"), columns=list("abc"))
        tm.assert_frame_equal(df, expected)

    @pytest.mark.parametrize("dtype", ["M8[s]", "m8[s]"])
    def test_0d_row_key(self, dtype):
        # GH#68521 a 0-d ndarray row key selects one row, like an int
        arr = np.arange(12).reshape(4, 3).astype("i8")
        df = pd.DataFrame(arr.copy().view(dtype), columns=list("abc"))

        df.iloc[np.array(1)] = np.array([100, 101, 102], dtype="i8").view(dtype)

        arr[1] = [100, 101, 102]
        expected = pd.DataFrame(arr.view(dtype), columns=list("abc"))
        tm.assert_frame_equal(df, expected)

    @pytest.mark.parametrize("col_key", [slice(None, None, -1), [2, 1, 0]])
    def test_shape_mismatch_message(self, col_key):
        # GH#68521 a value that cannot be broadcast is a shape problem; the upcast
        #  fallback reported it as a datetime resolution one
        arr = np.arange(12).reshape(4, 3).astype("i8")
        df = pd.DataFrame(arr.view("M8[s]"), columns=list("abc"))

        msg = (
            r"shape mismatch: value array of shape \(2,\) could not be broadcast "
            r"to indexing result of shape \(2,3\)"
        )
        with pytest.raises(ValueError, match=msg):
            df.iloc[[2, 0], col_key] = np.array([100, 101], dtype="i8").view("M8[s]")

    def test_row_vector_into_one_row(self):
        # GH#68521 numpy drops the row vector's leading length-1 axis; this raised
        #  because the value was transposed into a column vector
        arr = np.arange(12).reshape(4, 3).astype("i8")
        df = pd.DataFrame(arr.copy().view("M8[s]"), columns=list("abc"))

        value = np.array([[100, 101, 102]])
        df.iloc[1] = value.astype("i8").view("M8[s]")

        arr[1] = value
        tm.assert_frame_equal(df, pd.DataFrame(arr.view("M8[s]"), columns=list("abc")))


def test_iloc_setitem_1d_ea_block_shape_mismatch_keeps_its_message():
    # GH#68521 the broadcast message is for 2D blocks only; a Series keeps its
    #  dtype error
    ser = pd.Series(np.arange(4).astype("i8").view("M8[s]"))

    with pytest.raises(TypeError, match="Invalid value"):
        ser.iloc[0] = [1, 2]


@pytest.mark.parametrize(
    "dtype",
    [
        "Int64",
        # string dtypes
        ("python", np.nan),
        pytest.param(("pyarrow", np.nan), marks=td.skip_if_no("pyarrow")),
    ],
)
@pytest.mark.parametrize(
    "box",
    [
        list,
        np.array,
        pytest.param(
            lambda x: pd.Series(x, index=["a"]),
            marks=pytest.mark.xfail(reason="Series indexer fails"),
        ),
        pd.Index,
        pd.array,
    ],
    # [np.array],
    ids=["list", "ndarray", "Series", "Index", "pd.array"],
)
def test_iloc_setitem_single_column_frame_ea_dtype(dtype, box):
    # https://github.com/pandas-dev/pandas/issues/66527
    # column boolean mask that sets into the single column of a 1-column df
    if isinstance(dtype, tuple):
        dtype = pd.StringDtype(*dtype)
    df = pd.DataFrame({"a": pd.array([1, 2, 3], dtype=dtype)})

    # setting with a 2d dataframe
    df.iloc[:, box([True])] = df * 2

    expected = pd.DataFrame({"a": pd.array([1, 2, 3], dtype=dtype) * 2})
    tm.assert_frame_equal(df, expected)

    df.iloc[[0, 1], box([True])] = df.iloc[[0, 1], :] * 2

    arr = pd.array([1, 2, 3], dtype=dtype) * 2
    arr[[0, 1]] = arr[[0, 1]] * 2
    expected = pd.DataFrame({"a": arr})
    tm.assert_frame_equal(df, expected)

    # setting with a scalar
    df = pd.DataFrame({"a": pd.array([1, 2, 3], dtype=dtype)})
    scalar = df.iloc[1, 0]

    df.iloc[:, box([True])] = scalar

    expected = pd.DataFrame({"a": pd.array([scalar] * 3, dtype=dtype)})
    tm.assert_frame_equal(df, expected)

    df.iloc[[0, 1], box([True])] = scalar * 2

    expected = pd.DataFrame(
        {"a": pd.array([scalar * 2, scalar * 2, scalar], dtype=dtype)}
    )
    tm.assert_frame_equal(df, expected)
