"""
Collection of tests asserting things that should be true for
any index subclass. Makes use of the `indices` fixture defined
in pandas/tests/indexes/conftest.py.
"""
import re

import numpy as np
import pytest

from pandas._libs.tslibs import iNaT

from pandas.core.dtypes.common import needs_i8_conversion

import pandas as pd
from pandas import CategoricalIndex, MultiIndex, RangeIndex
import pandas._testing as tm


class TestCommon:
    def test_droplevel(self, indices):
        # GH 21115
        if isinstance(indices, MultiIndex):
            # Tested separately in test_multi.py
            return

        assert indices.droplevel([]).equals(indices)

        for level in indices.name, [indices.name]:
            if isinstance(indices.name, tuple) and level is indices.name:
                # GH 21121 : droplevel with tuple name
                continue
            with pytest.raises(ValueError):
                indices.droplevel(level)

        for level in "wrong", ["wrong"]:
            with pytest.raises(
                KeyError,
                match=r"'Requested level \(wrong\) does not match index name \(None\)'",
            ):
                indices.droplevel(level)

    def test_constructor_non_hashable_name(self, indices):
        # GH 20527

        if isinstance(indices, MultiIndex):
            pytest.skip("multiindex handled in test_multi.py")

        message = "Index.name must be a hashable type"
        renamed = [["1"]]

        # With .rename()
        with pytest.raises(TypeError, match=message):
            indices.rename(name=renamed)

        # With .set_names()
        with pytest.raises(TypeError, match=message):
            indices.set_names(names=renamed)

    def test_constructor_unwraps_index(self, indices):
        if isinstance(indices, pd.MultiIndex):
            raise pytest.skip("MultiIndex has no ._data")
        a = indices
        b = type(a)(a)
        tm.assert_equal(a._data, b._data)

    @pytest.mark.parametrize("itm", [101, "no_int"])
    # FutureWarning from non-tuple sequence of nd indexing
    @pytest.mark.filterwarnings("ignore::FutureWarning")
    def test_getitem_error(self, indices, itm):
        with pytest.raises(IndexError):
            indices[itm]

    @pytest.mark.parametrize(
        "fname, sname, expected_name",
        [
            ("A", "A", "A"),
            ("A", "B", None),
            ("A", None, None),
            (None, "B", None),
            (None, None, None),
        ],
    )
    def test_corner_union(self, indices, fname, sname, expected_name):
        # GH 9943 9862
        # Test unions with various name combinations
        # Do not test MultiIndex or repeats

        if isinstance(indices, MultiIndex) or not indices.is_unique:
            pytest.skip("Not for MultiIndex or repeated indices")

        # Test copy.union(copy)
        first = indices.copy().set_names(fname)
        second = indices.copy().set_names(sname)
        union = first.union(second)
        expected = indices.copy().set_names(expected_name)
        tm.assert_index_equal(union, expected)

        # Test copy.union(empty)
        first = indices.copy().set_names(fname)
        second = indices.drop(indices).set_names(sname)
        union = first.union(second)
        expected = indices.copy().set_names(expected_name)
        tm.assert_index_equal(union, expected)

        # Test empty.union(copy)
        first = indices.drop(indices).set_names(fname)
        second = indices.copy().set_names(sname)
        union = first.union(second)
        expected = indices.copy().set_names(expected_name)
        tm.assert_index_equal(union, expected)

        # Test empty.union(empty)
        first = indices.drop(indices).set_names(fname)
        second = indices.drop(indices).set_names(sname)
        union = first.union(second)
        expected = indices.drop(indices).set_names(expected_name)
        tm.assert_index_equal(union, expected)

    def test_to_flat_index(self, indices):
        # 22866
        if isinstance(indices, MultiIndex):
            pytest.skip("Separate expectation for MultiIndex")

        result = indices.to_flat_index()
        tm.assert_index_equal(result, indices)

    def test_wrong_number_names(self, indices):
        with pytest.raises(ValueError, match="^Length"):
            indices.names = ["apple", "banana", "carrot"]

    def test_set_name_methods(self, indices):
        new_name = "This is the new name for this index"

        # don't tests a MultiIndex here (as its tested separated)
        if isinstance(indices, MultiIndex):
            pytest.skip("Skip check for MultiIndex")
        original_name = indices.name
        new_ind = indices.set_names([new_name])
        assert new_ind.name == new_name
        assert indices.name == original_name
        res = indices.rename(new_name, inplace=True)

        # should return None
        assert res is None
        assert indices.name == new_name
        assert indices.names == [new_name]
        # FIXME: dont leave commented-out
        # with pytest.raises(TypeError, match="list-like"):
        #    # should still fail even if it would be the right length
        #    ind.set_names("a")
        with pytest.raises(ValueError, match="Level must be None"):
            indices.set_names("a", level=0)

        # rename in place just leaves tuples and other containers alone
        name = ("A", "B")
        indices.rename(name, inplace=True)
        assert indices.name == name
        assert indices.names == [name]

    def test_hash_error(self, indices):
        index = indices
        with pytest.raises(
            TypeError, match=f"unhashable type: '{type(index).__name__}'"
        ):
            hash(indices)

    def test_copy_and_deepcopy(self, indices):
        from copy import copy, deepcopy

        if isinstance(indices, MultiIndex):
            pytest.skip("Skip check for MultiIndex")

        for func in (copy, deepcopy):
            idx_copy = func(indices)
            assert idx_copy is not indices
            assert idx_copy.equals(indices)

        new_copy = indices.copy(deep=True, name="banana")
        assert new_copy.name == "banana"

    def test_unique(self, indices):
        # don't test a MultiIndex here (as its tested separated)
        # don't test a CategoricalIndex because categories change (GH 18291)
        if isinstance(indices, (MultiIndex, CategoricalIndex)):
            pytest.skip("Skip check for MultiIndex/CategoricalIndex")

        # GH 17896
        expected = indices.drop_duplicates()
        for level in 0, indices.name, None:
            result = indices.unique(level=level)
            tm.assert_index_equal(result, expected)

        msg = "Too many levels: Index has only 1 level, not 4"
        with pytest.raises(IndexError, match=msg):
            indices.unique(level=3)

        msg = (
            fr"Requested level \(wrong\) does not match index name "
            fr"\({re.escape(indices.name.__repr__())}\)"
        )
        with pytest.raises(KeyError, match=msg):
            indices.unique(level="wrong")

    def test_get_unique_index(self, indices):
        # MultiIndex tested separately
        if not len(indices) or isinstance(indices, MultiIndex):
            pytest.skip("Skip check for empty Index and MultiIndex")

        idx = indices[[0] * 5]
        idx_unique = indices[[0]]

        # We test against `idx_unique`, so first we make sure it's unique
        # and doesn't contain nans.
        assert idx_unique.is_unique is True
        try:
            assert idx_unique.hasnans is False
        except NotImplementedError:
            pass

        for dropna in [False, True]:
            result = idx._get_unique_index(dropna=dropna)
            tm.assert_index_equal(result, idx_unique)

        # nans:
        if not indices._can_hold_na:
            pytest.skip("Skip na-check if index cannot hold na")

        if needs_i8_conversion(indices):
            vals = indices.asi8[[0] * 5]
            vals[0] = iNaT
        else:
            vals = indices.values[[0] * 5]
            vals[0] = np.nan

        vals_unique = vals[:2]
        idx_nan = indices._shallow_copy(vals)
        idx_unique_nan = indices._shallow_copy(vals_unique)
        assert idx_unique_nan.is_unique is True

        assert idx_nan.dtype == indices.dtype
        assert idx_unique_nan.dtype == indices.dtype

        for dropna, expected in zip([False, True], [idx_unique_nan, idx_unique]):
            for i in [idx_nan, idx_unique_nan]:
                result = i._get_unique_index(dropna=dropna)
                tm.assert_index_equal(result, expected)

    def test_sort(self, indices):
        msg = "cannot sort an Index object in-place, use sort_values instead"
        with pytest.raises(TypeError, match=msg):
            indices.sort()

    def test_mutability(self, indices):
        if not len(indices):
            pytest.skip("Skip check for empty Index")
        msg = "Index does not support mutable operations"
        with pytest.raises(TypeError, match=msg):
            indices[0] = indices[0]

    def test_view(self, indices):
        assert indices.view().name == indices.name

    def test_compat(self, indices):
        assert indices.tolist() == list(indices)

    def test_searchsorted_monotonic(self, indices):
        # GH17271
        # not implemented for tuple searches in MultiIndex
        # or Intervals searches in IntervalIndex
        if isinstance(indices, (MultiIndex, pd.IntervalIndex)):
            pytest.skip("Skip check for MultiIndex/IntervalIndex")

        # nothing to test if the index is empty
        if indices.empty:
            pytest.skip("Skip check for empty Index")
        value = indices[0]

        # determine the expected results (handle dupes for 'right')
        expected_left, expected_right = 0, (indices == value).argmin()
        if expected_right == 0:
            # all values are the same, expected_right should be length
            expected_right = len(indices)

        # test _searchsorted_monotonic in all cases
        # test searchsorted only for increasing
        if indices.is_monotonic_increasing:
            ssm_left = indices._searchsorted_monotonic(value, side="left")
            assert expected_left == ssm_left

            ssm_right = indices._searchsorted_monotonic(value, side="right")
            assert expected_right == ssm_right

            ss_left = indices.searchsorted(value, side="left")
            assert expected_left == ss_left

            ss_right = indices.searchsorted(value, side="right")
            assert expected_right == ss_right

        elif indices.is_monotonic_decreasing:
            ssm_left = indices._searchsorted_monotonic(value, side="left")
            assert expected_left == ssm_left

            ssm_right = indices._searchsorted_monotonic(value, side="right")
            assert expected_right == ssm_right
        else:
            # non-monotonic should raise.
            with pytest.raises(ValueError):
                indices._searchsorted_monotonic(value, side="left")

    def test_pickle(self, indices):
        original_name, indices.name = indices.name, "foo"
        unpickled = tm.round_trip_pickle(indices)
        assert indices.equals(unpickled)
        indices.name = original_name

    @pytest.mark.parametrize("keep", ["first", "last", False])
    def test_duplicated(self, indices, keep):
        if not len(indices) or isinstance(indices, (MultiIndex, RangeIndex)):
            # MultiIndex tested separately in:
            # tests/indexes/multi/test_unique_and_duplicates
            pytest.skip("Skip check for empty Index, MultiIndex, RangeIndex")

        holder = type(indices)

        idx = holder(indices)
        if idx.has_duplicates:
            # We are testing the duplicated-method here, so we need to know
            # exactly which indices are duplicate and how (for the result).
            # This is not possible if "idx" has duplicates already, which we
            # therefore remove. This is seemingly circular, as drop_duplicates
            # invokes duplicated, but in the end, it all works out because we
            # cross-check with Series.duplicated, which is tested separately.
            idx = idx.drop_duplicates()

        n, k = len(idx), 10
        duplicated_selection = np.random.choice(n, k * n)
        expected = pd.Series(duplicated_selection).duplicated(keep=keep).values
        idx = holder(idx.values[duplicated_selection])

        result = idx.duplicated(keep=keep)
        tm.assert_numpy_array_equal(result, expected)

    def test_has_duplicates(self, indices):
        holder = type(indices)
        if not len(indices) or isinstance(indices, (MultiIndex, RangeIndex)):
            # MultiIndex tested separately in:
            #   tests/indexes/multi/test_unique_and_duplicates.
            # RangeIndex is unique by definition.
            pytest.skip("Skip check for empty Index, MultiIndex, and RangeIndex")

        idx = holder([indices[0]] * 5)
        assert idx.is_unique is False
        assert idx.has_duplicates is True
