import contextlib
from datetime import (
    date,
    datetime,
)
from decimal import Decimal

import numpy as np
import pytest

from pandas.compat import HAS_PYARROW
from pandas.errors import (
    IndexingError,
    Pandas4Warning,
)

from pandas.core.dtypes.common import is_list_like

import pandas as pd
import pandas._testing as tm

from pandas.tseries.offsets import BDay


class TestSetitemDT64Values:
    def test_setitem_none_nan(self):
        series = pd.Series(pd.date_range("1/1/2000", periods=10, unit="ns"))
        series[3] = None
        assert series[3] is pd.NaT

        series[3:5] = None
        assert series[4] is pd.NaT

        series[5] = np.nan
        assert series[5] is pd.NaT

        series[5:7] = np.nan
        assert series[6] is pd.NaT

    def test_setitem_multiindex_empty_slice(self):
        # https://github.com/pandas-dev/pandas/issues/35878
        idx = pd.MultiIndex.from_tuples([("a", 1), ("b", 2)])
        result = pd.Series([1, 2], index=idx)
        expected = result.copy()
        result.loc[[]] = 0
        tm.assert_series_equal(result, expected)

    def test_setitem_with_string_index(self):
        # GH#23451
        # Set object dtype to avoid upcast when setting a date object
        ser = pd.Series([1, 2, 3], index=["Date", "b", "other"], dtype=object)
        ser["Date"] = date(2011, 1, 1)
        assert ser.Date == date(2011, 1, 1)
        assert ser["Date"] == date(2011, 1, 1)

    def test_setitem_tuple_with_datetimetz_values(self):
        # GH#20441
        arr = pd.date_range("2017", periods=4, tz="US/Eastern", unit="ns")
        index = [(0, 1), (0, 2), (0, 3), (0, 4)]
        result = pd.Series(arr, index=index)
        expected = result.copy()
        result[(0, 1)] = np.nan
        expected.iloc[0] = np.nan
        tm.assert_series_equal(result, expected)

    @pytest.mark.parametrize("tz", ["US/Eastern", "UTC", "Asia/Tokyo"])
    def test_setitem_with_tz(self, tz, indexer_sli):
        orig = pd.Series(
            pd.date_range("2016-01-01", freq="h", periods=3, tz=tz, unit="ns")
        )
        assert orig.dtype == f"datetime64[ns, {tz}]"

        exp = pd.Series(
            [
                pd.Timestamp("2016-01-01 00:00", tz=tz),
                pd.Timestamp("2011-01-01 00:00", tz=tz),
                pd.Timestamp("2016-01-01 02:00", tz=tz),
            ],
            dtype=orig.dtype,
        )

        # scalar
        ser = orig.copy()
        indexer_sli(ser)[1] = pd.Timestamp("2011-01-01", tz=tz)
        tm.assert_series_equal(ser, exp)

        # vector
        vals = pd.Series(
            [pd.Timestamp("2011-01-01", tz=tz), pd.Timestamp("2012-01-01", tz=tz)],
            index=[1, 2],
            dtype=orig.dtype,
        )
        assert vals.dtype == f"datetime64[ns, {tz}]"

        exp = pd.Series(
            [
                pd.Timestamp("2016-01-01 00:00", tz=tz),
                pd.Timestamp("2011-01-01 00:00", tz=tz),
                pd.Timestamp("2012-01-01 00:00", tz=tz),
            ],
            dtype=orig.dtype,
        )

        ser = orig.copy()
        indexer_sli(ser)[[1, 2]] = vals
        tm.assert_series_equal(ser, exp)

    def test_setitem_with_tz_dst(self, indexer_sli):
        # GH#14146 trouble setting values near DST boundary
        tz = "US/Eastern"
        orig = pd.Series(
            pd.date_range("2016-11-06", freq="h", periods=3, tz=tz, unit="ns")
        )
        assert orig.dtype == f"datetime64[ns, {tz}]"

        exp = pd.Series(
            [
                pd.Timestamp("2016-11-06 00:00-04:00", tz=tz),
                pd.Timestamp("2011-01-01 00:00-05:00", tz=tz),
                pd.Timestamp("2016-11-06 01:00-05:00", tz=tz),
            ],
            dtype=orig.dtype,
        )

        # scalar
        ser = orig.copy()
        indexer_sli(ser)[1] = pd.Timestamp("2011-01-01", tz=tz)
        tm.assert_series_equal(ser, exp)

        # vector
        vals = pd.Series(
            [pd.Timestamp("2011-01-01", tz=tz), pd.Timestamp("2012-01-01", tz=tz)],
            index=[1, 2],
            dtype=orig.dtype,
        )
        assert vals.dtype == f"datetime64[ns, {tz}]"

        exp = pd.Series(
            [
                pd.Timestamp("2016-11-06 00:00", tz=tz),
                pd.Timestamp("2011-01-01 00:00", tz=tz),
                pd.Timestamp("2012-01-01 00:00", tz=tz),
            ],
            dtype=orig.dtype,
        )

        ser = orig.copy()
        indexer_sli(ser)[[1, 2]] = vals
        tm.assert_series_equal(ser, exp)

    def test_object_series_setitem_dt64array_exact_match(self):
        # make sure the dt64 isn't cast by numpy to integers
        # https://github.com/numpy/numpy/issues/12550

        ser = pd.Series({"X": np.nan}, dtype=object)

        indexer = [True]

        # "exact_match" -> size of array being set matches size of ser
        value = np.array([4], dtype="M8[ns]")

        ser.iloc[indexer] = value

        expected = pd.Series([value[0]], index=["X"], dtype=object)
        assert all(isinstance(x, np.datetime64) for x in expected.values)

        tm.assert_series_equal(ser, expected)


class TestSetitemScalarIndexer:
    @pytest.mark.parametrize("dtype", ["S1", "V1"])
    def test_setitem_cannot_hold_na_dtype(self, dtype):
        # GH#52373
        ser = pd.Series(np.array([b"a", b"b", b"c"], dtype=dtype))

        msg = "Invalid value 'nan' for dtype"
        with pytest.raises(TypeError, match=msg):
            ser[1] = np.nan

        expected = pd.Series(np.array([b"a", b"b", b"c"], dtype=dtype))
        tm.assert_series_equal(ser, expected)

    def test_setitem_negative_out_of_bounds(self):
        # As of 3.0, int keys are treated as labels, so this becomes
        #  setitem-with-expansion
        ser = pd.Series(["a"] * 10, index=["a"] * 10)
        ser[-11] = "foo"
        exp = pd.Series(["a"] * 10 + ["foo"], index=["a"] * 10 + [-11])
        tm.assert_series_equal(ser, exp)

    @pytest.mark.parametrize("indexer", [tm.loc, tm.at])
    @pytest.mark.parametrize("ser_index", [0, 1])
    def test_setitem_series_object_dtype(self, indexer, ser_index):
        # GH#38303
        ser = pd.Series([0, 0], dtype="object")
        idxr = indexer(ser)
        idxr[0] = pd.Series([42], index=[ser_index])
        expected = pd.Series([pd.Series([42], index=[ser_index]), 0], dtype="object")
        tm.assert_series_equal(ser, expected)

    @pytest.mark.parametrize("index, exp_value", [(0, 42), (1, np.nan)])
    def test_setitem_series(self, index, exp_value):
        # GH#38303
        ser = pd.Series([0, 0])
        ser.loc[0] = pd.Series([42], index=[index])
        expected = pd.Series([exp_value, 0])
        tm.assert_series_equal(ser, expected)


class TestSetitemSlices:
    def test_setitem_slice_float_raises(self, datetime_series):
        msg = (
            "cannot do slice indexing on DatetimeIndex with these indexers "
            r"\[{key}\] of type float"
        )
        with pytest.raises(TypeError, match=msg.format(key=r"4\.0")):
            datetime_series[4.0:10.0] = 0

        with pytest.raises(TypeError, match=msg.format(key=r"4\.5")):
            datetime_series[4.5:10.0] = 0

    def test_setitem_slice(self):
        ser = pd.Series(range(10), index=list(range(10)))
        ser[-12:] = 0
        assert (ser == 0).all()

        ser[:-12] = 5
        assert (ser == 0).all()

    def test_setitem_slice_integers(self):
        ser = pd.Series(
            np.random.default_rng(2).standard_normal(8),
            index=[2, 4, 6, 8, 10, 12, 14, 16],
        )

        ser[:4] = 0
        assert (ser[:4] == 0).all()
        assert not (ser[4:] == 0).any()

    def test_setitem_slicestep(self):
        # caught this bug when writing tests
        series = pd.Series(
            np.arange(20, dtype=np.float64), index=np.arange(20, dtype=np.int64)
        )

        series[::2] = 0
        assert (series[::2] == 0).all()

    def test_setitem_multiindex_slice(self, indexer_sli):
        # GH 8856
        mi = pd.MultiIndex.from_product(([0, 1], list("abcde")))
        result = pd.Series(np.arange(10, dtype=np.int64), mi)
        indexer_sli(result)[::4] = 100
        expected = pd.Series([100, 1, 2, 3, 100, 5, 6, 7, 100, 9], mi)
        tm.assert_series_equal(result, expected)


