from datetime import datetime
import zoneinfo

import numpy as np
import pytest

from pandas import (
    NA,
    DatetimeIndex,
    Index,
    NaT,
    Timestamp,
    date_range,
)
import pandas._testing as tm


class TestInsert:
    @pytest.mark.parametrize(
        "null", [None, np.nan, np.datetime64("NaT", "ns"), NaT, NA]
    )
    @pytest.mark.parametrize("tz", [None, "UTC", "US/Eastern"])
    def test_insert_nat(self, tz, null):
        # GH#16537, GH#18295 (test missing)

        idx = DatetimeIndex(["2017-01-01"], tz=tz)
        expected = DatetimeIndex(["NaT", "2017-01-01"], tz=tz)
        if tz is not None and isinstance(null, np.datetime64):
            expected = Index([null, idx[0]], dtype=object)

        res = idx.insert(0, null)
        tm.assert_index_equal(res, expected)

    @pytest.mark.parametrize("tz", [None, "UTC", "US/Eastern"])
    def test_insert_invalid_na(self, tz):
        idx = DatetimeIndex(["2017-01-01"], tz=tz)

        item = np.timedelta64("NaT", "ns")
        result = idx.insert(0, item)
        expected = Index([item, *list(idx)], dtype=object)
        tm.assert_index_equal(result, expected)

    def test_insert_empty_preserves_freq(self, tz_naive_fixture):
        # GH#33573
        tz = tz_naive_fixture
        dti = DatetimeIndex([], tz=tz, freq="D")
        item = Timestamp("2017-04-05").tz_localize(tz)

        result = dti.insert(0, item)
        assert result.freq == dti.freq

        # But not when we insert an item that doesn't conform to freq
        dti = DatetimeIndex([], tz=tz, freq="W-THU")
        result = dti.insert(0, item)
        assert result.freq is None

    def test_insert(self, unit):
        idx = DatetimeIndex(
            ["2000-01-04", "2000-01-01", "2000-01-02"], name="idx"
        ).as_unit(unit)

        result = idx.insert(2, datetime(2000, 1, 5))
        exp = DatetimeIndex(
            ["2000-01-04", "2000-01-01", "2000-01-05", "2000-01-02"], name="idx"
        ).as_unit(unit)
        tm.assert_index_equal(result, exp)

        # insertion of non-datetime should coerce to object index
        result = idx.insert(1, "inserted")
        expected = Index(
            [
                datetime(2000, 1, 4),
                "inserted",
                datetime(2000, 1, 1),
                datetime(2000, 1, 2),
            ],
            name="idx",
        )
        assert not isinstance(result, DatetimeIndex)
        tm.assert_index_equal(result, expected)
        assert result.name == expected.name

    def test_insert2(self, unit):
        idx = date_range("1/1/2000", periods=3, freq="ME", name="idx", unit=unit)

        # preserve freq
        expected_0 = DatetimeIndex(
            ["1999-12-31", "2000-01-31", "2000-02-29", "2000-03-31"],
            name="idx",
            freq="ME",
        ).as_unit(unit)
        expected_3 = DatetimeIndex(
            ["2000-01-31", "2000-02-29", "2000-03-31", "2000-04-30"],
            name="idx",
            freq="ME",
        ).as_unit(unit)

        # reset freq to None
        expected_1_nofreq = DatetimeIndex(
            ["2000-01-31", "2000-01-31", "2000-02-29", "2000-03-31"],
            name="idx",
            freq=None,
        ).as_unit(unit)
        expected_3_nofreq = DatetimeIndex(
            ["2000-01-31", "2000-02-29", "2000-03-31", "2000-01-02"],
            name="idx",
            freq=None,
        ).as_unit(unit)

        cases = [
            (0, datetime(1999, 12, 31), expected_0),
            (-3, datetime(1999, 12, 31), expected_0),
            (3, datetime(2000, 4, 30), expected_3),
            (1, datetime(2000, 1, 31), expected_1_nofreq),
            (3, datetime(2000, 1, 2), expected_3_nofreq),
        ]

        for n, d, expected in cases:
            result = idx.insert(n, d)
            tm.assert_index_equal(result, expected)
            assert result.name == expected.name
            assert result.freq == expected.freq

    def test_insert3(self, unit):
        idx = date_range("1/1/2000", periods=3, freq="ME", name="idx", unit=unit)

        # reset freq to None
        result = idx.insert(3, datetime(2000, 1, 2))
        expected = DatetimeIndex(
            ["2000-01-31", "2000-02-29", "2000-03-31", "2000-01-02"],
            name="idx",
            freq=None,
        ).as_unit(unit)
        tm.assert_index_equal(result, expected)
        assert result.name == expected.name
        assert result.freq is None

    @pytest.mark.parametrize("tz", ["US/Pacific", "Asia/Singapore"])
    @pytest.mark.parametrize(
        "to_ts",
        [lambda x: x, lambda x: x.to_pydatetime()],
        ids=["Timestamp", "datetime"],
    )
    def test_insert4(self, unit, tz, to_ts):
        idx = date_range(
            "1/1/2000 09:00", periods=6, freq="h", tz=tz, name="idx", unit=unit
        )
        # preserve freq
        expected = date_range(
            "1/1/2000 09:00", periods=7, freq="h", tz=tz, name="idx", unit=unit
        )
        tz = zoneinfo.ZoneInfo(tz)
        d = to_ts(Timestamp("2000-01-01 15:00", tz=tz))
        result = idx.insert(6, d)
        tm.assert_index_equal(result, expected)
        assert result.name == expected.name
        assert result.freq == expected.freq
        assert result.tz == expected.tz

    @pytest.mark.parametrize("tz", ["US/Pacific", "Asia/Singapore"])
    @pytest.mark.parametrize(
        "to_ts",
        [lambda x: x, lambda x: x.to_pydatetime()],
        ids=["Timestamp", "datetime"],
    )
    def test_insert4_no_freq(self, unit, tz, to_ts):
        idx = date_range(
            "1/1/2000 09:00", periods=6, freq="h", tz=tz, name="idx", unit=unit
        )
        expected = DatetimeIndex(
            [
                "2000-01-01 09:00",
                "2000-01-01 10:00",
                "2000-01-01 11:00",
                "2000-01-01 12:00",
                "2000-01-01 13:00",
                "2000-01-01 14:00",
                "2000-01-01 10:00",
            ],
            name="idx",
            tz=tz,
            freq=None,
        ).as_unit(unit)
        # reset freq to None
        d = to_ts(Timestamp("2000-01-01 10:00", tz=tz))
        result = idx.insert(6, d)
        tm.assert_index_equal(result, expected)
        assert result.name == expected.name
        assert result.tz == expected.tz
        assert result.freq is None

    # TODO: also changes DataFrame.__setitem__ with expansion
    def test_insert_mismatched_tzawareness(self):
        # see GH#7299
        idx = date_range("1/1/2000", periods=3, freq="D", tz="Asia/Tokyo", name="idx")

        # mismatched tz-awareness
        item = Timestamp("2000-01-04")
        result = idx.insert(3, item)
        expected = Index(
            [*list(idx[:3]), item, *list(idx[3:])], dtype=object, name="idx"
        )
        tm.assert_index_equal(result, expected)

        # mismatched tz-awareness
        item = datetime(2000, 1, 4)
        result = idx.insert(3, item)
        expected = Index(
            [*list(idx[:3]), item, *list(idx[3:])], dtype=object, name="idx"
        )
        tm.assert_index_equal(result, expected)

    # TODO: also changes DataFrame.__setitem__ with expansion
    def test_insert_mismatched_tz(self):
        # see GH#7299
        # pre-2.0 with mismatched tzs we would cast to object
        idx = date_range(
            "1/1/2000", periods=3, freq="D", tz="Asia/Tokyo", unit="ns", name="idx"
        )

        # mismatched tz -> cast to object (could reasonably cast to same tz or UTC)
        item = Timestamp("2000-01-04", tz="US/Eastern")
        result = idx.insert(3, item)
        expected = Index(
            [*list(idx[:3]), item.tz_convert(idx.tz), *list(idx[3:])],
            name="idx",
        )
        assert expected.dtype == idx.dtype
        tm.assert_index_equal(result, expected)

        item = datetime(2000, 1, 4, tzinfo=zoneinfo.ZoneInfo("US/Eastern"))
        result = idx.insert(3, item)
        expected = Index(
            [*list(idx[:3]), item.astimezone(idx.tzinfo), *list(idx[3:])],
            name="idx",
        )
        assert expected.dtype == idx.dtype
        tm.assert_index_equal(result, expected)

    @pytest.mark.parametrize(
        "item", [0, np.int64(0), np.float64(0), np.array(0), np.timedelta64(456, "ns")]
    )
    def test_insert_mismatched_types_raises(self, tz_aware_fixture, item):
        # GH#33703 dont cast these to dt64
        tz = tz_aware_fixture
        dti = date_range("2019-11-04", periods=9, freq="-1D", name=9, tz=tz)

        result = dti.insert(1, item)

        if isinstance(item, np.ndarray):
            assert item.item() == 0
            expected = Index([dti[0], 0, *list(dti[1:])], dtype=object, name=9)
        else:
            expected = Index([dti[0], item, *list(dti[1:])], dtype=object, name=9)

        tm.assert_index_equal(result, expected)

    def test_insert_castable_str(self, tz_aware_fixture):
        # GH#33703
        tz = tz_aware_fixture
        dti = date_range("2019-11-04", periods=3, freq="-1D", name=9, tz=tz)

        value = "2019-11-05"
        result = dti.insert(0, value)

        ts = Timestamp(value).tz_localize(tz)
        expected = DatetimeIndex([ts, *list(dti)], dtype=dti.dtype, name=9)
        tm.assert_index_equal(result, expected)

    def test_insert_non_castable_str(self, tz_aware_fixture):
        # GH#33703
        tz = tz_aware_fixture
        dti = date_range("2019-11-04", periods=3, freq="-1D", name=9, tz=tz)

        value = "foo"
        result = dti.insert(0, value)

        expected = Index(["foo", *list(dti)], dtype=object, name=9)
        tm.assert_index_equal(result, expected)
