from datetime import timedelta

import numpy as np
import pytest

from pandas._libs.tslibs import iNaT
from pandas._libs.tslibs.period import IncompatibleFrequency

import pandas as pd


class TestPeriodArithmetic:
    def test_add_overflow_raises(self):
        # GH#55503
        per = pd.Timestamp.max.to_period("ns")

        msg = "|".join(
            [
                "Python int too large to convert to C long",
                # windows, 32bit linux builds
                "int too big to convert",
            ]
        )
        with pytest.raises(OverflowError, match=msg):
            per + 1

        msg = "value too large"
        with pytest.raises(OverflowError, match=msg):
            per + pd.Timedelta(1)
        with pytest.raises(OverflowError, match=msg):
            per + pd.offsets.Nano(1)

    def test_add_sub_nat_sentinel_raises(self):
        # GH#66552: an ordinal that lands exactly on the NaT sentinel is not
        #  NaT, but was rendered as NaT instead of raising like the step one
        #  further out already does
        msg = "Period ordinal is out of bounds"
        per = pd.Timestamp.min.to_period("ns")

        with pytest.raises(OverflowError, match=msg):
            per - 1
        with pytest.raises(OverflowError, match=msg):
            per - pd.Timedelta(1)
        with pytest.raises(OverflowError, match=msg):
            per + pd.offsets.Nano(-1)

        # non-tick offsets take a separate branch
        per = pd.Period(ordinal=iNaT + 1, freq="M")

        with pytest.raises(OverflowError, match=msg):
            per - 1
        with pytest.raises(OverflowError, match=msg):
            per + pd.offsets.MonthEnd(-1)

    def test_period_add_integer(self):
        per1 = pd.Period(freq="D", year=2008, month=1, day=1)
        per2 = pd.Period(freq="D", year=2008, month=1, day=2)
        assert per1 + 1 == per2
        assert 1 + per1 == per2

    def test_period_add_invalid(self):
        # GH#4731
        per1 = pd.Period(freq="D", year=2008, month=1, day=1)
        per2 = pd.Period(freq="D", year=2008, month=1, day=2)

        msg = "|".join(
            [
                r"unsupported operand type\(s\)",
                "can only concatenate str",
                "must be str, not Period",
            ]
        )
        with pytest.raises(TypeError, match=msg):
            per1 + "str"
        with pytest.raises(TypeError, match=msg):
            "str" + per1
        with pytest.raises(TypeError, match=msg):
            per1 + per2

    def test_period_sub_period_annual(self):
        left, right = pd.Period("2011", freq="Y"), pd.Period("2007", freq="Y")
        result = left - right
        assert result == 4 * right.freq

        msg = r"Input has different freq=M from Period\(freq=Y-DEC\)"
        with pytest.raises(IncompatibleFrequency, match=msg):
            left - pd.Period("2007-01", freq="M")

    def test_period_sub_period(self):
        per1 = pd.Period("2011-01-01", freq="D")
        per2 = pd.Period("2011-01-15", freq="D")

        off = per1.freq
        assert per1 - per2 == -14 * off
        assert per2 - per1 == 14 * off

        msg = r"Input has different freq=M from Period\(freq=D\)"
        with pytest.raises(IncompatibleFrequency, match=msg):
            per1 - pd.Period("2011-02", freq="M")

    @pytest.mark.parametrize("n", [1, 2, 3, 4])
    def test_sub_n_gt_1_ticks(self, tick_classes, n):
        # GH#23878
        p1 = pd.Period("19910905", freq=tick_classes(n))
        p2 = pd.Period("19920406", freq=tick_classes(n))

        expected = pd.Period(str(p2), freq=p2.freq.base) - pd.Period(
            str(p1), freq=p1.freq.base
        )

        assert (p2 - p1) == expected

    @pytest.mark.parametrize("normalize", [True, False])
    @pytest.mark.parametrize("n", [1, 2, 3, 4])
    @pytest.mark.parametrize(
        "offset, kwd_name",
        [
            (pd.offsets.YearEnd, "month"),
            (pd.offsets.QuarterEnd, "startingMonth"),
            (pd.offsets.MonthEnd, None),
            (pd.offsets.Week, "weekday"),
        ],
    )
    def test_sub_n_gt_1_offsets(self, offset, kwd_name, n, normalize):
        # GH#23878
        kwds = {kwd_name: 3} if kwd_name is not None else {}
        p1_d = "19910905"
        p2_d = "19920406"
        p1 = pd.Period(p1_d, freq=offset(n, normalize, **kwds))
        p2 = pd.Period(p2_d, freq=offset(n, normalize, **kwds))

        expected = pd.Period(p2_d, freq=p2.freq.base) - pd.Period(
            p1_d, freq=p1.freq.base
        )

        assert (p2 - p1) == expected

    def test_period_add_offset(self):
        # freq is DateOffset
        for freq in ["Y", "2Y", "3Y"]:
            per = pd.Period("2011", freq=freq)
            exp = pd.Period("2013", freq=freq)
            assert per + pd.offsets.YearEnd(2) == exp
            assert pd.offsets.YearEnd(2) + per == exp

            for off in [
                pd.offsets.YearBegin(2),
                pd.offsets.MonthBegin(1),
                pd.offsets.Minute(),
                np.timedelta64(365, "D"),
                timedelta(365),
            ]:
                msg = "|".join(
                    ["Input has different freq", "Input cannot be converted to Period"]
                )
                with pytest.raises(IncompatibleFrequency, match=msg):
                    per + off
                with pytest.raises(IncompatibleFrequency, match=msg):
                    off + per

        for freq in ["M", "2M", "3M"]:
            per = pd.Period("2011-03", freq=freq)
            exp = pd.Period("2011-05", freq=freq)
            assert per + pd.offsets.MonthEnd(2) == exp
            assert pd.offsets.MonthEnd(2) + per == exp

            exp = pd.Period("2012-03", freq=freq)
            assert per + pd.offsets.MonthEnd(12) == exp
            assert pd.offsets.MonthEnd(12) + per == exp

            msg = "|".join(
                [
                    "Input has different freq",
                    "Input cannot be converted to Period",
                ]
            )

            for off in [
                pd.offsets.YearBegin(2),
                pd.offsets.MonthBegin(1),
                pd.offsets.Minute(),
                np.timedelta64(365, "D"),
                timedelta(365),
            ]:
                with pytest.raises(IncompatibleFrequency, match=msg):
                    per + off
                with pytest.raises(IncompatibleFrequency, match=msg):
                    off + per

        # freq is Tick
        for freq in ["D", "2D", "3D"]:
            per = pd.Period("2011-04-01", freq=freq)

            exp = pd.Period("2011-04-06", freq=freq)
            assert per + pd.offsets.Day(5) == exp
            assert pd.offsets.Day(5) + per == exp

            exp = pd.Period("2011-04-02", freq=freq)
            assert per + pd.offsets.Hour(24) == exp
            assert pd.offsets.Hour(24) + per == exp

            exp = pd.Period("2011-04-03", freq=freq)
            assert per + np.timedelta64(2, "D") == exp
            assert np.timedelta64(2, "D") + per == exp

            exp = pd.Period("2011-04-02", freq=freq)
            assert per + np.timedelta64(3600 * 24, "s") == exp
            assert np.timedelta64(3600 * 24, "s") + per == exp

            exp = pd.Period("2011-03-30", freq=freq)
            assert per + timedelta(-2) == exp
            assert timedelta(-2) + per == exp

            exp = pd.Period("2011-04-03", freq=freq)
            assert per + timedelta(hours=48) == exp
            assert timedelta(hours=48) + per == exp

            msg = "|".join(
                [
                    "Input has different freq",
                    "Input cannot be converted to Period",
                ]
            )

            for off in [
                pd.offsets.YearBegin(2),
                pd.offsets.MonthBegin(1),
                pd.offsets.Minute(),
                np.timedelta64(4, "h"),
                timedelta(hours=23),
            ]:
                with pytest.raises(IncompatibleFrequency, match=msg):
                    per + off
                with pytest.raises(IncompatibleFrequency, match=msg):
                    off + per

        for freq in ["h", "2h", "3h"]:
            per = pd.Period("2011-04-01 09:00", freq=freq)

            exp = pd.Period("2011-04-03 09:00", freq=freq)
            assert per + pd.offsets.Day(2) == exp
            assert pd.offsets.Day(2) + per == exp

            exp = pd.Period("2011-04-01 12:00", freq=freq)
            assert per + pd.offsets.Hour(3) == exp
            assert pd.offsets.Hour(3) + per == exp

            msg = "cannot use operands with types"
            exp = pd.Period("2011-04-01 12:00", freq=freq)
            assert per + np.timedelta64(3, "h") == exp
            assert np.timedelta64(3, "h") + per == exp

            exp = pd.Period("2011-04-01 10:00", freq=freq)
            assert per + np.timedelta64(3600, "s") == exp
            assert np.timedelta64(3600, "s") + per == exp

            exp = pd.Period("2011-04-01 11:00", freq=freq)
            assert per + timedelta(minutes=120) == exp
            assert timedelta(minutes=120) + per == exp

            exp = pd.Period("2011-04-05 12:00", freq=freq)
            assert per + timedelta(days=4, minutes=180) == exp
            assert timedelta(days=4, minutes=180) + per == exp

            msg = "|".join(
                [
                    "Input has different freq",
                    "Input cannot be converted to Period",
                ]
            )

            for off in [
                pd.offsets.YearBegin(2),
                pd.offsets.MonthBegin(1),
                pd.offsets.Minute(),
                np.timedelta64(3200, "s"),
                timedelta(hours=23, minutes=30),
            ]:
                with pytest.raises(IncompatibleFrequency, match=msg):
                    per + off
                with pytest.raises(IncompatibleFrequency, match=msg):
                    off + per

    def test_period_sub_offset(self):
        # freq is DateOffset
        msg = "|".join(
            [
                "Input has different freq",
                "Input cannot be converted to Period",
            ]
        )

        for freq in ["Y", "2Y", "3Y"]:
            per = pd.Period("2011", freq=freq)
            assert per - pd.offsets.YearEnd(2) == pd.Period("2009", freq=freq)

            for off in [
                pd.offsets.YearBegin(2),
                pd.offsets.MonthBegin(1),
                pd.offsets.Minute(),
                np.timedelta64(365, "D"),
                timedelta(365),
            ]:
                with pytest.raises(IncompatibleFrequency, match=msg):
                    per - off

        for freq in ["M", "2M", "3M"]:
            per = pd.Period("2011-03", freq=freq)
            assert per - pd.offsets.MonthEnd(2) == pd.Period("2011-01", freq=freq)
            assert per - pd.offsets.MonthEnd(12) == pd.Period("2010-03", freq=freq)

            for off in [
                pd.offsets.YearBegin(2),
                pd.offsets.MonthBegin(1),
                pd.offsets.Minute(),
                np.timedelta64(365, "D"),
                timedelta(365),
            ]:
                with pytest.raises(IncompatibleFrequency, match=msg):
                    per - off

        # freq is Tick
        for freq in ["D", "2D", "3D"]:
            per = pd.Period("2011-04-01", freq=freq)
            assert per - pd.offsets.Day(5) == pd.Period("2011-03-27", freq=freq)
            assert per - pd.offsets.Hour(24) == pd.Period("2011-03-31", freq=freq)
            assert per - np.timedelta64(2, "D") == pd.Period("2011-03-30", freq=freq)
            assert per - np.timedelta64(3600 * 24, "s") == pd.Period(
                "2011-03-31", freq=freq
            )
            assert per - timedelta(-2) == pd.Period("2011-04-03", freq=freq)
            assert per - timedelta(hours=48) == pd.Period("2011-03-30", freq=freq)

            for off in [
                pd.offsets.YearBegin(2),
                pd.offsets.MonthBegin(1),
                pd.offsets.Minute(),
                np.timedelta64(4, "h"),
                timedelta(hours=23),
            ]:
                with pytest.raises(IncompatibleFrequency, match=msg):
                    per - off

        for freq in ["h", "2h", "3h"]:
            per = pd.Period("2011-04-01 09:00", freq=freq)
            assert per - pd.offsets.Day(2) == pd.Period("2011-03-30 09:00", freq=freq)
            assert per - pd.offsets.Hour(3) == pd.Period("2011-04-01 06:00", freq=freq)
            assert per - np.timedelta64(3, "h") == pd.Period(
                "2011-04-01 06:00", freq=freq
            )
            assert per - np.timedelta64(3600, "s") == pd.Period(
                "2011-04-01 08:00", freq=freq
            )
            assert per - timedelta(minutes=120) == pd.Period(
                "2011-04-01 07:00", freq=freq
            )
            assert per - timedelta(days=4, minutes=180) == pd.Period(
                "2011-03-28 06:00", freq=freq
            )

            for off in [
                pd.offsets.YearBegin(2),
                pd.offsets.MonthBegin(1),
                pd.offsets.Minute(),
                np.timedelta64(3200, "s"),
                timedelta(hours=23, minutes=30),
            ]:
                with pytest.raises(IncompatibleFrequency, match=msg):
                    per - off

    @pytest.mark.parametrize("freq", ["M", "2M", "3M"])
    def test_period_addsub_nat(self, freq):
        # GH#13071
        per = pd.Period("2011-01", freq=freq)

        # For subtraction, NaT is treated as another Period object
        assert pd.NaT - per is pd.NaT
        assert per - pd.NaT is pd.NaT

        # For addition, NaT is treated as offset-like
        assert pd.NaT + per is pd.NaT
        assert per + pd.NaT is pd.NaT

    @pytest.mark.parametrize("unit", ["ns", "us", "ms", "s", "m"])
    def test_period_add_sub_td64_nat(self, unit):
        # GH#47196
        per = pd.Period("2022-06-01", "D")
        nat = np.timedelta64("NaT", unit)

        assert per + nat is pd.NaT
        assert nat + per is pd.NaT
        assert per - nat is pd.NaT

        with pytest.raises(TypeError, match="unsupported operand"):
            nat - per

    def test_period_ops_offset(self):
        per = pd.Period("2011-04-01", freq="D")
        result = per + pd.offsets.Day()
        exp = pd.Period("2011-04-02", freq="D")
        assert result == exp

        result = per - pd.offsets.Day(2)
        exp = pd.Period("2011-03-30", freq="D")
        assert result == exp

        msg = r"Input cannot be converted to Period\(freq=D\)"
        with pytest.raises(IncompatibleFrequency, match=msg):
            per + pd.offsets.Hour(2)

        with pytest.raises(IncompatibleFrequency, match=msg):
            per - pd.offsets.Hour(2)

    def test_period_add_timestamp_raises(self):
        # GH#17983
        ts = pd.Timestamp("2017")
        per = pd.Period("2017", freq="M")

        msg = r"unsupported operand type\(s\) for \+: 'Timestamp' and 'Period'"
        with pytest.raises(TypeError, match=msg):
            ts + per

        msg = r"unsupported operand type\(s\) for \+: 'Period' and 'Timestamp'"
        with pytest.raises(TypeError, match=msg):
            per + ts