class TestSetitemBooleanMask:
    def test_setitem_mask_cast(self):
        # GH#2746
        # need to upcast
        ser = pd.Series([1, 2], index=[1, 2], dtype="int64")
        ser[[True, False]] = pd.Series([0], index=[1], dtype="int64")
        expected = pd.Series([0, 2], index=[1, 2], dtype="int64")

        tm.assert_series_equal(ser, expected)

    def test_setitem_mask_align_and_promote(self):
        # GH#8387: test that changing types does not break alignment
        ts = pd.Series(
            np.random.default_rng(2).standard_normal(100), index=np.arange(100, 0, -1)
        ).round(5)
        mask = ts > 0
        left = ts.copy()
        right = ts[mask].copy().map(str)
        with pytest.raises(TypeError, match="Invalid value"):
            left[mask] = right

    def test_setitem_mask_promote_strs(self):
        ser = pd.Series([0, 1, 2, 0])
        mask = ser > 0
        ser2 = ser[mask].map(str)
        with pytest.raises(TypeError, match="Invalid value"):
            ser[mask] = ser2

    def test_setitem_mask_promote(self):
        ser = pd.Series([0, "foo", "bar", 0])
        mask = pd.Series([False, True, True, False])
        ser2 = ser[mask]
        ser[mask] = ser2

        expected = pd.Series([0, "foo", "bar", 0])
        tm.assert_series_equal(ser, expected)

    def test_setitem_boolean(self, string_series):
        mask = string_series > string_series.median()

        # similar indexed series
        result = string_series.copy()
        result[mask] = string_series * 2
        expected = string_series * 2
        tm.assert_series_equal(result[mask], expected[mask])

        # needs alignment
        result = string_series.copy()
        result[mask] = (string_series * 2)[0:5]
        expected = (string_series * 2)[0:5].reindex_like(string_series)
        expected[-mask] = string_series[mask]
        tm.assert_series_equal(result[mask], expected[mask])

    def test_setitem_boolean_corner(self, datetime_series):
        ts = datetime_series
        mask_shifted = ts.shift(1, freq=BDay()) > ts.median()

        msg = (
            r"Unalignable boolean Series provided as indexer \(index of "
            r"the boolean Series and of the indexed object do not match"
        )
        with pytest.raises(IndexingError, match=msg):
            ts[mask_shifted] = 1

        with pytest.raises(IndexingError, match=msg):
            ts.loc[mask_shifted] = 1

    def test_setitem_boolean_different_order(self, string_series):
        ordered = string_series.sort_values()

        copy = string_series.copy()
        copy[ordered > 0] = 0

        expected = string_series.copy()
        expected[expected > 0] = 0

        tm.assert_series_equal(copy, expected)

    @pytest.mark.parametrize("func", [list, np.array, pd.Series])
    def test_setitem_boolean_python_list(self, func):
        # GH19406
        ser = pd.Series([None, "b", None])
        mask = func([True, False, True])
        ser[mask] = ["a", "c"]
        expected = pd.Series(["a", "b", "c"])
        tm.assert_series_equal(ser, expected)

    def test_setitem_boolean_nullable_int_types(self, any_numeric_ea_dtype):
        # GH: 26468
        ser = pd.Series([5, 6, 7, 8], dtype=any_numeric_ea_dtype)
        ser[ser > 6] = pd.Series(range(4), dtype=any_numeric_ea_dtype)
        expected = pd.Series([5, 6, 2, 3], dtype=any_numeric_ea_dtype)
        tm.assert_series_equal(ser, expected)

        ser = pd.Series([5, 6, 7, 8], dtype=any_numeric_ea_dtype)
        ser.loc[ser > 6] = pd.Series(range(4), dtype=any_numeric_ea_dtype)
        tm.assert_series_equal(ser, expected)

        ser = pd.Series([5, 6, 7, 8], dtype=any_numeric_ea_dtype)
        loc_ser = pd.Series(range(4), dtype=any_numeric_ea_dtype)
        ser.loc[ser > 6] = loc_ser.loc[loc_ser > 1]
        tm.assert_series_equal(ser, expected)

    def test_setitem_with_bool_mask_and_values_matching_n_trues_in_length(self):
        # GH#30567
        ser = pd.Series([None] * 10)
        mask = [False] * 3 + [True] * 5 + [False] * 2
        ser[mask] = range(5)
        result = ser
        expected = pd.Series([None] * 3 + list(range(5)) + [None] * 2, dtype=object)
        tm.assert_series_equal(result, expected)

    def test_setitem_nan_with_bool(self):
        # GH 13034
        result = pd.Series([True, False, True])
        with pytest.raises(TypeError, match="Invalid value"):
            result[0] = np.nan

    def test_setitem_mask_smallint_upcast(self):
        orig = pd.Series([1, 2, 3], dtype="int8")
        alt = np.array([999, 1000, 1001], dtype=np.int64)

        mask = np.array([True, False, True])

        ser = orig.copy()
        with pytest.raises(TypeError, match="Invalid value"):
            ser[mask] = pd.Series(alt)

        with pytest.raises(TypeError, match="Invalid value"):
            ser.mask(mask, alt, inplace=True)

        res = ser.where(~mask, pd.Series(alt))
        expected = pd.Series([999, 2, 1001])
        tm.assert_series_equal(res, expected)

    def test_setitem_mask_smallint_no_upcast(self):
        # like test_setitem_mask_smallint_upcast, but while we can't hold 'alt',
        #  we *can* hold alt[mask] without casting
        orig = pd.Series([1, 2, 3], dtype="uint8")
        alt = pd.Series([245, 1000, 246], dtype=np.int64)

        mask = np.array([True, False, True])

        ser = orig.copy()
        ser[mask] = alt
        expected = pd.Series([245, 2, 246], dtype="uint8")
        tm.assert_series_equal(ser, expected)

        ser2 = orig.copy()
        ser2.mask(mask, alt, inplace=True)
        tm.assert_series_equal(ser2, expected)

        # TODO: ser.where(~mask, alt) unnecessarily upcasts to int64
        ser3 = orig.copy()
        res = ser3.where(~mask, alt)
        tm.assert_series_equal(res, expected, check_dtype=False)


class TestSetitemViewCopySemantics:
    def test_setitem_invalidates_datetime_index_freq(self):
        # GH#24096 altering a datetime64tz Series inplace does not propagate
        #  back to the underlying DatetimeIndex

        dti = pd.date_range("20130101", periods=3, tz="US/Eastern")
        ts = dti[1]
        ser = pd.Series(dti)
        assert ser._values is not dti
        assert ser._values._ndarray.base is dti._data._ndarray.base
        assert dti.freq == "D"
        ser.iloc[1] = pd.NaT

        # check that the DatetimeIndex was not altered in place
        assert ser._values is not dti
        assert ser._values._ndarray.base is not dti._data._ndarray.base
        assert dti[1] == ts
        assert dti.freq == "D"

    def test_dt64tz_setitem_does_not_mutate_dti(self):
        # GH#21907, GH#24096
        dti = pd.date_range("2016-01-01", periods=10, tz="US/Pacific")
        ts = dti[0]
        ser = pd.Series(dti)
        assert ser._values is not dti
        assert ser._values._ndarray.base is dti._data._ndarray.base
        assert ser._mgr.blocks[0].values._ndarray.base is dti._data._ndarray.base

        assert ser._mgr.blocks[0].values is not dti

        ser[::3] = pd.NaT
        assert ser[0] is pd.NaT
        assert dti[0] == ts


class TestSetitemCallable:
    def test_setitem_callable_key(self):
        # GH#12533
        ser = pd.Series([1, 2, 3, 4], index=list("ABCD"))
        ser[lambda x: "A"] = -1

        expected = pd.Series([-1, 2, 3, 4], index=list("ABCD"))
        tm.assert_series_equal(ser, expected)

    def test_setitem_callable_other(self):
        # GH#13299
        inc = lambda x: x + 1

        # set object dtype to avoid upcast when setting inc
        ser = pd.Series([1, 2, -1, 4], dtype=object)
        ser[ser < 0] = inc

        expected = pd.Series([1, 2, inc, 4])
        tm.assert_series_equal(ser, expected)


