from __future__ import annotations

from datetime import datetime
import gc

import numpy as np
import pytest

from pandas._libs.tslibs import Timestamp

from pandas.core.dtypes.common import (
    is_datetime64tz_dtype,
    is_integer_dtype,
)
from pandas.core.dtypes.dtypes import CategoricalDtype

import pandas as pd
from pandas import (
    CategoricalIndex,
    DatetimeIndex,
    Index,
    IntervalIndex,
    MultiIndex,
    PeriodIndex,
    RangeIndex,
    Series,
    TimedeltaIndex,
    isna,
)
import pandas._testing as tm
from pandas.core.api import (  # noqa:F401
    Float64Index,
    Int64Index,
    NumericIndex,
    UInt64Index,
)
from pandas.core.arrays import BaseMaskedArray


class Base:
    """
    Base class for index sub-class tests.
    """

    _index_cls: type[Index]

    @pytest.fixture
    def simple_index(self):
        raise NotImplementedError("Method not implemented")

    def create_index(self) -> Index:
        raise NotImplementedError("Method not implemented")

    def test_pickle_compat_construction(self):
        # need an object to create with
        msg = "|".join(
            [
                r"Index\(\.\.\.\) must be called with a collection of some "
                r"kind, None was passed",
                r"DatetimeIndex\(\) must be called with a collection of some "
                r"kind, None was passed",
                r"TimedeltaIndex\(\) must be called with a collection of some "
                r"kind, None was passed",
                r"__new__\(\) missing 1 required positional argument: 'data'",
                r"__new__\(\) takes at least 2 arguments \(1 given\)",
            ]
        )
        with pytest.raises(TypeError, match=msg):
            self._index_cls()

    def test_shift(self, simple_index):

        # GH8083 test the base class for shift
        idx = simple_index
        msg = (
            f"This method is only implemented for DatetimeIndex, PeriodIndex and "
            f"TimedeltaIndex; Got type {type(idx).__name__}"
        )
        with pytest.raises(NotImplementedError, match=msg):
            idx.shift(1)
        with pytest.raises(NotImplementedError, match=msg):
            idx.shift(1, 2)

    def test_constructor_name_unhashable(self, simple_index):
        # GH#29069 check that name is hashable
        # See also same-named test in tests.series.test_constructors
        idx = simple_index
        with pytest.raises(TypeError, match="Index.name must be a hashable type"):
            type(idx)(idx, name=[])

    def test_create_index_existing_name(self, simple_index):

        # GH11193, when an existing index is passed, and a new name is not
        # specified, the new index should inherit the previous object name
        expected = simple_index
        if not isinstance(expected, MultiIndex):
            expected.name = "foo"
            result = Index(expected)
            tm.assert_index_equal(result, expected)

            result = Index(expected, name="bar")
            expected.name = "bar"
            tm.assert_index_equal(result, expected)
        else:
            expected.names = ["foo", "bar"]
            result = Index(expected)
            tm.assert_index_equal(
                result,
                Index(
                    Index(
                        [
                            ("foo", "one"),
                            ("foo", "two"),
                            ("bar", "one"),
                            ("baz", "two"),
                            ("qux", "one"),
                            ("qux", "two"),
                        ],
                        dtype="object",
                    ),
                    names=["foo", "bar"],
                ),
            )

            result = Index(expected, names=["A", "B"])
            tm.assert_index_equal(
                result,
                Index(
                    Index(
                        [
                            ("foo", "one"),
                            ("foo", "two"),
                            ("bar", "one"),
                            ("baz", "two"),
                            ("qux", "one"),
                            ("qux", "two"),
                        ],
                        dtype="object",
                    ),
                    names=["A", "B"],
                ),
            )

    def test_numeric_compat(self, simple_index):

        idx = simple_index
        # Check that this doesn't cover MultiIndex case, if/when it does,
        #  we can remove multi.test_compat.test_numeric_compat
        assert not isinstance(idx, MultiIndex)
        if type(idx) is Index:
            return

        typ = type(idx._data).__name__
        cls = type(idx).__name__
        lmsg = "|".join(
            [
                rf"unsupported operand type\(s\) for \*: '{typ}' and 'int'",
                "cannot perform (__mul__|__truediv__|__floordiv__) with "
                f"this index type: ({cls}|{typ})",
            ]
        )
        with pytest.raises(TypeError, match=lmsg):
            idx * 1
        rmsg = "|".join(
            [
                rf"unsupported operand type\(s\) for \*: 'int' and '{typ}'",
                "cannot perform (__rmul__|__rtruediv__|__rfloordiv__) with "
                f"this index type: ({cls}|{typ})",
            ]
        )
        with pytest.raises(TypeError, match=rmsg):
            1 * idx

        div_err = lmsg.replace("*", "/")
        with pytest.raises(TypeError, match=div_err):
            idx / 1
        div_err = rmsg.replace("*", "/")
        with pytest.raises(TypeError, match=div_err):
            1 / idx

        floordiv_err = lmsg.replace("*", "//")
        with pytest.raises(TypeError, match=floordiv_err):
            idx // 1
        floordiv_err = rmsg.replace("*", "//")
        with pytest.raises(TypeError, match=floordiv_err):
            1 // idx

    def test_logical_compat(self, simple_index):
        idx = simple_index
        with pytest.raises(TypeError, match="cannot perform all"):
            idx.all()
        with pytest.raises(TypeError, match="cannot perform any"):
            idx.any()

    def test_repr_roundtrip(self, simple_index):

        idx = simple_index
        tm.assert_index_equal(eval(repr(idx)), idx)

    def test_repr_max_seq_item_setting(self, simple_index):
        # GH10182
        idx = simple_index
        idx = idx.repeat(50)
        with pd.option_context("display.max_seq_items", None):
            repr(idx)
            assert "..." not in str(idx)

    def test_ensure_copied_data(self, index):
        # Check the "copy" argument of each Index.__new__ is honoured
        # GH12309
        init_kwargs = {}
        if isinstance(index, PeriodIndex):
            # Needs "freq" specification:
            init_kwargs["freq"] = index.freq
        elif isinstance(index, (RangeIndex, MultiIndex, CategoricalIndex)):
            # RangeIndex cannot be initialized from data
            # MultiIndex and CategoricalIndex are tested separately
            return

        index_type = type(index)
        result = index_type(index.values, copy=True, **init_kwargs)
        if is_datetime64tz_dtype(index.dtype):
            result = result.tz_localize("UTC").tz_convert(index.tz)
        if isinstance(index, (DatetimeIndex, TimedeltaIndex)):
            index = index._with_freq(None)

        tm.assert_index_equal(index, result)

        if isinstance(index, PeriodIndex):
            # .values an object array of Period, thus copied
            result = index_type(ordinal=index.asi8, copy=False, **init_kwargs)
            tm.assert_numpy_array_equal(index.asi8, result.asi8, check_same="same")
        elif isinstance(index, IntervalIndex):
            # checked in test_interval.py
            pass
        elif type(index) is Index and not isinstance(index.dtype, np.dtype):
            result = index_type(index.values, copy=False, **init_kwargs)
            tm.assert_index_equal(result, index)

            if isinstance(index._values, BaseMaskedArray):
                assert np.shares_memory(index._values._data, result._values._data)
                tm.assert_numpy_array_equal(
                    index._values._data, result._values._data, check_same="same"
                )
                assert np.shares_memory(index._values._mask, result._values._mask)
                tm.assert_numpy_array_equal(
                    index._values._mask, result._values._mask, check_same="same"
                )
            elif index.dtype == "string[python]":
                assert np.shares_memory(index._values._ndarray, result._values._ndarray)
                tm.assert_numpy_array_equal(
                    index._values._ndarray, result._values._ndarray, check_same="same"
                )
            elif index.dtype == "string[pyarrow]":
                assert tm.shares_memory(result._values, index._values)
            else:
                raise NotImplementedError(index.dtype)
        else:
            result = index_type(index.values, copy=False, **init_kwargs)
            tm.assert_numpy_array_equal(index.values, result.values, check_same="same")

    def test_memory_usage(self, index):
        index._engine.clear_mapping()
        result = index.memory_usage()
        if index.empty:
            # we report 0 for no-length
            assert result == 0
            return

        # non-zero length
        index.get_loc(index[0])
        result2 = index.memory_usage()
        result3 = index.memory_usage(deep=True)

        # RangeIndex, IntervalIndex
        # don't have engines
        # Index[EA] has engine but it does not have a Hashtable .mapping
        if not isinstance(index, (RangeIndex, IntervalIndex)) and not (
            type(index) is Index and not isinstance(index.dtype, np.dtype)
        ):
            assert result2 > result

        if index.inferred_type == "object":
            assert result3 > result2

    def test_argsort(self, request, index):
        # separately tested
        if isinstance(index, CategoricalIndex):
            return

        result = index.argsort()
        expected = np.array(index).argsort()
        tm.assert_numpy_array_equal(result, expected, check_dtype=False)

    def test_numpy_argsort(self, index):
        result = np.argsort(index)
        expected = index.argsort()
        tm.assert_numpy_array_equal(result, expected)

        result = np.argsort(index, kind="mergesort")
        expected = index.argsort(kind="mergesort")
        tm.assert_numpy_array_equal(result, expected)

        # these are the only two types that perform
        # pandas compatibility input validation - the
        # rest already perform separate (or no) such
        # validation via their 'values' attribute as
        # defined in pandas.core.indexes/base.py - they
        # cannot be changed at the moment due to
        # backwards compatibility concerns
        if isinstance(index, (CategoricalIndex, RangeIndex)):
            msg = "the 'axis' parameter is not supported"
            with pytest.raises(ValueError, match=msg):
                np.argsort(index, axis=1)

            msg = "the 'order' parameter is not supported"
            with pytest.raises(ValueError, match=msg):
                np.argsort(index, order=("a", "b"))

    def test_repeat(self, simple_index):
        rep = 2
        idx = simple_index.copy()
        new_index_cls = Int64Index if isinstance(idx, RangeIndex) else idx._constructor
        expected = new_index_cls(idx.values.repeat(rep), name=idx.name)
        tm.assert_index_equal(idx.repeat(rep), expected)

        idx = simple_index
        rep = np.arange(len(idx))
        expected = new_index_cls(idx.values.repeat(rep), name=idx.name)
        tm.assert_index_equal(idx.repeat(rep), expected)

    def test_numpy_repeat(self, simple_index):
        rep = 2
        idx = simple_index
        expected = idx.repeat(rep)
        tm.assert_index_equal(np.repeat(idx, rep), expected)

        msg = "the 'axis' parameter is not supported"
        with pytest.raises(ValueError, match=msg):
            np.repeat(idx, rep, axis=0)

    def test_where(self, listlike_box, simple_index):
        klass = listlike_box

        idx = simple_index
        if isinstance(idx, (DatetimeIndex, TimedeltaIndex)):
            # where does not preserve freq
            idx = idx._with_freq(None)

        cond = [True] * len(idx)
        result = idx.where(klass(cond))
        expected = idx
        tm.assert_index_equal(result, expected)

        cond = [False] + [True] * len(idx[1:])
        expected = Index([idx._na_value] + idx[1:].tolist(), dtype=idx.dtype)
        result = idx.where(klass(cond))
        tm.assert_index_equal(result, expected)

    def test_insert_base(self, index):
        result = index[1:4]

        if not len(index):
            return

        # test 0th element
        assert index[0:4].equals(result.insert(0, index[0]))

    def test_insert_out_of_bounds(self, index):
        # TypeError/IndexError matches what np.insert raises in these cases

        if len(index) > 0:
            err = TypeError
        else:
            err = IndexError
        if len(index) == 0:
            # 0 vs 0.5 in error message varies with numpy version
            msg = "index (0|0.5) is out of bounds for axis 0 with size 0"
        else:
            msg = "slice indices must be integers or None or have an __index__ method"
        with pytest.raises(err, match=msg):
            index.insert(0.5, "foo")

        msg = "|".join(
            [
                r"index -?\d+ is out of bounds for axis 0 with size \d+",
                "loc must be an integer between",
            ]
        )
        with pytest.raises(IndexError, match=msg):
            index.insert(len(index) + 1, 1)

        with pytest.raises(IndexError, match=msg):
            index.insert(-len(index) - 1, 1)

    def test_delete_base(self, index):
        if not len(index):
            return

        if isinstance(index, RangeIndex):
            # tested in class
            return

        expected = index[1:]
        result = index.delete(0)
        assert result.equals(expected)
        assert result.name == expected.name

        expected = index[:-1]
        result = index.delete(-1)
        assert result.equals(expected)
        assert result.name == expected.name

        length = len(index)
        msg = f"index {length} is out of bounds for axis 0 with size {length}"
        with pytest.raises(IndexError, match=msg):
            index.delete(length)

    def test_equals(self, index):
        if isinstance(index, IntervalIndex):
            # IntervalIndex tested separately, the index.equals(index.astype(object))
            #  fails for IntervalIndex
            return

        is_ea_idx = type(index) is Index and not isinstance(index.dtype, np.dtype)

        assert index.equals(index)
        assert index.equals(index.copy())
        if not is_ea_idx:
            # doesn't hold for e.g. IntegerDtype
            assert index.equals(index.astype(object))

        assert not index.equals(list(index))
        assert not index.equals(np.array(index))

        # Cannot pass in non-int64 dtype to RangeIndex
        if not isinstance(index, RangeIndex) and not is_ea_idx:
            same_values = Index(index, dtype=object)
            assert index.equals(same_values)
            assert same_values.equals(index)

        if index.nlevels == 1:
            # do not test MultiIndex
            assert not index.equals(Series(index))

    def test_equals_op(self, simple_index):
        # GH9947, GH10637
        index_a = simple_index

        n = len(index_a)
        index_b = index_a[0:-1]
        index_c = index_a[0:-1].append(index_a[-2:-1])
        index_d = index_a[0:1]

        msg = "Lengths must match|could not be broadcast"
        with pytest.raises(ValueError, match=msg):
            index_a == index_b
        expected1 = np.array([True] * n)
        expected2 = np.array([True] * (n - 1) + [False])
        tm.assert_numpy_array_equal(index_a == index_a, expected1)
        tm.assert_numpy_array_equal(index_a == index_c, expected2)

        # test comparisons with numpy arrays
        array_a = np.array(index_a)
        array_b = np.array(index_a[0:-1])
        array_c = np.array(index_a[0:-1].append(index_a[-2:-1]))
        array_d = np.array(index_a[0:1])
        with pytest.raises(ValueError, match=msg):
            index_a == array_b
        tm.assert_numpy_array_equal(index_a == array_a, expected1)
        tm.assert_numpy_array_equal(index_a == array_c, expected2)

        # test comparisons with Series
        series_a = Series(array_a)
        series_b = Series(array_b)
        series_c = Series(array_c)
        series_d = Series(array_d)
        with pytest.raises(ValueError, match=msg):
            index_a == series_b

        tm.assert_numpy_array_equal(index_a == series_a, expected1)
        tm.assert_numpy_array_equal(index_a == series_c, expected2)

        # cases where length is 1 for one of them
        with pytest.raises(ValueError, match="Lengths must match"):
            index_a == index_d
        with pytest.raises(ValueError, match="Lengths must match"):
            index_a == series_d
        with pytest.raises(ValueError, match="Lengths must match"):
            index_a == array_d
        msg = "Can only compare identically-labeled Series objects"
        with pytest.raises(ValueError, match=msg):
            series_a == series_d
        with pytest.raises(ValueError, match="Lengths must match"):
            series_a == array_d

        # comparing with a scalar should broadcast; note that we are excluding
        # MultiIndex because in this case each item in the index is a tuple of
        # length 2, and therefore is considered an array of length 2 in the
        # comparison instead of a scalar
        if not isinstance(index_a, MultiIndex):
            expected3 = np.array([False] * (len(index_a) - 2) + [True, False])
            # assuming the 2nd to last item is unique in the data
            item = index_a[-2]
            tm.assert_numpy_array_equal(index_a == item, expected3)
            # For RangeIndex we can convert to Int64Index
            tm.assert_series_equal(series_a == item, Series(expected3))

    def test_format(self, simple_index):
        # GH35439
        idx = simple_index
        expected = [str(x) for x in idx]
        assert idx.format() == expected

    def test_format_empty(self):
        # GH35712
        empty_idx = self._index_cls([])
        assert empty_idx.format() == []
        assert empty_idx.format(name=True) == [""]

    def test_fillna(self, index):
        # GH 11343
        if len(index) == 0:
            return
        elif isinstance(index, NumericIndex) and is_integer_dtype(index.dtype):
            return
        elif isinstance(index, MultiIndex):
            idx = index.copy(deep=True)
            msg = "isna is not defined for MultiIndex"
            with pytest.raises(NotImplementedError, match=msg):
                idx.fillna(idx[0])
        else:
            idx = index.copy(deep=True)
            result = idx.fillna(idx[0])
            tm.assert_index_equal(result, idx)
            assert result is not idx

            msg = "'value' must be a scalar, passed: "
            with pytest.raises(TypeError, match=msg):
                idx.fillna([idx[0]])

            idx = index.copy(deep=True)
            values = idx._values

            values[1] = np.nan

            idx = type(index)(values)

            msg = "does not support 'downcast'"
            with pytest.raises(NotImplementedError, match=msg):
                # For now at least, we only raise if there are NAs present
                idx.fillna(idx[0], downcast="infer")

            expected = np.array([False] * len(idx), dtype=bool)
            expected[1] = True
            tm.assert_numpy_array_equal(idx._isnan, expected)
            assert idx.hasnans is True

    def test_nulls(self, index):
        # this is really a smoke test for the methods
        # as these are adequately tested for function elsewhere
        if len(index) == 0:
            tm.assert_numpy_array_equal(index.isna(), np.array([], dtype=bool))
        elif isinstance(index, MultiIndex):
            idx = index.copy()
            msg = "isna is not defined for MultiIndex"
            with pytest.raises(NotImplementedError, match=msg):
                idx.isna()
        elif not index.hasnans:
            tm.assert_numpy_array_equal(index.isna(), np.zeros(len(index), dtype=bool))
            tm.assert_numpy_array_equal(index.notna(), np.ones(len(index), dtype=bool))
        else:
            result = isna(index)
            tm.assert_numpy_array_equal(index.isna(), result)
            tm.assert_numpy_array_equal(index.notna(), ~result)

    def test_empty(self, simple_index):
        # GH 15270
        idx = simple_index
        assert not idx.empty
        assert idx[:0].empty

    def test_join_self_unique(self, join_type, simple_index):
        idx = simple_index
        if idx.is_unique:
            joined = idx.join(idx, how=join_type)
            assert (idx == joined).all()

    def test_map(self, simple_index):
        # callable
        idx = simple_index

        result = idx.map(lambda x: x)
        # For RangeIndex we convert to Int64Index
        tm.assert_index_equal(result, idx, exact="equiv")

    @pytest.mark.parametrize(
        "mapper",
        [
            lambda values, index: {i: e for e, i in zip(values, index)},
            lambda values, index: Series(values, index),
        ],
    )
    def test_map_dictlike(self, mapper, simple_index):

        idx = simple_index
        if isinstance(idx, CategoricalIndex):
            # TODO(2.0): see if we can avoid skipping once
            #  CategoricalIndex.reindex is removed.
            pytest.skip(f"skipping tests for {type(idx)}")

        identity = mapper(idx.values, idx)

        result = idx.map(identity)
        # For RangeIndex we convert to Int64Index
        tm.assert_index_equal(result, idx, exact="equiv")

        # empty mappable
        dtype = None
        if idx._is_backward_compat_public_numeric_index:
            new_index_cls = NumericIndex
            if idx.dtype.kind == "f":
                dtype = idx.dtype
        else:
            new_index_cls = Float64Index

        expected = new_index_cls([np.nan] * len(idx), dtype=dtype)
        result = idx.map(mapper(expected, idx))
        tm.assert_index_equal(result, expected)

    def test_map_str(self, simple_index):
        # GH 31202
        idx = simple_index
        result = idx.map(str)
        expected = Index([str(x) for x in idx], dtype=object)
        tm.assert_index_equal(result, expected)

    @pytest.mark.parametrize("copy", [True, False])
    @pytest.mark.parametrize("name", [None, "foo"])
    @pytest.mark.parametrize("ordered", [True, False])
    def test_astype_category(self, copy, name, ordered, simple_index):
        # GH 18630
        idx = simple_index
        if name:
            idx = idx.rename(name)

        # standard categories
        dtype = CategoricalDtype(ordered=ordered)
        result = idx.astype(dtype, copy=copy)
        expected = CategoricalIndex(idx, name=name, ordered=ordered)
        tm.assert_index_equal(result, expected, exact=True)

        # non-standard categories
        dtype = CategoricalDtype(idx.unique().tolist()[:-1], ordered)
        result = idx.astype(dtype, copy=copy)
        expected = CategoricalIndex(idx, name=name, dtype=dtype)
        tm.assert_index_equal(result, expected, exact=True)

        if ordered is False:
            # dtype='category' defaults to ordered=False, so only test once
            result = idx.astype("category", copy=copy)
            expected = CategoricalIndex(idx, name=name)
            tm.assert_index_equal(result, expected, exact=True)

    def test_is_unique(self, simple_index):
        # initialize a unique index
        index = simple_index.drop_duplicates()
        assert index.is_unique is True

        # empty index should be unique
        index_empty = index[:0]
        assert index_empty.is_unique is True

        # test basic dupes
        index_dup = index.insert(0, index[0])
        assert index_dup.is_unique is False

        # single NA should be unique
        index_na = index.insert(0, np.nan)
        assert index_na.is_unique is True

        # multiple NA should not be unique
        index_na_dup = index_na.insert(0, np.nan)
        assert index_na_dup.is_unique is False

    @pytest.mark.arm_slow
    def test_engine_reference_cycle(self, simple_index):
        # GH27585
        index = simple_index
        nrefs_pre = len(gc.get_referrers(index))
        index._engine
        assert len(gc.get_referrers(index)) == nrefs_pre

    def test_getitem_2d_deprecated(self, simple_index):
        # GH#30588, GH#31479
        idx = simple_index
        msg = "Support for multi-dimensional indexing"
        with tm.assert_produces_warning(FutureWarning, match=msg):
            res = idx[:, None]

        assert isinstance(res, np.ndarray), type(res)

        if not isinstance(idx, RangeIndex):
            # GH#44051 RangeIndex already raises
            with tm.assert_produces_warning(FutureWarning, match=msg):
                res = idx[True]
            assert isinstance(res, np.ndarray), type(res)
            with tm.assert_produces_warning(FutureWarning, match=msg):
                res = idx[False]
            assert isinstance(res, np.ndarray), type(res)
        else:
            msg = "only integers, slices"
            with pytest.raises(IndexError, match=msg):
                idx[True]
            with pytest.raises(IndexError, match=msg):
                idx[False]

    def test_copy_shares_cache(self, simple_index):
        # GH32898, GH36840
        idx = simple_index
        idx.get_loc(idx[0])  # populates the _cache.
        copy = idx.copy()

        assert copy._cache is idx._cache

    def test_shallow_copy_shares_cache(self, simple_index):
        # GH32669, GH36840
        idx = simple_index
        idx.get_loc(idx[0])  # populates the _cache.
        shallow_copy = idx._view()

        assert shallow_copy._cache is idx._cache

        shallow_copy = idx._shallow_copy(idx._data)
        assert shallow_copy._cache is not idx._cache
        assert shallow_copy._cache == {}

    def test_index_groupby(self, simple_index):
        idx = simple_index[:5]
        to_groupby = np.array([1, 2, np.nan, 2, 1])
        tm.assert_dict_equal(
            idx.groupby(to_groupby), {1.0: idx[[0, 4]], 2.0: idx[[1, 3]]}
        )

        to_groupby = DatetimeIndex(
            [
                datetime(2011, 11, 1),
                datetime(2011, 12, 1),
                pd.NaT,
                datetime(2011, 12, 1),
                datetime(2011, 11, 1),
            ],
            tz="UTC",
        ).values

        ex_keys = [Timestamp("2011-11-01"), Timestamp("2011-12-01")]
        expected = {ex_keys[0]: idx[[0, 4]], ex_keys[1]: idx[[1, 3]]}
        tm.assert_dict_equal(idx.groupby(to_groupby), expected)

    def test_append_preserves_dtype(self, simple_index):
        # In particular NumericIndex with dtype float32
        index = simple_index
        N = len(index)

        result = index.append(index)
        assert result.dtype == index.dtype
        tm.assert_index_equal(result[:N], index, check_exact=True)
        tm.assert_index_equal(result[N:], index, check_exact=True)

        alt = index.take(list(range(N)) * 2)
        tm.assert_index_equal(result, alt, check_exact=True)

    def test_inv(self, simple_index):
        idx = simple_index

        if idx.dtype.kind in ["i", "u"]:
            res = ~idx
            expected = Index(~idx.values, name=idx.name)
            tm.assert_index_equal(res, expected)

            # check that we are matching Series behavior
            res2 = ~Series(idx)
            # TODO(2.0): once we preserve dtype, check_dtype can be True
            tm.assert_series_equal(res2, Series(expected), check_dtype=False)
        else:
            if idx.dtype.kind == "f":
                msg = "ufunc 'invert' not supported for the input types"
            else:
                msg = "bad operand"
            with pytest.raises(TypeError, match=msg):
                ~idx

            # check that we get the same behavior with Series
            with pytest.raises(TypeError, match=msg):
                ~Series(idx)