class TestPeriodComparisons:
    def test_period_comparison_same_freq(self):
        jan = pd.Period("2000-01", "M")
        feb = pd.Period("2000-02", "M")

        assert not jan == feb
        assert jan != feb
        assert jan < feb
        assert jan <= feb
        assert not jan > feb
        assert not jan >= feb

    def test_period_comparison_same_period_different_object(self):
        # Separate Period objects for the same period
        left = pd.Period("2000-01", "M")
        right = pd.Period("2000-01", "M")

        assert left == right
        assert left >= right
        assert left <= right
        assert not left < right
        assert not left > right

    def test_period_comparison_mismatched_freq(self):
        jan = pd.Period("2000-01", "M")
        day = pd.Period("2012-01-01", "D")

        assert not jan == day
        assert jan != day
        msg = r"Input has different freq=D from Period\(freq=M\)"
        with pytest.raises(IncompatibleFrequency, match=msg):
            jan < day
        with pytest.raises(IncompatibleFrequency, match=msg):
            jan <= day
        with pytest.raises(IncompatibleFrequency, match=msg):
            jan > day
        with pytest.raises(IncompatibleFrequency, match=msg):
            jan >= day

    def test_period_comparison_invalid_type(self):
        jan = pd.Period("2000-01", "M")

        assert not jan == 1
        assert jan != 1

        int_or_per = "'(Period|int)'"
        msg = f"not supported between instances of {int_or_per} and {int_or_per}"
        for left, right in [(jan, 1), (1, jan)]:
            with pytest.raises(TypeError, match=msg):
                left > right
            with pytest.raises(TypeError, match=msg):
                left >= right
            with pytest.raises(TypeError, match=msg):
                left < right
            with pytest.raises(TypeError, match=msg):
                left <= right

    def test_period_comparison_nat(self):
        per = pd.Period("2011-01-01", freq="D")

        ts = pd.Timestamp("2011-01-01")
        # confirm Period('NaT') work identical with Timestamp('NaT')
        for left, right in [
            (pd.NaT, per),
            (per, pd.NaT),
            (pd.NaT, ts),
            (ts, pd.NaT),
        ]:
            assert not left < right
            assert not left > right
            assert not left == right
            assert left != right
            assert not left <= right
            assert not left >= right

    @pytest.mark.parametrize(
        "scalar, expected",
        ((0, False), (pd.Period("2000-01", "M"), True)),
    )
    def test_period_comparison_numpy_zerodim_arr(self, scalar, expected):
        zerodim_arr = np.array(scalar)
        per = pd.Period("2000-01", "M")

        assert (per == zerodim_arr) is expected
        assert (zerodim_arr == per) is expected