class TestSetitemWithExpansion:
    def test_setitem_empty_series(self, using_infer_string):
        # GH#10193
        key = pd.Timestamp("2012-01-01")
        series = pd.Series(dtype=object)
        series[key] = 47
        expected = pd.Series(
            47, index=[key] if using_infer_string else pd.Index([key], dtype=object)
        )
        tm.assert_series_equal(series, expected)

    def test_setitem_empty_series_datetimeindex_preserves_freq(self):
        # GH#33573 our index should retain its freq
        dti = pd.DatetimeIndex([], freq="D", dtype="M8[ns]")
        series = pd.Series([], index=dti, dtype=object)
        key = pd.Timestamp("2012-01-01")
        series[key] = 47
        expected = pd.Series(47, pd.DatetimeIndex([key], freq="D").as_unit("ns"))
        tm.assert_series_equal(series, expected)
        assert series.index.freq == expected.index.freq

    def test_setitem_empty_series_timestamp_preserves_dtype(self):
        # GH 21881
        timestamp = pd.Timestamp(1412526600000000000)
        series = pd.Series([timestamp], index=["timestamp"], dtype=object)
        expected = series["timestamp"]

        series = pd.Series([], dtype=object)
        series["anything"] = 300.0
        # the existing dtype is float64 here, so inserting a Timestamp
        #  changes it to object
        with tm.assert_produces_warning(Pandas4Warning, match="incompatible dtype"):
            series["timestamp"] = timestamp
        result = series["timestamp"]
        assert result == expected

    @pytest.mark.parametrize(
        "td",
        [
            pd.Timedelta("9 days"),
            pd.Timedelta("9 days").to_timedelta64(),
            pd.Timedelta("9 days").to_pytimedelta(),
        ],
    )
    def test_append_timedelta_does_not_cast(self, td, using_infer_string, request):
        # GH#22717 inserting a Timedelta should _not_ cast to int64
        expected = pd.Series(["x", td], index=[0, "td"], dtype=object)

        # With infer_string, dtype changes from str -> object; without it,
        # the Series is already object so no dtype change occurs.
        warn = Pandas4Warning if using_infer_string else None

        ser = pd.Series(["x"])
        with tm.assert_produces_warning(warn, match="incompatible dtype"):
            ser["td"] = td
        tm.assert_series_equal(ser, expected)
        assert isinstance(ser["td"], pd.Timedelta)

        ser = pd.Series(["x"])
        with tm.assert_produces_warning(warn, match="incompatible dtype"):
            ser.loc["td"] = pd.Timedelta("9 days")
        tm.assert_series_equal(ser, expected)
        assert isinstance(ser["td"], pd.Timedelta)

    def test_setitem_with_expansion_type_promotion(self):
        # GH#12599
        ser = pd.Series(dtype=object)
        ser["a"] = pd.Timestamp("2016-01-01")
        # After the first insert ser has datetime64 dtype; inserting a float
        # is incompatible with that.
        with tm.assert_produces_warning(Pandas4Warning, match="incompatible dtype"):
            ser["b"] = 3.0
        ser["c"] = "foo"
        expected = pd.Series(
            [pd.Timestamp("2016-01-01"), 3.0, "foo"], index=["a", "b", "c"]
        )
        tm.assert_series_equal(ser, expected)

    def test_setitem_not_contained(self, string_series):
        # set item that's not contained
        ser = string_series.copy()
        assert "foobar" not in ser.index
        ser["foobar"] = 1

        app = pd.Series([1], index=["foobar"], name="series")
        expected = pd.concat([string_series, app])
        tm.assert_series_equal(ser, expected)

    def test_setitem_keep_precision(self, any_numeric_ea_dtype):
        # GH#32346
        ser = pd.Series([1, 2], dtype=any_numeric_ea_dtype)
        ser[2] = 10
        expected = pd.Series([1, 2, 10], dtype=any_numeric_ea_dtype)
        tm.assert_series_equal(ser, expected)

    @pytest.mark.parametrize(
        "na, target_na, dtype, target_dtype, indexer, raises",
        [
            (pd.NA, pd.NA, "Int64", "Int64", 1, False),
            (pd.NA, pd.NA, "Int64", "Int64", 2, False),
            (pd.NA, np.nan, "int64", "float64", 1, False),
            (pd.NA, np.nan, "int64", "float64", 2, False),
            (pd.NaT, pd.NaT, "int64", "object", 1, True),
            (pd.NaT, pd.NaT, "int64", "object", 2, False),
            (np.nan, pd.NA, "Int64", "Int64", 1, False),
            (np.nan, pd.NA, "Int64", "Int64", 2, False),
            (np.nan, pd.NA, "Float64", "Float64", 1, False),
            (np.nan, pd.NA, "Float64", "Float64", 2, False),
            (np.nan, np.nan, "int64", "float64", 1, False),
            (np.nan, np.nan, "int64", "float64", 2, False),
        ],
    )
    def test_setitem_enlarge_with_na(
        self, na, target_na, dtype, target_dtype, indexer, raises
    ):
        # GH#32346
        ser = pd.Series([1, 2], dtype=dtype)
        if raises:
            with pytest.raises(TypeError, match="Invalid value"):
                ser[indexer] = na
        else:
            # GH#62369 enlargement silently changing int64 -> object warns
            warn = Pandas4Warning if indexer == 2 and target_dtype == "object" else None
            with tm.assert_produces_warning(warn, match="incompatible dtype"):
                ser[indexer] = na
            expected_values = [1, target_na] if indexer == 1 else [1, 2, target_na]
            expected = pd.Series(expected_values, dtype=target_dtype)
            tm.assert_series_equal(ser, expected)

    def test_setitem_enlargement_object_none(self, nulls_fixture, using_infer_string):
        # GH#48665
        ser = pd.Series(["a", "b"])

        dtype = (
            "str"
            if using_infer_string
            and not (isinstance(nulls_fixture, Decimal) and not HAS_PYARROW)
            else object
        )

        # Warn when the dtype changes (str -> object for Decimal without
        # pyarrow); no warning when the dtype is preserved.
        warn = Pandas4Warning if ser.dtype != dtype else None
        with tm.assert_produces_warning(warn, match="incompatible dtype"):
            ser[3] = nulls_fixture

        expected = pd.Series(["a", "b", nulls_fixture], index=[0, 1, 3], dtype=dtype)
        tm.assert_series_equal(ser, expected)
        if dtype == "str":
            ser[3] is np.nan
        elif using_infer_string:
            assert ser[3] is nulls_fixture
        else:
            assert type(ser[3]) is type(nulls_fixture)


def test_setitem_scalar_into_readonly_backing_data():
    # GH#14359: test that you cannot mutate a read only buffer

    array = np.zeros(5)
    array.flags.writeable = False  # make the array immutable
    series = pd.Series(array, copy=False)

    for n in series.index:
        msg = "assignment destination is read-only"
        with pytest.raises(ValueError, match=msg):
            series[n] = 1

        assert array[n] == 0


def test_setitem_slice_into_readonly_backing_data():
    # GH#14359: test that you cannot mutate a read only buffer

    array = np.zeros(5)
    array.flags.writeable = False  # make the array immutable
    series = pd.Series(array, copy=False)

    msg = "assignment destination is read-only"
    with pytest.raises(ValueError, match=msg):
        series[1:3] = 1

    assert not array.any()


def test_setitem_categorical_assigning_ops():
    orig = pd.Series(pd.Categorical(["b", "b"], categories=["a", "b"]))
    ser = orig.copy()
    ser[:] = "a"
    exp = pd.Series(pd.Categorical(["a", "a"], categories=["a", "b"]))
    tm.assert_series_equal(ser, exp)

    ser = orig.copy()
    ser[1] = "a"
    exp = pd.Series(pd.Categorical(["b", "a"], categories=["a", "b"]))
    tm.assert_series_equal(ser, exp)

    ser = orig.copy()
    ser[ser.index > 0] = "a"
    exp = pd.Series(pd.Categorical(["b", "a"], categories=["a", "b"]))
    tm.assert_series_equal(ser, exp)

    ser = orig.copy()
    ser[[False, True]] = "a"
    exp = pd.Series(pd.Categorical(["b", "a"], categories=["a", "b"]))
    tm.assert_series_equal(ser, exp)

    ser = orig.copy()
    ser.index = ["x", "y"]
    ser["y"] = "a"
    exp = pd.Series(pd.Categorical(["b", "a"], categories=["a", "b"]), index=["x", "y"])
    tm.assert_series_equal(ser, exp)


def test_setitem_nan_into_categorical():
    # ensure that one can set something to np.nan
    ser = pd.Series(pd.Categorical([1, 2, 3]))
    exp = pd.Series(pd.Categorical([1, np.nan, 3], categories=[1, 2, 3]))
    ser[1] = np.nan
    tm.assert_series_equal(ser, exp)


class TestSetitemCasting:
    @pytest.mark.parametrize("unique", [True, False])
    @pytest.mark.parametrize("val", [3, 3.0, "3"], ids=type)
    def test_setitem_non_bool_into_bool(self, val, indexer_sli, unique):
        # dont cast these 3-like values to bool
        ser = pd.Series([True, False])
        if not unique:
            ser.index = [1, 1]

        with pytest.raises(TypeError, match="Invalid value"):
            indexer_sli(ser)[1] = val

    def test_setitem_boolean_array_into_npbool(self):
        # GH#45462
        ser = pd.Series([True, False, True])
        values = ser._values
        arr = pd.array([True, False, None])

        ser[:2] = arr[:2]  # no NAs -> can set inplace
        assert ser._values is values

        with pytest.raises(TypeError, match="Invalid value"):
            ser[1:] = arr[1:]  # has an NA -> cast to boolean dtype


