import numpy
import numpy as np
import datetime
import pytest
from numpy.testing import (
    IS_WASM,
    assert_, assert_equal, assert_raises, assert_warns, suppress_warnings,
    assert_raises_regex, assert_array_equal,
    )
from numpy.compat import pickle

# Use pytz to test out various time zones if available
try:
    from pytz import timezone as tz
    _has_pytz = True
except ImportError:
    _has_pytz = False

try:
    RecursionError
except NameError:
    RecursionError = RuntimeError  # python < 3.5


class TestDateTime:
    def test_datetime_dtype_creation(self):
        for unit in ['Y', 'M', 'W', 'D',
                     'h', 'm', 's', 'ms', 'us',
                     'μs',  # alias for us
                     'ns', 'ps', 'fs', 'as']:
            dt1 = np.dtype('M8[750%s]' % unit)
            assert_(dt1 == np.dtype('datetime64[750%s]' % unit))
            dt2 = np.dtype('m8[%s]' % unit)
            assert_(dt2 == np.dtype('timedelta64[%s]' % unit))

        # Generic units shouldn't add [] to the end
        assert_equal(str(np.dtype("M8")), "datetime64")

        # Should be possible to specify the endianness
        assert_equal(np.dtype("=M8"), np.dtype("M8"))
        assert_equal(np.dtype("=M8[s]"), np.dtype("M8[s]"))
        assert_(np.dtype(">M8") == np.dtype("M8") or
                np.dtype("M8[D]") == np.dtype("M8[D]") or
                np.dtype("M8") != np.dtype("m8") == np.dtype("m8") or
                np.dtype("m8[D]") == np.dtype("m8[D]") or
                np.dtype("m8") != np.dtype(" Scalars
        assert_equal(np.datetime64(b, '[s]'), np.datetime64('NaT', '[s]'))
        assert_equal(np.datetime64(b, '[ms]'), np.datetime64('NaT', '[ms]'))
        assert_equal(np.datetime64(b, '[M]'), np.datetime64('NaT', '[M]'))
        assert_equal(np.datetime64(b, '[Y]'), np.datetime64('NaT', '[Y]'))
        assert_equal(np.datetime64(b, '[W]'), np.datetime64('NaT', '[W]'))

        # Arrays -> Scalars
        assert_equal(np.datetime64(a, '[s]'), np.datetime64('NaT', '[s]'))
        assert_equal(np.datetime64(a, '[ms]'), np.datetime64('NaT', '[ms]'))
        assert_equal(np.datetime64(a, '[M]'), np.datetime64('NaT', '[M]'))
        assert_equal(np.datetime64(a, '[Y]'), np.datetime64('NaT', '[Y]'))
        assert_equal(np.datetime64(a, '[W]'), np.datetime64('NaT', '[W]'))

        # NaN -> NaT
        nan = np.array([np.nan] * 8)
        fnan = nan.astype('f')
        lnan = nan.astype('g')
        cnan = nan.astype('D')
        cfnan = nan.astype('F')
        clnan = nan.astype('G')

        nat = np.array([np.datetime64('NaT')] * 8)
        assert_equal(nan.astype('M8[ns]'), nat)
        assert_equal(fnan.astype('M8[ns]'), nat)
        assert_equal(lnan.astype('M8[ns]'), nat)
        assert_equal(cnan.astype('M8[ns]'), nat)
        assert_equal(cfnan.astype('M8[ns]'), nat)
        assert_equal(clnan.astype('M8[ns]'), nat)

        nat = np.array([np.timedelta64('NaT')] * 8)
        assert_equal(nan.astype('timedelta64[ns]'), nat)
        assert_equal(fnan.astype('timedelta64[ns]'), nat)
        assert_equal(lnan.astype('timedelta64[ns]'), nat)
        assert_equal(cnan.astype('timedelta64[ns]'), nat)
        assert_equal(cfnan.astype('timedelta64[ns]'), nat)
        assert_equal(clnan.astype('timedelta64[ns]'), nat)

    def test_days_creation(self):
        assert_equal(np.array('1599', dtype='M8[D]').astype('i8'),
                (1600-1970)*365 - (1972-1600)/4 + 3 - 365)
        assert_equal(np.array('1600', dtype='M8[D]').astype('i8'),
                (1600-1970)*365 - (1972-1600)/4 + 3)
        assert_equal(np.array('1601', dtype='M8[D]').astype('i8'),
                (1600-1970)*365 - (1972-1600)/4 + 3 + 366)
        assert_equal(np.array('1900', dtype='M8[D]').astype('i8'),
                (1900-1970)*365 - (1970-1900)//4)
        assert_equal(np.array('1901', dtype='M8[D]').astype('i8'),
                (1900-1970)*365 - (1970-1900)//4 + 365)
        assert_equal(np.array('1967', dtype='M8[D]').astype('i8'), -3*365 - 1)
        assert_equal(np.array('1968', dtype='M8[D]').astype('i8'), -2*365 - 1)
        assert_equal(np.array('1969', dtype='M8[D]').astype('i8'), -1*365)
        assert_equal(np.array('1970', dtype='M8[D]').astype('i8'), 0*365)
        assert_equal(np.array('1971', dtype='M8[D]').astype('i8'), 1*365)
        assert_equal(np.array('1972', dtype='M8[D]').astype('i8'), 2*365)
        assert_equal(np.array('1973', dtype='M8[D]').astype('i8'), 3*365 + 1)
        assert_equal(np.array('1974', dtype='M8[D]').astype('i8'), 4*365 + 1)
        assert_equal(np.array('2000', dtype='M8[D]').astype('i8'),
                 (2000 - 1970)*365 + (2000 - 1972)//4)
        assert_equal(np.array('2001', dtype='M8[D]').astype('i8'),
                 (2000 - 1970)*365 + (2000 - 1972)//4 + 366)
        assert_equal(np.array('2400', dtype='M8[D]').astype('i8'),
                 (2400 - 1970)*365 + (2400 - 1972)//4 - 3)
        assert_equal(np.array('2401', dtype='M8[D]').astype('i8'),
                 (2400 - 1970)*365 + (2400 - 1972)//4 - 3 + 366)

        assert_equal(np.array('1600-02-29', dtype='M8[D]').astype('i8'),
                (1600-1970)*365 - (1972-1600)//4 + 3 + 31 + 28)
        assert_equal(np.array('1600-03-01', dtype='M8[D]').astype('i8'),
                (1600-1970)*365 - (1972-1600)//4 + 3 + 31 + 29)
        assert_equal(np.array('2000-02-29', dtype='M8[D]').astype('i8'),
                 (2000 - 1970)*365 + (2000 - 1972)//4 + 31 + 28)
        assert_equal(np.array('2000-03-01', dtype='M8[D]').astype('i8'),
                 (2000 - 1970)*365 + (2000 - 1972)//4 + 31 + 29)
        assert_equal(np.array('2001-03-22', dtype='M8[D]').astype('i8'),
                 (2000 - 1970)*365 + (2000 - 1972)//4 + 366 + 31 + 28 + 21)

    def test_days_to_pydate(self):
        assert_equal(np.array('1599', dtype='M8[D]').astype('O'),
                    datetime.date(1599, 1, 1))
        assert_equal(np.array('1600', dtype='M8[D]').astype('O'),
                    datetime.date(1600, 1, 1))
        assert_equal(np.array('1601', dtype='M8[D]').astype('O'),
                    datetime.date(1601, 1, 1))
        assert_equal(np.array('1900', dtype='M8[D]').astype('O'),
                    datetime.date(1900, 1, 1))
        assert_equal(np.array('1901', dtype='M8[D]').astype('O'),
                    datetime.date(1901, 1, 1))
        assert_equal(np.array('2000', dtype='M8[D]').astype('O'),
                    datetime.date(2000, 1, 1))
        assert_equal(np.array('2001', dtype='M8[D]').astype('O'),
                    datetime.date(2001, 1, 1))
        assert_equal(np.array('1600-02-29', dtype='M8[D]').astype('O'),
                    datetime.date(1600, 2, 29))
        assert_equal(np.array('1600-03-01', dtype='M8[D]').astype('O'),
                    datetime.date(1600, 3, 1))
        assert_equal(np.array('2001-03-22', dtype='M8[D]').astype('O'),
                    datetime.date(2001, 3, 22))

    def test_dtype_comparison(self):
        assert_(not (np.dtype('M8[us]') == np.dtype('M8[ms]')))
        assert_(np.dtype('M8[us]') != np.dtype('M8[ms]'))
        assert_(np.dtype('M8[2D]') != np.dtype('M8[D]'))
        assert_(np.dtype('M8[D]') != np.dtype('M8[2D]'))

    def test_pydatetime_creation(self):
        a = np.array(['1960-03-12', datetime.date(1960, 3, 12)], dtype='M8[D]')
        assert_equal(a[0], a[1])
        a = np.array(['1999-12-31', datetime.date(1999, 12, 31)], dtype='M8[D]')
        assert_equal(a[0], a[1])
        a = np.array(['2000-01-01', datetime.date(2000, 1, 1)], dtype='M8[D]')
        assert_equal(a[0], a[1])
        # Will fail if the date changes during the exact right moment
        a = np.array(['today', datetime.date.today()], dtype='M8[D]')
        assert_equal(a[0], a[1])
        # datetime.datetime.now() returns local time, not UTC
        #a = np.array(['now', datetime.datetime.now()], dtype='M8[s]')
        #assert_equal(a[0], a[1])

        # we can give a datetime.date time units
        assert_equal(np.array(datetime.date(1960, 3, 12), dtype='M8[s]'),
                     np.array(np.datetime64('1960-03-12T00:00:00')))

    def test_datetime_string_conversion(self):
        a = ['2011-03-16', '1920-01-01', '2013-05-19']
        str_a = np.array(a, dtype='S')
        uni_a = np.array(a, dtype='U')
        dt_a = np.array(a, dtype='M')

        # String to datetime
        assert_equal(dt_a, str_a.astype('M'))
        assert_equal(dt_a.dtype, str_a.astype('M').dtype)
        dt_b = np.empty_like(dt_a)
        dt_b[...] = str_a
        assert_equal(dt_a, dt_b)

        # Datetime to string
        assert_equal(str_a, dt_a.astype('S0'))
        str_b = np.empty_like(str_a)
        str_b[...] = dt_a
        assert_equal(str_a, str_b)

        # Unicode to datetime
        assert_equal(dt_a, uni_a.astype('M'))
        assert_equal(dt_a.dtype, uni_a.astype('M').dtype)
        dt_b = np.empty_like(dt_a)
        dt_b[...] = uni_a
        assert_equal(dt_a, dt_b)

        # Datetime to unicode
        assert_equal(uni_a, dt_a.astype('U'))
        uni_b = np.empty_like(uni_a)
        uni_b[...] = dt_a
        assert_equal(uni_a, uni_b)

        # Datetime to long string - gh-9712
        assert_equal(str_a, dt_a.astype((np.bytes_, 128)))
        str_b = np.empty(str_a.shape, dtype=(np.bytes_, 128))
        str_b[...] = dt_a
        assert_equal(str_a, str_b)

    @pytest.mark.parametrize("time_dtype", ["m8[D]", "M8[Y]"])
    def test_time_byteswapping(self, time_dtype):
        times = np.array(["2017", "NaT"], dtype=time_dtype)
        times_swapped = times.astype(times.dtype.newbyteorder())
        assert_array_equal(times, times_swapped)

        unswapped = times_swapped.view(np.int64).newbyteorder()
        assert_array_equal(unswapped, times.view(np.int64))

    @pytest.mark.parametrize(["time1", "time2"],
            [("M8[s]", "M8[D]"), ("m8[s]", "m8[ns]")])
    def test_time_byteswapped_cast(self, time1, time2):
        dtype1 = np.dtype(time1)
        dtype2 = np.dtype(time2)
        times = np.array(["2017", "NaT"], dtype=dtype1)
        expected = times.astype(dtype2)

        # Test that every byte-swapping combination also returns the same
        # results (previous tests check that this comparison works fine).
        res = times.astype(dtype1.newbyteorder()).astype(dtype2)
        assert_array_equal(res, expected)
        res = times.astype(dtype2.newbyteorder())
        assert_array_equal(res, expected)
        res = times.astype(dtype1.newbyteorder()).astype(dtype2.newbyteorder())
        assert_array_equal(res, expected)

    @pytest.mark.parametrize("time_dtype", ["m8[D]", "M8[Y]"])
    @pytest.mark.parametrize("str_dtype", ["U", "S"])
    def test_datetime_conversions_byteorders(self, str_dtype, time_dtype):
        times = np.array(["2017", "NaT"], dtype=time_dtype)
        # Unfortunately, timedelta does not roundtrip:
        from_strings = np.array(["2017", "NaT"], dtype=str_dtype)
        to_strings = times.astype(str_dtype)  # assume this is correct

        # Check that conversion from times to string works if src is swapped:
        times_swapped = times.astype(times.dtype.newbyteorder())
        res = times_swapped.astype(str_dtype)
        assert_array_equal(res, to_strings)
        # And also if both are swapped:
        res = times_swapped.astype(to_strings.dtype.newbyteorder())
        assert_array_equal(res, to_strings)
        # only destination is swapped:
        res = times.astype(to_strings.dtype.newbyteorder())
        assert_array_equal(res, to_strings)

        # Check that conversion from string to times works if src is swapped:
        from_strings_swapped = from_strings.astype(
                from_strings.dtype.newbyteorder())
        res = from_strings_swapped.astype(time_dtype)
        assert_array_equal(res, times)
        # And if both are swapped:
        res = from_strings_swapped.astype(times.dtype.newbyteorder())
        assert_array_equal(res, times)
        # Only destination is swapped:
        res = from_strings.astype(times.dtype.newbyteorder())
        assert_array_equal(res, times)

    def test_datetime_array_str(self):
        a = np.array(['2011-03-16', '1920-01-01', '2013-05-19'], dtype='M')
        assert_equal(str(a), "['2011-03-16' '1920-01-01' '2013-05-19']")

        a = np.array(['2011-03-16T13:55', '1920-01-01T03:12'], dtype='M')
        assert_equal(np.array2string(a, separator=', ',
                    formatter={'datetime': lambda x:
                            "'%s'" % np.datetime_as_string(x, timezone='UTC')}),
                     "['2011-03-16T13:55Z', '1920-01-01T03:12Z']")

        # Check that one NaT doesn't corrupt subsequent entries
        a = np.array(['2010', 'NaT', '2030']).astype('M')
        assert_equal(str(a), "['2010'  'NaT' '2030']")

    def test_timedelta_array_str(self):
        a = np.array([-1, 0, 100], dtype='m')
        assert_equal(str(a), "[ -1   0 100]")
        a = np.array(['NaT', 'NaT'], dtype='m')
        assert_equal(str(a), "['NaT' 'NaT']")
        # Check right-alignment with NaTs
        a = np.array([-1, 'NaT', 0], dtype='m')
        assert_equal(str(a), "[   -1 'NaT'     0]")
        a = np.array([-1, 'NaT', 1234567], dtype='m')
        assert_equal(str(a), "[     -1   'NaT' 1234567]")

        # Test with other byteorder:
        a = np.array([-1, 'NaT', 1234567], dtype='>m')
        assert_equal(str(a), "[     -1   'NaT' 1234567]")
        a = np.array([-1, 'NaT', 1234567], dtype=''\np4\nNNNI-1\nI-1\nI0\n((dp5\n(S'us'\np6\n" + \
              b"I1\nI1\nI1\ntp7\ntp8\ntp9\nb."
        assert_equal(pickle.loads(pkl), np.dtype('>M8[us]'))

    def test_setstate(self):
        "Verify that datetime dtype __setstate__ can handle bad arguments"
        dt = np.dtype('>M8[us]')
        assert_raises(ValueError, dt.__setstate__, (4, '>', None, None, None, -1, -1, 0, 1))
        assert_(dt.__reduce__()[2] == np.dtype('>M8[us]').__reduce__()[2])
        assert_raises(TypeError, dt.__setstate__, (4, '>', None, None, None, -1, -1, 0, ({}, 'xxx')))
        assert_(dt.__reduce__()[2] == np.dtype('>M8[us]').__reduce__()[2])

    def test_dtype_promotion(self):
        # datetime  datetime computes the metadata gcd
        # timedelta  timedelta computes the metadata gcd
        for mM in ['m', 'M']:
            assert_equal(
                np.promote_types(np.dtype(mM+'8[2Y]'), np.dtype(mM+'8[2Y]')),
                np.dtype(mM+'8[2Y]'))
            assert_equal(
                np.promote_types(np.dtype(mM+'8[12Y]'), np.dtype(mM+'8[15Y]')),
                np.dtype(mM+'8[3Y]'))
            assert_equal(
                np.promote_types(np.dtype(mM+'8[62M]'), np.dtype(mM+'8[24M]')),
                np.dtype(mM+'8[2M]'))
            assert_equal(
                np.promote_types(np.dtype(mM+'8[1W]'), np.dtype(mM+'8[2D]')),
                np.dtype(mM+'8[1D]'))
            assert_equal(
                np.promote_types(np.dtype(mM+'8[W]'), np.dtype(mM+'8[13s]')),
                np.dtype(mM+'8[s]'))
            assert_equal(
                np.promote_types(np.dtype(mM+'8[13W]'), np.dtype(mM+'8[49s]')),
                np.dtype(mM+'8[7s]'))
        # timedelta  timedelta raises when there is no reasonable gcd
        assert_raises(TypeError, np.promote_types,
                            np.dtype('m8[Y]'), np.dtype('m8[D]'))
        assert_raises(TypeError, np.promote_types,
                            np.dtype('m8[M]'), np.dtype('m8[W]'))
        # timedelta and float cannot be safely cast with each other
        assert_raises(TypeError, np.promote_types, "float32", "m8")
        assert_raises(TypeError, np.promote_types, "m8", "float32")
        assert_raises(TypeError, np.promote_types, "uint64", "m8")
        assert_raises(TypeError, np.promote_types, "m8", "uint64")

        # timedelta  timedelta may overflow with big unit ranges
        assert_raises(OverflowError, np.promote_types,
                            np.dtype('m8[W]'), np.dtype('m8[fs]'))
        assert_raises(OverflowError, np.promote_types,
                            np.dtype('m8[s]'), np.dtype('m8[as]'))

    def test_cast_overflow(self):
        # gh-4486
        def cast():
            numpy.datetime64("1971-01-01 00:00:00.000000000000000").astype("datetime64[%s]',
                                      'timedelta64[%s]'])
    def test_isfinite_isinf_isnan_units(self, unit, dstr):
        '''check isfinite, isinf, isnan for all units of M, m dtypes
        '''
        arr_val = [123, -321, "NaT"]
        arr = np.array(arr_val,  dtype= dstr % unit)
        pos = np.array([True, True,  False])
        neg = np.array([False, False,  True])
        false = np.array([False, False,  False])
        assert_equal(np.isfinite(arr), pos)
        assert_equal(np.isinf(arr), false)
        assert_equal(np.isnan(arr), neg)

    def test_assert_equal(self):
        assert_raises(AssertionError, assert_equal,
                np.datetime64('nat'), np.timedelta64('nat'))

    def test_corecursive_input(self):
        # construct a co-recursive list
        a, b = [], []
        a.append(b)
        b.append(a)
        obj_arr = np.array([None])
        obj_arr[0] = a

        # At some point this caused a stack overflow (gh-11154). Now raises
        # ValueError since the nested list cannot be converted to a datetime.
        assert_raises(ValueError, obj_arr.astype, 'M8')
        assert_raises(ValueError, obj_arr.astype, 'm8')

    @pytest.mark.parametrize("shape", [(), (1,)])
    def test_discovery_from_object_array(self, shape):
        arr = np.array("2020-10-10", dtype=object).reshape(shape)
        res = np.array("2020-10-10", dtype="M8").reshape(shape)
        assert res.dtype == np.dtype("M8[D]")
        assert_equal(arr.astype("M8"), res)
        arr[...] = np.bytes_("2020-10-10")  # try a numpy string type
        assert_equal(arr.astype("M8"), res)
        arr = arr.astype("S")
        assert_equal(arr.astype("S").astype("M8"), res)

    @pytest.mark.parametrize("time_unit", [
        "Y", "M", "W", "D", "h", "m", "s", "ms", "us", "ns", "ps", "fs", "as",
        # compound units
        "10D", "2M",
    ])
    def test_limit_symmetry(self, time_unit):
        """
        Dates should have symmetric limits around the unix epoch at +/-np.int64
        """
        epoch = np.datetime64(0, time_unit)
        latest = np.datetime64(np.iinfo(np.int64).max, time_unit)
        earliest = np.datetime64(-np.iinfo(np.int64).max, time_unit)

        # above should not have overflowed
        assert earliest < epoch < latest

    @pytest.mark.parametrize("time_unit", [
        "Y", "M",
        pytest.param("W", marks=pytest.mark.xfail(reason="gh-13197")),
        "D", "h", "m",
        "s", "ms", "us", "ns", "ps", "fs", "as",
        pytest.param("10D", marks=pytest.mark.xfail(reason="similar to gh-13197")),
    ])
    @pytest.mark.parametrize("sign", [-1, 1])
    def test_limit_str_roundtrip(self, time_unit, sign):
        """
        Limits should roundtrip when converted to strings.

        This tests the conversion to and from npy_datetimestruct.
        """
        # TODO: add absolute (gold standard) time span limit strings
        limit = np.datetime64(np.iinfo(np.int64).max * sign, time_unit)

        # Convert to string and back. Explicit unit needed since the day and
        # week reprs are not distinguishable.
        limit_via_str = np.datetime64(str(limit), time_unit)
        assert limit_via_str == limit


class TestDateTimeData:

    def test_basic(self):
        a = np.array(['1980-03-23'], dtype=np.datetime64)
        assert_equal(np.datetime_data(a.dtype), ('D', 1))

    def test_bytes(self):
        # byte units are converted to unicode
        dt = np.datetime64('2000', (b'ms', 5))
        assert np.datetime_data(dt.dtype) == ('ms', 5)

        dt = np.datetime64('2000', b'5ms')
        assert np.datetime_data(dt.dtype) == ('ms', 5)

    def test_non_ascii(self):
        # μs is normalized to μ
        dt = np.datetime64('2000', ('μs', 5))
        assert np.datetime_data(dt.dtype) == ('us', 5)

        dt = np.datetime64('2000', '5μs')
        assert np.datetime_data(dt.dtype) == ('us', 5)


def test_comparisons_return_not_implemented():
    # GH#17017

    class custom:
        __array_priority__ = 10000

    obj = custom()

    dt = np.datetime64('2000', 'ns')
    td = dt - dt

    for item in [dt, td]:
        assert item.__eq__(obj) is NotImplemented
        assert item.__ne__(obj) is NotImplemented
        assert item.__le__(obj) is NotImplemented
        assert item.__lt__(obj) is NotImplemented
        assert item.__ge__(obj) is NotImplemented
        assert item.__gt__(obj) is NotImplemented