class NumericBase(Base):
    """
    Base class for numeric index (incl. RangeIndex) sub-class tests.
    """

    def test_constructor_unwraps_index(self, dtype):
        index_cls = self._index_cls

        idx = Index([1, 2], dtype=dtype)
        result = index_cls(idx)
        expected = np.array([1, 2], dtype=idx.dtype)
        tm.assert_numpy_array_equal(result._data, expected)

    def test_where(self):
        # Tested in numeric.test_indexing
        pass

    def test_can_hold_identifiers(self, simple_index):
        idx = simple_index
        key = idx[0]
        assert idx._can_hold_identifiers_and_holds_name(key) is False

    def test_format(self, simple_index):
        # GH35439
        idx = simple_index
        max_width = max(len(str(x)) for x in idx)
        expected = [str(x).ljust(max_width) for x in idx]
        assert idx.format() == expected

    def test_numeric_compat(self):
        pass  # override Base method

    def test_insert_non_na(self, simple_index):
        # GH#43921 inserting an element that we know we can hold should
        #  not change dtype or type (except for RangeIndex)
        index = simple_index

        result = index.insert(0, index[0])

        cls = type(index)
        if cls is RangeIndex:
            cls = Int64Index

        expected = cls([index[0]] + list(index), dtype=index.dtype)
        tm.assert_index_equal(result, expected, exact=True)

    def test_insert_na(self, nulls_fixture, simple_index):
        # GH 18295 (test missing)
        index = simple_index
        na_val = nulls_fixture

        if na_val is pd.NaT:
            expected = Index([index[0], pd.NaT] + list(index[1:]), dtype=object)
        else:
            expected = Float64Index([index[0], np.nan] + list(index[1:]))

            if index._is_backward_compat_public_numeric_index:
                # GH#43921 we preserve NumericIndex
                if index.dtype.kind == "f":
                    expected = NumericIndex(expected, dtype=index.dtype)
                else:
                    expected = NumericIndex(expected)

        result = index.insert(1, na_val)
        tm.assert_index_equal(result, expected, exact=True)

    def test_arithmetic_explicit_conversions(self):
        # GH 8608
        # add/sub are overridden explicitly for Float/Int Index
        index_cls = self._index_cls
        if index_cls is RangeIndex:
            idx = RangeIndex(5)
        else:
            idx = index_cls(np.arange(5, dtype="int64"))

        # float conversions
        arr = np.arange(5, dtype="int64") * 3.2
        expected = Float64Index(arr)
        fidx = idx * 3.2
        tm.assert_index_equal(fidx, expected)
        fidx = 3.2 * idx
        tm.assert_index_equal(fidx, expected)

        # interops with numpy arrays
        expected = Float64Index(arr)
        a = np.zeros(5, dtype="float64")
        result = fidx - a
        tm.assert_index_equal(result, expected)

        expected = Float64Index(-arr)
        a = np.zeros(5, dtype="float64")
        result = a - fidx
        tm.assert_index_equal(result, expected)

    def test_invalid_dtype(self, invalid_dtype):
        # GH 29539
        dtype = invalid_dtype
        msg = rf"Incorrect `dtype` passed: expected \w+(?: \w+)?, received {dtype}"
        with pytest.raises(ValueError, match=msg):
            self._index_cls([1, 2, 3], dtype=dtype)