class SetitemCastingEquivalents:
    """
    Check each of several methods that _should_ be equivalent to `obj[key] = val`

    We assume that
        - obj.index is the default Index(range(len(obj)))
        - the setitem does not expand the obj
    """

    @pytest.fixture
    def is_inplace(self, obj, expected):
        """
        Whether we expect the setting to be in-place or not.
        """
        return expected.dtype == obj.dtype

    def check_indexer(self, obj, key, expected, val, indexer, is_inplace):
        orig = obj
        obj = obj.copy()
        arr = obj._values

        indexer(obj)[key] = val
        tm.assert_series_equal(obj, expected)

        self._check_inplace(is_inplace, orig, arr, obj)

    def _check_inplace(self, is_inplace, orig, arr, obj):
        if is_inplace is None:
            # We are not (yet) checking whether setting is inplace or not
            pass
        elif is_inplace:
            if arr.dtype.kind in ["m", "M"]:
                # We may not have the same DTA/TDA, but will have the same
                #  underlying data
                assert arr._ndarray is obj._values._ndarray
            else:
                assert obj._values is arr
        else:
            # otherwise original array should be unchanged
            tm.assert_equal(arr, orig._values)

    def test_int_key(self, obj, key, expected, raises, val, indexer_sli, is_inplace):
        if not isinstance(key, int):
            pytest.skip("Not relevant for int key")
        if raises:
            ctx = pytest.raises(TypeError, match="Invalid value")
        else:
            ctx = contextlib.nullcontext()

        with ctx:
            self.check_indexer(obj, key, expected, val, indexer_sli, is_inplace)

        if indexer_sli is tm.loc:
            with ctx:
                self.check_indexer(obj, key, expected, val, tm.at, is_inplace)
        elif indexer_sli is tm.iloc:
            with ctx:
                self.check_indexer(obj, key, expected, val, tm.iat, is_inplace)

        rng = range(key, key + 1)
        with ctx:
            self.check_indexer(obj, rng, expected, val, indexer_sli, is_inplace)

        if indexer_sli is not tm.loc:
            # Note: no .loc because that handles slice edges differently
            slc = slice(key, key + 1)
            with ctx:
                self.check_indexer(obj, slc, expected, val, indexer_sli, is_inplace)

        ilkey = [key]
        with ctx:
            self.check_indexer(obj, ilkey, expected, val, indexer_sli, is_inplace)

        indkey = np.array(ilkey)
        with ctx:
            self.check_indexer(obj, indkey, expected, val, indexer_sli, is_inplace)

        genkey = (x for x in [key])
        with ctx:
            self.check_indexer(obj, genkey, expected, val, indexer_sli, is_inplace)

    def test_slice_key(self, obj, key, expected, raises, val, indexer_sli, is_inplace):
        if not isinstance(key, slice):
            pytest.skip("Not relevant for slice key")
        if raises:
            ctx = pytest.raises(TypeError, match="Invalid value")
        else:
            ctx = contextlib.nullcontext()

        if indexer_sli is not tm.loc:
            # Note: no .loc because that handles slice edges differently
            with ctx:
                self.check_indexer(obj, key, expected, val, indexer_sli, is_inplace)

        ilkey = list(range(len(obj)))[key]
        with ctx:
            self.check_indexer(obj, ilkey, expected, val, indexer_sli, is_inplace)

        indkey = np.array(ilkey)
        with ctx:
            self.check_indexer(obj, indkey, expected, val, indexer_sli, is_inplace)

        genkey = (x for x in indkey)
        with ctx:
            self.check_indexer(obj, genkey, expected, val, indexer_sli, is_inplace)

    def test_mask_key(self, obj, key, expected, raises, val, indexer_sli):
        # setitem with boolean mask
        mask = np.zeros(obj.shape, dtype=bool)
        mask[key] = True

        obj = obj.copy()

        if is_list_like(val) and len(val) < mask.sum():
            msg = "boolean index did not match indexed array along dimension"
            with pytest.raises(IndexError, match=msg):
                indexer_sli(obj)[mask] = val
            return

        if raises:
            with pytest.raises(TypeError, match="Invalid value"):
                indexer_sli(obj)[mask] = val
        else:
            indexer_sli(obj)[mask] = val

    def test_series_where(self, obj, key, expected, raises, val, is_inplace):
        mask = np.zeros(obj.shape, dtype=bool)
        mask[key] = True

        if is_list_like(val) and len(val) < len(obj):
            # Series.where is not valid here
            msg = r"Length of values \(\d+\) does not match length of index"
            with pytest.raises(ValueError, match=msg):
                obj.where(~mask, val)
            return

        orig = obj
        obj = obj.copy()
        arr = obj._values

        res = obj.where(~mask, val)

        if val is pd.NA and res.dtype == object:
            expected = expected.fillna(pd.NA)
        elif val is None and res.dtype == object:
            assert expected.dtype == object
            expected = expected.copy()
            expected[expected.isna()] = None
        tm.assert_series_equal(res, expected)

        self._check_inplace(is_inplace, orig, arr, obj)

    def test_index_where(self, obj, key, expected, raises, val):
        mask = np.zeros(obj.shape, dtype=bool)
        mask[key] = True

        if is_list_like(val) and len(val) < len(obj):
            # see test_series_where
            return

        res = pd.Index(obj).where(~mask, val)
        ser_res = obj.where(~mask, val)
        tm.assert_index_equal(res, pd.Index(ser_res, dtype=ser_res.dtype))

    def test_index_putmask(self, obj, key, expected, raises, val):
        mask = np.zeros(obj.shape, dtype=bool)
        mask[key] = True

        if is_list_like(val) and len(val) < len(obj):
            # see test_series_where
            return

        res = pd.Index(obj).putmask(mask, val)
        ser_res = obj.where(~mask, val)
        tm.assert_index_equal(res, pd.Index(ser_res, dtype=ser_res.dtype))


@pytest.mark.parametrize(
    "obj,expected,key,raises",
    [
        pytest.param(
            # GH#45568 setting a valid NA value into IntervalDtype[int] should
            #  cast to IntervalDtype[float]
            pd.Series(pd.interval_range(1, 5)),
            pd.Series(
                [pd.Interval(1, 2), np.nan, pd.Interval(3, 4), pd.Interval(4, 5)],
                dtype="interval[float64]",
            ),
            1,
            True,
            id="interval_int_na_value",
        ),
        pytest.param(
            # these induce dtype changes
            pd.Series([2, 3, 4, 5, 6, 7, 8, 9, 10]),
            pd.Series([np.nan, 3, np.nan, 5, np.nan, 7, np.nan, 9, np.nan]),
            slice(None, None, 2),
            False,
            id="int_series_slice_key_step",
        ),
        pytest.param(
            pd.Series([True, True, False, False]),
            pd.Series([np.nan, True, np.nan, False], dtype=object),
            slice(None, None, 2),
            True,
            id="bool_series_slice_key_step",
        ),
        pytest.param(
            # these induce dtype changes
            pd.Series(np.arange(10)),
            pd.Series([np.nan, np.nan, np.nan, np.nan, np.nan, 5, 6, 7, 8, 9]),
            slice(None, 5),
            False,
            id="int_series_slice_key",
        ),
        pytest.param(
            # changes dtype GH#4463
            pd.Series([1, 2, 3]),
            pd.Series([np.nan, 2, 3]),
            0,
            False,
            id="int_series_int_key",
        ),
        pytest.param(
            # changes dtype GH#4463
            pd.Series([False]),
            pd.Series([np.nan], dtype=object),
            # TODO: maybe go to float64 since we are changing the _whole_ Series?
            0,
            True,
            id="bool_series_int_key_change_all",
        ),
        pytest.param(
            # changes dtype GH#4463
            pd.Series([False, True]),
            pd.Series([np.nan, True], dtype=object),
            0,
            True,
            id="bool_series_int_key",
        ),
    ],
)
class TestSetitemCastingEquivalents(SetitemCastingEquivalents):
    @pytest.fixture(params=[np.nan, np.float64("NaN"), None, pd.NA])
    def val(self, request):
        """
        NA values that should generally be valid_na for *all* dtypes.

        Include both python float NaN and np.float64; only np.float64 has a
        `dtype` attribute.
        """
        return request.param


class TestSetitemTimedelta64IntoNumeric(SetitemCastingEquivalents):
    # timedelta64 should not be treated as integers when setting into
    #  numeric Series

    @pytest.fixture
    def val(self):
        td = np.timedelta64(4, "ns")
        return td
        # TODO: could also try np.full((1,), td)

    @pytest.fixture(params=[complex, int, float])
    def dtype(self, request):
        return request.param

    @pytest.fixture
    def obj(self, dtype):
        arr = np.arange(5).astype(dtype)
        ser = pd.Series(arr)
        return ser

    @pytest.fixture
    def expected(self, dtype):
        arr = np.arange(5).astype(dtype)
        ser = pd.Series(arr)
        ser = ser.astype(object)
        ser.iloc[0] = np.timedelta64(4, "ns")
        return ser

    @pytest.fixture
    def key(self):
        return 0

    @pytest.fixture
    def raises(self):
        return True


class TestSetitemDT64IntoInt(SetitemCastingEquivalents):
    # GH#39619 dont cast dt64 to int when doing this setitem

    @pytest.fixture(params=["M8[ns]", "m8[ns]"])
    def dtype(self, request):
        return request.param

    @pytest.fixture
    def scalar(self, dtype):
        val = np.datetime64("2021-01-18 13:25:00", "ns")
        if dtype == "m8[ns]":
            val = val - val
        return val

    @pytest.fixture
    def expected(self, scalar):
        expected = pd.Series([scalar, scalar, 3], dtype=object)
        assert isinstance(expected[0], type(scalar))
        return expected

    @pytest.fixture
    def obj(self):
        return pd.Series([1, 2, 3])

    @pytest.fixture
    def key(self):
        return slice(None, -1)

    @pytest.fixture(params=[None, list, np.array])
    def val(self, scalar, request):
        box = request.param
        if box is None:
            return scalar
        return box([scalar, scalar])

    @pytest.fixture
    def raises(self):
        return True


class TestSetitemNAPeriodDtype(SetitemCastingEquivalents):
    # Setting compatible NA values into Series with PeriodDtype

    @pytest.fixture
    def expected(self, key):
        exp = pd.Series(pd.period_range("2000-01-01", periods=10, freq="D"))
        exp._values.view("i8")[key] = pd.NaT._value
        assert exp[key] is pd.NaT or all(x is pd.NaT for x in exp[key])
        return exp

    @pytest.fixture
    def obj(self):
        return pd.Series(pd.period_range("2000-01-01", periods=10, freq="D"))

    @pytest.fixture(params=[3, slice(3, 5)])
    def key(self, request):
        return request.param

    @pytest.fixture(params=[None, np.nan])
    def val(self, request):
        return request.param

    @pytest.fixture
    def raises(self):
        return False


class TestSetitemNADatetimeLikeDtype(SetitemCastingEquivalents):
    # some nat-like values should be cast to datetime64/timedelta64 when
    #  inserting into a datetime64/timedelta64 series.  Others should coerce
    #  to object and retain their dtypes.
    # GH#18586 for td64 and boolean mask case

    @pytest.fixture(
        params=["m8[ns]", "M8[ns]", "datetime64[ns, UTC]", "datetime64[ns, US/Central]"]
    )
    def dtype(self, request):
        return request.param

    @pytest.fixture
    def obj(self, dtype):
        i8vals = pd.date_range("2016-01-01", periods=3).asi8
        idx = pd.Index(i8vals, dtype=dtype)
        assert idx.dtype == dtype
        return pd.Series(idx)

    @pytest.fixture(
        params=[
            None,
            np.nan,
            pd.NaT,
            np.timedelta64("NaT", "ns"),
            np.datetime64("NaT", "ns"),
        ]
    )
    def val(self, request):
        return request.param

    @pytest.fixture
    def is_inplace(self, val, obj):
        # td64   -> cast to object iff val is datetime64("NaT")
        # dt64   -> cast to object iff val is timedelta64("NaT")
        # dt64tz -> cast to object with anything _but_ NaT
        return val is pd.NaT or val is None or val is np.nan or obj.dtype == val.dtype

    @pytest.fixture
    def expected(self, obj, val, is_inplace):
        dtype = obj.dtype if is_inplace else object
        expected = pd.Series([val, *list(obj[1:])], dtype=dtype)
        return expected

    @pytest.fixture
    def key(self):
        return 0

    @pytest.fixture
    def raises(self, is_inplace):
        return False if is_inplace else True


class TestSetitemMismatchedTZCastsToObject(SetitemCastingEquivalents):
    # GH#24024
    @pytest.fixture
    def obj(self):
        return pd.Series(pd.date_range("2000", periods=2, tz="US/Central"))

    @pytest.fixture
    def val(self):
        return pd.Timestamp("2000", tz="US/Eastern")

    @pytest.fixture
    def key(self):
        return 0

    @pytest.fixture
    def expected(self, obj, val):
        # pre-2.0 this would cast to object, in 2.0 we cast the val to
        #  the target tz
        expected = pd.Series(
            [
                val.tz_convert("US/Central"),
                pd.Timestamp("2000-01-02 00:00:00-06:00", tz="US/Central"),
            ],
            dtype=obj.dtype,
        )
        return expected

    @pytest.fixture
    def raises(self):
        return False


@pytest.mark.parametrize(
    "obj,expected",
    [
        # For numeric series, we should coerce to NaN.
        (pd.Series([1, 2, 3]), pd.Series([np.nan, 2, 3])),
        (pd.Series([1.0, 2.0, 3.0]), pd.Series([np.nan, 2.0, 3.0])),
        # For datetime series, we should coerce to NaT.
        (
            pd.Series(
                [datetime(2000, 1, 1), datetime(2000, 1, 2), datetime(2000, 1, 3)]
            ),
            pd.Series([pd.NaT, datetime(2000, 1, 2), datetime(2000, 1, 3)]),
        ),
        # For objects, we should preserve the None value.
        (pd.Series(["foo", "bar", "baz"]), pd.Series([None, "bar", "baz"])),
    ],
)
class TestSeriesNoneCoercion(SetitemCastingEquivalents):
    @pytest.fixture
    def key(self):
        return 0

    @pytest.fixture
    def val(self):
        return None

    @pytest.fixture
    def raises(self):
        return False


class TestSetitemFloatIntervalWithIntIntervalValues(SetitemCastingEquivalents):
    # GH#44201 Cast to shared IntervalDtype rather than object

    def test_setitem_example(self):
        # Just a case here to make obvious what this test class is aimed at
        idx = pd.IntervalIndex.from_breaks(range(4))
        obj = pd.Series(idx)
        val = pd.Interval(0.5, 1.5)

        with pytest.raises(TypeError, match="Invalid value"):
            obj[0] = val

    @pytest.fixture
    def obj(self):
        """
        Fixture to create a Series [(0, 1], (1, 2], (2, 3]]
        """
        idx = pd.IntervalIndex.from_breaks(range(4))
        return pd.Series(idx)

    @pytest.fixture
    def val(self):
        """
        Fixture to get an interval (0.5, 1.5]
        """
        return pd.Interval(0.5, 1.5)

    @pytest.fixture
    def key(self):
        """
        Fixture to get a key 0
        """
        return 0

    @pytest.fixture
    def expected(self, obj, val):
        """
        Fixture to get a Series [(0.5, 1.5], (1.0, 2.0], (2.0, 3.0]]
        """
        data = [val, *list(obj[1:])]
        idx = pd.IntervalIndex(data, dtype="Interval[float64]")
        return pd.Series(idx)

    @pytest.fixture
    def raises(self):
        """
        Fixture to enable raising pytest exceptions
        """
        return True


class TestSetitemRangeIntoIntegerSeries(SetitemCastingEquivalents):
    # GH#44261 Setting a range with sufficiently-small integers into
    #  small-itemsize integer dtypes should not need to upcast

    @pytest.fixture
    def obj(self, any_int_numpy_dtype):
        dtype = np.dtype(any_int_numpy_dtype)
        ser = pd.Series(range(5), dtype=dtype)
        return ser

    @pytest.fixture
    def val(self):
        return range(2, 4)

    @pytest.fixture
    def key(self):
        return slice(0, 2)

    @pytest.fixture
    def expected(self, any_int_numpy_dtype):
        dtype = np.dtype(any_int_numpy_dtype)
        exp = pd.Series([2, 3, 2, 3, 4], dtype=dtype)
        return exp

    @pytest.fixture
    def raises(self):
        return False


@pytest.mark.parametrize(
    "val, raises",
    [
        (np.array([2.0, 3.0]), False),
        (np.array([2.5, 3.5]), True),
        (
            np.array([2**65, 2**65 + 1], dtype=np.float64),
            True,
        ),  # all ints, but can't cast
    ],
)
class TestSetitemFloatNDarrayIntoIntegerSeries(SetitemCastingEquivalents):
    @pytest.fixture
    def obj(self):
        return pd.Series(range(5), dtype=np.int64)

    @pytest.fixture
    def key(self):
        return slice(0, 2)

    @pytest.fixture
    def expected(self, val):
        if val[0] == 2:
            # NB: this condition is based on currently-hardcoded "val" cases
            dtype = np.int64
        else:
            dtype = np.float64
        res_values = np.array(range(5), dtype=dtype)
        res_values[:2] = val
        return pd.Series(res_values)


@pytest.mark.parametrize("val", [512, np.int16(512)])
class TestSetitemIntoIntegerSeriesNeedsUpcast(SetitemCastingEquivalents):
    @pytest.fixture
    def obj(self):
        return pd.Series([1, 2, 3], dtype=np.int8)

    @pytest.fixture
    def key(self):
        return 1

    @pytest.fixture
    def expected(self):
        return pd.Series([1, 512, 3], dtype=np.int16)

    @pytest.fixture
    def raises(self):
        return True


@pytest.mark.parametrize("val", [2**33 + 1.0, 2**33 + 1.1, 2**62])
class TestSmallIntegerSetitemUpcast(SetitemCastingEquivalents):
    # https://github.com/pandas-dev/pandas/issues/39584#issuecomment-941212124
    @pytest.fixture
    def obj(self):
        return pd.Series([1, 2, 3], dtype="i4")

    @pytest.fixture
    def key(self):
        return 0

    @pytest.fixture
    def expected(self, val):
        if val % 1 != 0:
            dtype = "f8"
        else:
            dtype = "i8"
        return pd.Series([val, 2, 3], dtype=dtype)

    @pytest.fixture
    def raises(self):
        return True


class CoercionTest(SetitemCastingEquivalents):
    # Tests ported from tests.indexing.test_coercion

    @pytest.fixture
    def key(self):
        return 1

    @pytest.fixture
    def expected(self, obj, key, val, exp_dtype):
        vals = list(obj)
        vals[key] = val
        return pd.Series(vals, dtype=exp_dtype)


@pytest.mark.parametrize(
    "val,exp_dtype,raises",
    [(np.int32(1), np.int8, None), (np.int16(2**9), np.int16, True)],
)
class TestCoercionInt8(CoercionTest):
    # previously test_setitem_series_int8 in tests.indexing.test_coercion
    @pytest.fixture
    def obj(self):
        return pd.Series([1, 2, 3, 4], dtype=np.int8)


@pytest.mark.parametrize("val", [1, 1.1, 1 + 1j, True])
@pytest.mark.parametrize("exp_dtype", [object])
class TestCoercionObject(CoercionTest):
    # previously test_setitem_series_object in tests.indexing.test_coercion
    @pytest.fixture
    def obj(self):
        return pd.Series(["a", "b", "c", "d"], dtype=object)

    @pytest.fixture
    def raises(self):
        return False


@pytest.mark.parametrize(
    "val,exp_dtype,raises",
    [
        (1, object, True),
        ("e", pd.StringDtype(na_value=np.nan), False),
    ],
)
class TestCoercionString(CoercionTest):
    @pytest.fixture
    def obj(self):
        return pd.Series(["a", "b", "c", "d"], dtype=pd.StringDtype(na_value=np.nan))


@pytest.mark.parametrize(
    "val,exp_dtype,raises",
    [
        (1, np.complex128, False),
        (1.1, np.complex128, False),
        (1 + 1j, np.complex128, False),
        (True, object, True),
    ],
)
class TestCoercionComplex(CoercionTest):
    # previously test_setitem_series_complex128 in tests.indexing.test_coercion
    @pytest.fixture
    def obj(self):
        return pd.Series([1 + 1j, 2 + 2j, 3 + 3j, 4 + 4j])


@pytest.mark.parametrize(
    "val,exp_dtype,raises",
    [
        (1, object, True),
        ("3", object, True),
        (3, object, True),
        (1.1, object, True),
        (1 + 1j, object, True),
        (True, bool, False),
    ],
)
class TestCoercionBool(CoercionTest):
    # previously test_setitem_series_bool in tests.indexing.test_coercion
    @pytest.fixture
    def obj(self):
        return pd.Series([True, False, True, False], dtype=bool)


@pytest.mark.parametrize(
    "val,exp_dtype,raises",
    [
        (1, np.int64, False),
        (1.1, np.float64, True),
        (1 + 1j, np.complex128, True),
        (True, object, True),
    ],
)
class TestCoercionInt64(CoercionTest):
    # previously test_setitem_series_int64 in tests.indexing.test_coercion
    @pytest.fixture
    def obj(self):
        return pd.Series([1, 2, 3, 4])


@pytest.mark.parametrize(
    "val,exp_dtype,raises",
    [
        (1, np.float64, False),
        (1.1, np.float64, False),
        (1 + 1j, np.complex128, True),
        (True, object, True),
    ],
)
class TestCoercionFloat64(CoercionTest):
    # previously test_setitem_series_float64 in tests.indexing.test_coercion
    @pytest.fixture
    def obj(self):
        return pd.Series([1.1, 2.2, 3.3, 4.4])


@pytest.mark.parametrize(
    "val,exp_dtype,raises",
    [
        (1, np.float32, False),
        (1.1, np.float32, False),
        (1 + 1j, np.complex128, True),
        (True, object, True),
        (np.uint8(2), np.float32, False),
        (np.uint32(2), np.float32, False),
        # float32 cannot hold np.iinfo(np.uint32).max exactly
        # (closest it can hold is 4294967300.0 which off by 5.0), so
        # we cast to float64
        (np.uint32(np.iinfo(np.uint32).max), np.float64, True),
        (np.uint64(2), np.float32, False),
        (np.int64(2), np.float32, False),
    ],
)
class TestCoercionFloat32(CoercionTest):
    @pytest.fixture
    def obj(self):
        return pd.Series([1.1, 2.2, 3.3, 4.4], dtype=np.float32)


@pytest.mark.parametrize(
    "exp_dtype",
    [
        "M8[ms]",
        "M8[ms, UTC]",
        "m8[ms]",
    ],
)
class TestCoercionDatetime64HigherReso(CoercionTest):
    @pytest.fixture
    def obj(self, exp_dtype):
        idx = pd.date_range("2011-01-01", freq="D", periods=4, unit="s")
        if exp_dtype == "m8[ms]":
            idx = idx - pd.Timestamp("1970-01-01").as_unit("s")
            assert idx.dtype == "m8[s]"
        elif exp_dtype == "M8[ms, UTC]":
            idx = idx.tz_localize("UTC")
        return pd.Series(idx)

    @pytest.fixture
    def val(self, exp_dtype):
        ts = pd.Timestamp("2011-01-02 03:04:05.678").as_unit("ms")
        if exp_dtype == "m8[ms]":
            return ts - pd.Timestamp("1970-01-01").as_unit("s")
        elif exp_dtype == "M8[ms, UTC]":
            return ts.tz_localize("UTC")
        return ts

    @pytest.fixture
    def raises(self):
        return True


@pytest.mark.parametrize(
    "val,exp_dtype,raises",
    [
        (pd.Timestamp("2012-01-01"), "datetime64[ns]", False),
        (1, object, True),
        ("x", object, True),
    ],
)
class TestCoercionDatetime64(CoercionTest):
    # previously test_setitem_series_datetime64 in tests.indexing.test_coercion

    @pytest.fixture
    def obj(self):
        return pd.Series(pd.date_range("2011-01-01", freq="D", periods=4, unit="ns"))

    @pytest.fixture
    def raises(self):
        return False


@pytest.mark.parametrize(
    "val,exp_dtype,raises",
    [
        (
            pd.Timestamp("2012-01-01", tz="US/Eastern"),
            "datetime64[ns, US/Eastern]",
            False,
        ),
        # pre-2.0, a mis-matched tz would end up casting to object
        (
            pd.Timestamp("2012-01-01", tz="US/Pacific"),
            "datetime64[ns, US/Eastern]",
            False,
        ),
        (pd.Timestamp("2012-01-01"), object, True),
        (1, object, True),
    ],
)
class TestCoercionDatetime64TZ(CoercionTest):
    # previously test_setitem_series_datetime64tz in tests.indexing.test_coercion
    @pytest.fixture
    def obj(self):
        tz = "US/Eastern"
        return pd.Series(
            pd.date_range("2011-01-01", freq="D", periods=4, tz=tz, unit="ns")
        )

    @pytest.fixture
    def raises(self):
        return False


@pytest.mark.parametrize(
    "val,exp_dtype,raises",
    [
        (pd.Timedelta("12 day"), "timedelta64[us]", False),
        (1, object, True),
        ("x", object, True),
    ],
)
class TestCoercionTimedelta64(CoercionTest):
    # previously test_setitem_series_timedelta64 in tests.indexing.test_coercion
    @pytest.fixture
    def obj(self):
        return pd.Series(pd.timedelta_range("1 day", periods=4))

    @pytest.fixture
    def raises(self):
        return False


@pytest.mark.parametrize(
    "val", ["foo", pd.Period("2016", freq="Y"), pd.Interval(1, 2, closed="both")]
)
@pytest.mark.parametrize("exp_dtype", [object])
class TestPeriodIntervalCoercion(CoercionTest):
    # GH#45768
    @pytest.fixture(
        params=[
            pd.period_range("2016-01-01", periods=3, freq="D"),
            pd.interval_range(1, 5),
        ]
    )
    def obj(self, request):
        return pd.Series(request.param)

    @pytest.fixture
    def raises(self):
        return True


def test_20643():
    # closed by GH#45121
    orig = pd.Series([0, 1, 2], index=["a", "b", "c"])

    ser = orig.copy()
    with pytest.raises(TypeError, match="Invalid value"):
        ser.at["b"] = 2.7

    with pytest.raises(TypeError, match="Invalid value"):
        ser.loc["b"] = 2.7

    with pytest.raises(TypeError, match="Invalid value"):
        ser["b"] = 2.7

    ser = orig.copy()
    with pytest.raises(TypeError, match="Invalid value"):
        ser.iat[1] = 2.7

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

    orig_df = orig.to_frame("A")

    df = orig_df.copy()
    with pytest.raises(TypeError, match="Invalid value"):
        df.at["b", "A"] = 2.7

    with pytest.raises(TypeError, match="Invalid value"):
        df.loc["b", "A"] = 2.7

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

    with pytest.raises(TypeError, match="Invalid value"):
        df.iat[1, 0] = 2.7


def test_20643_comment():
    # https://github.com/pandas-dev/pandas/issues/20643#issuecomment-431244590
    # fixed sometime prior to GH#45121
    orig = pd.Series([0, 1, 2], index=["a", "b", "c"])
    expected = pd.Series([np.nan, 1, 2], index=["a", "b", "c"])

    ser = orig.copy()
    ser.iat[0] = None
    tm.assert_series_equal(ser, expected)

    ser = orig.copy()
    ser.iloc[0] = None
    tm.assert_series_equal(ser, expected)


def test_15413():
    # fixed by GH#45121
    ser = pd.Series([1, 2, 3])

    with pytest.raises(TypeError, match="Invalid value"):
        ser[ser == 2] += 0.5

    with pytest.raises(TypeError, match="Invalid value"):
        ser[1] += 0.5

    with pytest.raises(TypeError, match="Invalid value"):
        ser.loc[1] += 0.5

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

    with pytest.raises(TypeError, match="Invalid value"):
        ser.iat[1] += 0.5

    with pytest.raises(TypeError, match="Invalid value"):
        ser.at[1] += 0.5


def test_32878_int_itemsize():
    # Fixed by GH#45121
    arr = np.arange(5).astype("i4")
    ser = pd.Series(arr)
    val = np.int64(np.iinfo(np.int64).max)
    with pytest.raises(TypeError, match="Invalid value"):
        ser[0] = val


def test_32878_complex_itemsize():
    arr = np.arange(5).astype("c8")
    ser = pd.Series(arr)
    val = np.finfo(np.float64).max
    val = val.astype("c16")

    # GH#32878 used to coerce val to inf+0.000000e+00j
    with pytest.raises(TypeError, match="Invalid value"):
        ser[0] = val


def test_37692(indexer_al):
    # GH#37692
    ser = pd.Series([1, 2, 3], index=["a", "b", "c"])
    with pytest.raises(TypeError, match="Invalid value"):
        indexer_al(ser)["b"] = "test"


def test_setitem_bool_int_float_consistency(indexer_sli):
    # GH#21513
    # bool-with-int and bool-with-float both upcast to object
    #  int-with-float and float-with-int are both non-casting so long
    #  as the setitem can be done losslessly
    for dtype in [np.float64, np.int64]:
        ser = pd.Series(0, index=range(3), dtype=dtype)
        with pytest.raises(TypeError, match="Invalid value"):
            indexer_sli(ser)[0] = True

        ser = pd.Series(0, index=range(3), dtype=bool)
        with pytest.raises(TypeError, match="Invalid value"):
            ser[0] = dtype(1)

    # 1.0 can be held losslessly, so no casting
    ser = pd.Series(0, index=range(3), dtype=np.int64)
    indexer_sli(ser)[0] = np.float64(1.0)
    assert ser.dtype == np.int64

    # 1 can be held losslessly, so no casting
    ser = pd.Series(0, index=range(3), dtype=np.float64)
    indexer_sli(ser)[0] = np.int64(1)


def test_setitem_positional_with_casting():
    # GH#45070 case where in __setitem__ we get a KeyError, then when
    #  we fallback we *also* get a ValueError if we try to set inplace.
    # As of 3.0 we always treat int keys as labels, so this becomes
    #  setitem-with-expansion
    ser = pd.Series([1, 2, 3], index=["a", "b", "c"])

    with tm.assert_produces_warning(Pandas4Warning, match="incompatible dtype"):
        ser[0] = "X"
    expected = pd.Series([1, 2, 3, "X"], index=["a", "b", "c", 0], dtype=object)
    tm.assert_series_equal(ser, expected)


def test_setitem_positional_float_into_int_coerces():
    # Case where we hit a KeyError and then trying to set in-place incorrectly
    #  casts a float to an int;
    # As of 3.0 we always treat int keys as labels, so this becomes
    #  setitem-with-expansion
    ser = pd.Series([1, 2, 3], index=["a", "b", "c"])

    with tm.assert_produces_warning(Pandas4Warning, match="incompatible dtype"):
        ser[0] = 1.5
    expected = pd.Series([1, 2, 3, 1.5], index=["a", "b", "c", 0])
    tm.assert_series_equal(ser, expected)


def test_setitem_int_not_positional():
    # GH#42215 deprecated falling back to positional on __setitem__ with an
    #  int not contained in the index; enforced in 2.0
    ser = pd.Series([1, 2, 3, 4], index=[1.1, 2.1, 3.0, 4.1])
    assert not ser.index._should_fallback_to_positional
    # assert not ser.index.astype(object)._should_fallback_to_positional

    # 3.0 is in our index, so post-enforcement behavior is unchanged
    ser[3] = 10
    expected = pd.Series([1, 2, 10, 4], index=ser.index)
    tm.assert_series_equal(ser, expected)

    # pre-enforcement `ser[5] = 5` raised IndexError
    ser[5] = 5
    expected = pd.Series([1, 2, 10, 4, 5], index=[1.1, 2.1, 3.0, 4.1, 5.0])
    tm.assert_series_equal(ser, expected)

    ii = pd.IntervalIndex.from_breaks(range(10))[::2]
    ser2 = pd.Series(range(len(ii)), index=ii)
    exp_index = ii.astype(object).append(pd.Index([4]))
    expected2 = pd.Series([0, 1, 2, 3, 4, 9], index=exp_index)
    # pre-enforcement `ser2[4] = 9` interpreted 4 as positional
    ser2[4] = 9
    tm.assert_series_equal(ser2, expected2)

    mi = pd.MultiIndex.from_product([ser.index, ["A", "B"]])
    ser3 = pd.Series(range(len(mi)), index=mi)
    expected3 = ser3.copy()
    msg = "Setting a new value on a Series with a MultiIndex"
    with tm.assert_produces_warning(Pandas4Warning, match=msg):
        expected3.loc[4] = 99
    # pre-enforcement `ser3[4] = 99` interpreted 4 as positional
    with tm.assert_produces_warning(Pandas4Warning, match=msg):
        ser3[4] = 99
    tm.assert_series_equal(ser3, expected3)


def test_setitem_with_bool_indexer():
    # GH#42530

    df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
    result = df.pop("b").copy()
    result[[True, False, False]] = 9
    expected = pd.Series(data=[9, 5, 6], name="b")
    tm.assert_series_equal(result, expected)

    df.loc[[True, False, False], "a"] = 10
    expected = pd.DataFrame({"a": [10, 2, 3]})
    tm.assert_frame_equal(df, expected)


@pytest.mark.parametrize("size", range(2, 6))
@pytest.mark.parametrize(
    "mask", [[True, False, False, False, False], [True, False], [False]]
)
@pytest.mark.parametrize(
    "item", [2.0, np.nan, np.finfo(float).max, np.finfo(float).min]
)
@pytest.mark.parametrize("box", [np.array, list, tuple])
def test_setitem_bool_indexer_dont_broadcast_length1_values(size, mask, item, box):
    # GH#44265
    # see also tests.series.indexing.test_where.test_broadcast

    selection = np.resize(mask, size)

    data = np.arange(size, dtype=float)

    ser = pd.Series(data)

    if selection.sum() != 1:
        msg = (
            "cannot set using a list-like indexer with a different "
            "length than the value"
        )
        with pytest.raises(ValueError, match=msg):
            # GH#44265
            ser[selection] = box([item])
    else:
        # In this corner case setting is equivalent to setting with the unboxed
        #  item
        ser[selection] = box([item])

        expected = pd.Series(np.arange(size, dtype=float))
        expected[selection] = item
        tm.assert_series_equal(ser, expected)


def test_setitem_empty_mask_dont_upcast_dt64():
    dti = pd.date_range("2016-01-01", periods=3)
    ser = pd.Series(dti)
    orig = ser.copy()
    mask = np.zeros(3, dtype=bool)

    ser[mask] = "foo"
    assert ser.dtype == dti.dtype  # no-op -> dont upcast
    tm.assert_series_equal(ser, orig)

    ser.mask(mask, "foo", inplace=True)
    assert ser.dtype == dti.dtype  # no-op -> dont upcast
    tm.assert_series_equal(ser, orig)


def test_setitem_bool_dtype_with_boolean_indexer():
    # GH 57338
    s1 = pd.Series([True, True, True], dtype=bool)
    s2 = pd.Series([False, False, False], dtype=bool)
    condition = [False, True, False]
    s1[condition] = s2[condition]
    expected = pd.Series([True, False, True], dtype=bool)
    tm.assert_series_equal(s1, expected)


@pytest.mark.parametrize(
    "value, expected_dtype",
    [
        ("foo", object),
        (3.5, np.float64),
        (pd.Timestamp("2025-01-01"), object),
    ],
)
def test_setitem_expansion_incompatible_dtype_warns(value, expected_dtype):
    # GH#62369 silent dtype change during setitem-with-expansion
    ser = pd.Series([1, 2], dtype=np.int64)
    with tm.assert_produces_warning(Pandas4Warning, match="incompatible dtype"):
        ser.loc[2] = value
    assert ser.dtype == expected_dtype


@pytest.mark.parametrize(
    "value, expected_dtype",
    [
        # compatible value, dtype unchanged
        (3, np.int64),
        # float losslessly downcast back to int64
        (3.0, np.int64),
        # PDEP-6 exception: int -> float when the value introduces NaN
        (np.nan, np.float64),
    ],
)
def test_setitem_expansion_compatible_dtype_no_warning(value, expected_dtype):
    # GH#62369
    ser = pd.Series([1, 2], dtype=np.int64)
    with tm.assert_produces_warning(None):
        ser.loc[2] = value
    assert ser.dtype == expected_dtype


def test_setitem_expansion_float64_holds_int_no_warning():
    # GH#62369 float64 can hold an int value losslessly, dtype is retained
    ser = pd.Series([1.5, 2.5])
    with tm.assert_produces_warning(None):
        ser.loc[2] = 3
    assert ser.dtype == np.float64


def test_setitem_expansion_complex_into_float_warns():
    # GH#62369 complex is numeric but float64 cannot hold it losslessly,
    #  so the dtype changes (with a warning) and the value is preserved
    ser = pd.Series([1.0, 2.0])
    with tm.assert_produces_warning(Pandas4Warning, match="incompatible dtype"):
        ser.loc[2] = 1 + 2j
    expected = pd.Series([1.0, 2.0, 1 + 2j], dtype=np.complex128)
    tm.assert_series_equal(ser, expected)


def test_setitem_expansion_float64_value_into_float32_warns():
    # GH#62369 value does not fit in float32, so the dtype changes
    #  (with a warning) and the value is preserved
    ser = pd.Series([1.0, 2.0], dtype=np.float32)
    with tm.assert_produces_warning(Pandas4Warning, match="incompatible dtype"):
        ser.loc[2] = 1e300
    expected = pd.Series([1.0, 2.0, 1e300], dtype=np.float64)
    tm.assert_series_equal(ser, expected)


@pytest.mark.parametrize("dtype", ["Int64", "Float64", "boolean"])
def test_setitem_expansion_incompatible_masked_dtype_warns(dtype):
    # GH#62369 extension dtypes participate in the expansion deprecation
    ser = pd.Series([1, 0], dtype=dtype)
    with tm.assert_produces_warning(Pandas4Warning, match="incompatible dtype"):
        ser.loc[2] = "foo"
    assert ser.dtype == object


def test_setitem_expansion_masked_lossy_float_warns():
    # GH#62369 2.5 cannot be held losslessly by Int64
    ser = pd.Series([1, 2], dtype="Int64")
    with tm.assert_produces_warning(Pandas4Warning, match="incompatible dtype"):
        ser.loc[2] = 2.5
    assert ser.dtype == "Float64"


def test_setitem_expansion_masked_compatible_no_warning():
    # GH#62369 NA and lossless floats can be held by Int64; dtype is retained
    ser = pd.Series([1, 2], dtype="Int64")
    with tm.assert_produces_warning(None):
        ser.loc[2] = pd.NA
        ser.loc[3] = 4.0
    assert ser.dtype == "Int64"


def test_setitem_expansion_categorical_novel_value_warns():
    # GH#62369 a value outside the categories changes the dtype
    ser = pd.Series(pd.Categorical(["a", "b"]))
    with tm.assert_produces_warning(Pandas4Warning, match="incompatible dtype"):
        ser.loc[2] = "zzz"
    assert ser.dtype != "category"


def test_setitem_expansion_categorical_known_value_no_warning():
    # GH#62369 a value among the categories is compatible
    ser = pd.Series(pd.Categorical(["a", "b"]))
    with tm.assert_produces_warning(None):
        ser.loc[2] = "a"
    assert ser.dtype == "category"


def test_setitem_expansion_arrow_incompatible_warns():
    # GH#62369
    pytest.importorskip("pyarrow")
    ser = pd.Series([1, 2], dtype="int64[pyarrow]")
    with tm.assert_produces_warning(Pandas4Warning, match="incompatible dtype"):
        ser.loc[2] = "foo"
    assert ser.dtype == object


@pytest.mark.parametrize("value", [2**63, np.inf, -np.inf])
def test_setitem_enlarge_out_of_int64_range(value):
    # GH#66394 the result must not be downcast back to int64; on aarch64 the
    #  out-of-range cast saturated and 2**63 was silently stored as 2**63 - 1
    ser = pd.Series([1, 2], dtype="int64")

    # GH#62369 int64 cannot hold the value, so the widening is deprecated
    with tm.assert_produces_warning(
        Pandas4Warning, match="incompatible dtype"
    ) as record:
        ser.loc[2] = value
    assert not any(issubclass(warning.category, RuntimeWarning) for warning in record)

    expected = pd.Series([1.0, 2.0, value], dtype="float64")
    tm.assert_series_equal(ser, expected)


def test_setitem_enlarge_within_int64_range():
    # GH#66394 in-range values still downcast back to int64
    ser = pd.Series([1, 2], dtype="int64")
    ser.loc[2] = 2**62

    expected = pd.Series([1, 2, 2**62], dtype="int64")
    tm.assert_series_equal(ser, expected)


def test_setitem_enlarge_float16_no_overflow_warning():
    # GH#68315 floats_fit_integer_dtype's float16 overflow warning (see
    #  test_downcast.py) fires here too, on setitem-with-expansion
    ser = pd.Series([1, 2], dtype="int64")

    with tm.assert_produces_warning(None):
        ser.loc[2] = np.float16(3.0)

    expected = pd.Series([1, 2, 3], dtype="int64")
    tm.assert_series_equal(ser, expected)


@pytest.mark.parametrize(
    "dtype, expected_dtype",
    [
        ("Int64", "Float64"),
        ("UInt64", "Float64"),
        ("int64[pyarrow]", "double[pyarrow]"),
        ("uint64[pyarrow]", "double[pyarrow]"),
    ],
)
def test_setitem_enlarge_out_of_int_ea_range(dtype, expected_dtype):
    # GH#65094 the out-of-range float->int cast saturates instead of raising,
    #  and the roundtrip check could not see it, since e.g. 2**63 and 2**63 - 1
    #  are the same float64, so the value was silently stored as the dtype max
    if "pyarrow" in dtype:
        pytest.importorskip("pyarrow")
    value = 2.0**64 if dtype.lower().startswith("uint") else 2.0**63
    ser = pd.Series([1, 2], dtype=dtype)

    with tm.assert_produces_warning(Pandas4Warning, match="incompatible dtype"):
        ser.loc[2] = value

    expected = pd.Series([1.0, 2.0, value], dtype=expected_dtype)
    tm.assert_series_equal(ser, expected)


@pytest.mark.parametrize(
    "dtype, value", [("Sparse[int64]", 2.0**63), ("Sparse[uint64]", 2.0**64)]
)
def test_setitem_enlarge_out_of_sparse_int_range(dtype, value):
    # GH#65094 SparseDtype reaches the same cast, but spells its backing numpy
    #  dtype `subtype`, so it was skipping the range check and storing the
    #  saturated value
    ser = pd.Series([1, 2], dtype=dtype)

    with tm.assert_produces_warning(
        Pandas4Warning, match="incompatible dtype", raise_on_extra_warnings=False
    ):
        ser.loc[2] = value

    expected = pd.Series([1.0, 2.0, value], dtype="Sparse[float64]")
    tm.assert_series_equal(ser, expected)


@pytest.mark.parametrize("dtype", ["Int64", "int64[pyarrow]", "Sparse[int64]"])
def test_setitem_enlarge_int_ea_above_2_53(dtype):
    # GH#65094 an in-range integral float above 2**53 is exact, so the dtype is
    #  retained.  For ArrowDtype the check used to roundtrip through pyarrow,
    #  whose int64->double cast rejects anything above 2**53, raising
    #  ArrowInvalid out of .loc
    if "pyarrow" in dtype:
        pytest.importorskip("pyarrow")
    ser = pd.Series([1, 2], dtype=dtype)

    with tm.assert_produces_warning(None):
        ser.loc[2] = 1.7e18

    expected = pd.Series([1, 2, 1700000000000000000], dtype=dtype)
    tm.assert_series_equal(ser, expected)
