# SPDX-FileCopyrightText: 2001-2021 Python Software Foundation.All rights reserved.
# SPDX-FileCopyrightText: 2000 BeOpen.com. All rights reserved.
# SPDX-FileCopyrightText: 1995-2001 Corporation for National Research Initiatives.
#                         All rights reserved.
# SPDX-FileCopyrightText: 1995-2001 Corporation for National Research Initiatives.
#                         All rights reserved.
# SPDX-FileCopyrightText: 1991-1995 Stichting Mathematisch Centrum. All rights reserved.
# SPDX-FileCopyrightText: 2021 Brent Rubell for Adafruit Industries
# SPDX-License-Identifier: Python-2.0
# Implements a subset of https://github.com/python/cpython/blob/master/Lib/test/datetimetester.py
# NOTE: This test is based off CPython and therefore linting is disabled within this file.
# pylint:disable=invalid-name, no-member, wrong-import-position, undefined-variable, no-self-use, cell-var-from-loop, misplaced-comparison-constant, too-many-public-methods, fixme, import-outside-toplevel, unused-argument, too-few-public-methods
import sys
import unittest

# CPython standard implementation
from datetime import date as cpython_date
from datetime import MINYEAR, MAXYEAR

# CircuitPython subset implementation
sys.path.append("..")
from adafruit_datetime import date as cpy_date

# An arbitrary collection of objects of non-datetime types, for testing
# mixed-type comparisons.
OTHERSTUFF = (10, 34.5, "abc", {}, [], ())


class TestDate(unittest.TestCase):
    def test_basic_attributes(self):
        dt = cpy_date(2002, 3, 1)
        dt_2 = cpython_date(2002, 3, 1)
        self.assertEqual(dt.year, dt_2.year)
        self.assertEqual(dt.month, dt_2.month)
        self.assertEqual(dt.day, dt_2.day)

    def test_bad_constructor_arguments(self):
        # bad years
        cpy_date(MINYEAR, 1, 1)  # no exception
        cpy_date(MAXYEAR, 1, 1)  # no exception
        self.assertRaises(ValueError, cpy_date, MINYEAR - 1, 1, 1)
        self.assertRaises(ValueError, cpy_date, MAXYEAR + 1, 1, 1)
        # bad months
        cpy_date(2000, 1, 1)  # no exception
        cpy_date(2000, 12, 1)  # no exception
        self.assertRaises(ValueError, cpy_date, 2000, 0, 1)
        self.assertRaises(ValueError, cpy_date, 2000, 13, 1)
        # bad days
        cpy_date(2000, 2, 29)  # no exception
        cpy_date(2004, 2, 29)  # no exception
        cpy_date(2400, 2, 29)  # no exception
        self.assertRaises(ValueError, cpy_date, 2000, 2, 30)
        self.assertRaises(ValueError, cpy_date, 2001, 2, 29)
        self.assertRaises(ValueError, cpy_date, 2100, 2, 29)
        self.assertRaises(ValueError, cpy_date, 1900, 2, 29)
        self.assertRaises(ValueError, cpy_date, 2000, 1, 0)
        self.assertRaises(ValueError, cpy_date, 2000, 1, 32)

    def test_hash_equality(self):
        d = cpy_date(2000, 12, 31)
        e = cpy_date(2000, 12, 31)
        self.assertEqual(d, e)
        self.assertEqual(hash(d), hash(e))

        dic = {d: 1}
        dic[e] = 2
        self.assertEqual(len(dic), 1)
        self.assertEqual(dic[d], 2)
        self.assertEqual(dic[e], 2)

        d = cpy_date(2001, 1, 1)
        e = cpy_date(2001, 1, 1)
        self.assertEqual(d, e)
        self.assertEqual(hash(d), hash(e))

        dic = {d: 1}
        dic[e] = 2
        self.assertEqual(len(dic), 1)
        self.assertEqual(dic[d], 2)
        self.assertEqual(dic[e], 2)

    def test_fromtimestamp(self):
        import time

        # Try an arbitrary fixed value.
        year, month, day = 1999, 9, 19
        ts = time.mktime((year, month, day, 0, 0, 0, 0, 0, -1))
        d = cpy_date.fromtimestamp(ts)
        self.assertEqual(d.year, year)
        self.assertEqual(d.month, month)
        self.assertEqual(d.day, day)

    def test_fromisoformat(self):
        # Try an arbitrary fixed value.
        iso_date_string = "1999-09-19"
        d = cpy_date.fromisoformat(iso_date_string)
        self.assertEqual(d.year, 1999)
        self.assertEqual(d.month, 9)
        self.assertEqual(d.day, 19)

    def test_fromisoformat_bad_formats(self):
        # Try an arbitrary fixed value.
        self.assertRaises(ValueError, cpy_date.fromisoformat, "99-09-19")
        self.assertRaises(ValueError, cpy_date.fromisoformat, "1999-13-19")

    # TODO: Test this when timedelta is added in
    @unittest.skip("Skip for CircuitPython - timedelta() not yet implemented.")
    def test_today(self):
        import time

        # We claim that today() is like fromtimestamp(time.time()), so
        # prove it.
        for dummy in range(3):
            today = cpy_date.today()
            ts = time.time()
            todayagain = cpy_date.fromtimestamp(ts)
            if today == todayagain:
                break
            # There are several legit reasons that could fail:
            # 1. It recently became midnight, between the today() and the
            #    time() calls.
            # 2. The platform time() has such fine resolution that we'll
            #    never get the same value twice.
            # 3. The platform time() has poor resolution, and we just
            #    happened to call today() right before a resolution quantum
            #    boundary.
            # 4. The system clock got fiddled between calls.
            # In any case, wait a little while and try again.
            time.sleep(0.1)

        # It worked or it didn't.  If it didn't, assume it's reason #2, and
        # let the test pass if they're within half a second of each other.
        self.assertTrue(
            today == todayagain or abs(todayagain - today) < timedelta(seconds=0.5)
        )

    def test_weekday(self):
        for i in range(7):
            # March 4, 2002 is a Monday
            self.assertEqual(
                cpy_date(2002, 3, 4 + i).weekday(),
                cpython_date(2002, 3, 4 + i).weekday(),
            )
            self.assertEqual(
                cpy_date(2002, 3, 4 + i).isoweekday(),
                cpython_date(2002, 3, 4 + i).isoweekday(),
            )
            # January 2, 1956 is a Monday
            self.assertEqual(
                cpy_date(1956, 1, 2 + i).weekday(),
                cpython_date(1956, 1, 2 + i).weekday(),
            )
            self.assertEqual(
                cpy_date(1956, 1, 2 + i).isoweekday(),
                cpython_date(1956, 1, 2 + i).isoweekday(),
            )

    @unittest.skip(
        "Skip for CircuitPython - isocalendar() not implemented for date objects."
    )
    def test_isocalendar(self):
        # Check examples from
        # http://www.phys.uu.nl/~vgent/calendar/isocalendar.htm
        for i in range(7):
            d = cpy_date(2003, 12, 22 + i)
            self.assertEqual(d.isocalendar(), (2003, 52, i + 1))
            d = cpy_date(2003, 12, 29) + timedelta(i)
            self.assertEqual(d.isocalendar(), (2004, 1, i + 1))
            d = cpy_date(2004, 1, 5 + i)
            self.assertEqual(d.isocalendar(), (2004, 2, i + 1))
            d = cpy_date(2009, 12, 21 + i)
            self.assertEqual(d.isocalendar(), (2009, 52, i + 1))
            d = cpy_date(2009, 12, 28) + timedelta(i)
            self.assertEqual(d.isocalendar(), (2009, 53, i + 1))
            d = cpy_date(2010, 1, 4 + i)
            self.assertEqual(d.isocalendar(), (2010, 1, i + 1))

    def test_isoformat(self):
        # test isoformat against expected and cpython equiv.
        t = cpy_date(2, 3, 2)
        t2 = cpython_date(2, 3, 2)
        self.assertEqual(t.isoformat(), "0002-03-02")
        self.assertEqual(t.isoformat(), t2.isoformat())

    @unittest.skip("Skip for CircuitPython - ctime() not implemented for date objects.")
    def test_ctime(self):
        t = cpy_date(2002, 3, 2)
        self.assertEqual(t.ctime(), "Sat Mar  2 00:00:00 2002")

    @unittest.skip(
        "Skip for CircuitPython - strftime() not implemented for date objects."
    )
    def test_strftime(self):
        t = cpy_date(2005, 3, 2)
        self.assertEqual(t.strftime("m:%m d:%d y:%y"), "m:03 d:02 y:05")
        self.assertEqual(t.strftime(""), "")  # SF bug #761337
        #        self.assertEqual(t.strftime('x'*1000), 'x'*1000) # SF bug #1556784

        self.assertRaises(TypeError, t.strftime)  # needs an arg
        self.assertRaises(TypeError, t.strftime, "one", "two")  # too many args
        self.assertRaises(TypeError, t.strftime, 42)  # arg wrong type

        # test that unicode input is allowed (issue 2782)
        self.assertEqual(t.strftime("%m"), "03")

        # A naive object replaces %z and %Z w/ empty strings.
        self.assertEqual(t.strftime("'%z' '%Z'"), "'' ''")

        # make sure that invalid format specifiers are handled correctly
        # self.assertRaises(ValueError, t.strftime, "%e")
        # self.assertRaises(ValueError, t.strftime, "%")
        # self.assertRaises(ValueError, t.strftime, "%#")

        # oh well, some systems just ignore those invalid ones.
        # at least, excercise them to make sure that no crashes
        # are generated
        for f in ["%e", "%", "%#"]:
            try:
                t.strftime(f)
            except ValueError:
                pass

        # check that this standard extension works
        t.strftime("%f")

    def test_format(self):
        dt = cpy_date(2007, 9, 10)
        self.assertEqual(dt.__format__(""), str(dt))

        # check that a derived class's __str__() gets called
        class A(cpy_date):
            def __str__(self):
                return "A"

        a = A(2007, 9, 10)
        self.assertEqual(a.__format__(""), "A")

        # check that a derived class's strftime gets called
        class B(cpy_date):
            def strftime(self, format_spec):
                return "B"

        b = B(2007, 9, 10)
        self.assertEqual(b.__format__(""), str(dt))

        # date strftime not implemented in CircuitPython, skip
        """for fmt in ["m:%m d:%d y:%y",
                    "m:%m d:%d y:%y H:%H M:%M S:%S",
                    "%z %Z",
                    ]:
            self.assertEqual(dt.__format__(fmt), dt.strftime(fmt))
            self.assertEqual(a.__format__(fmt), dt.strftime(fmt))
            self.assertEqual(b.__format__(fmt), 'B')"""

    @unittest.skip(
        "Skip for CircuitPython - min/max/resolution not implemented for date objects."
    )
    def test_resolution_info(self):
        # XXX: Should min and max respect subclassing?
        if issubclass(cpy_date, datetime):
            expected_class = datetime
        else:
            expected_class = date
        self.assertIsInstance(cpy_date.min, expected_class)
        self.assertIsInstance(cpy_date.max, expected_class)
        self.assertIsInstance(cpy_date.resolution, timedelta)
        self.assertTrue(cpy_date.max > cpy_date.min)

    # TODO: Needs timedelta
    @unittest.skip("Skip for CircuitPython - timedelta not implemented.")
    def test_extreme_timedelta(self):
        big = cpy_date.max - cpy_date.min
        # 3652058 days, 23 hours, 59 minutes, 59 seconds, 999999 microseconds
        n = (big.days * 24 * 3600 + big.seconds) * 1000000 + big.microseconds
        # n == 315537897599999999 ~= 2**58.13
        justasbig = timedelta(0, 0, n)
        self.assertEqual(big, justasbig)
        self.assertEqual(cpy_date.min + big, cpy_date.max)
        self.assertEqual(cpy_date.max - big, cpy_date.min)

    def test_timetuple(self):
        for i in range(7):
            # January 2, 1956 is a Monday (0)
            d = cpy_date(1956, 1, 2 + i)
            t = d.timetuple()
            d2 = cpython_date(1956, 1, 2 + i)
            t2 = d2.timetuple()
            self.assertEqual(t, t2)
            # February 1, 1956 is a Wednesday (2)
            d = cpy_date(1956, 2, 1 + i)
            t = d.timetuple()
            d2 = cpython_date(1956, 2, 1 + i)
            t2 = d2.timetuple()
            self.assertEqual(t, t2)
            # March 1, 1956 is a Thursday (3), and is the 31+29+1 = 61st day
            # of the year.
            d = cpy_date(1956, 3, 1 + i)
            t = d.timetuple()
            d2 = cpython_date(1956, 3, 1 + i)
            t2 = d2.timetuple()
            self.assertEqual(t, t2)
            self.assertEqual(t.tm_year, t2.tm_year)
            self.assertEqual(t.tm_mon, t2.tm_mon)
            self.assertEqual(t.tm_mday, t2.tm_mday)
            self.assertEqual(t.tm_hour, t2.tm_hour)
            self.assertEqual(t.tm_min, t2.tm_min)
            self.assertEqual(t.tm_sec, t2.tm_sec)
            self.assertEqual(t.tm_wday, t2.tm_wday)
            self.assertEqual(t.tm_yday, t2.tm_yday)
            self.assertEqual(t.tm_isdst, t2.tm_isdst)

    def test_compare(self):
        t1 = cpy_date(2, 3, 4)
        t2 = cpy_date(2, 3, 4)
        self.assertEqual(t1, t2)
        self.assertTrue(t1 <= t2)
        self.assertTrue(t1 >= t2)
        self.assertTrue(not t1 != t2)
        self.assertTrue(not t1 < t2)
        self.assertTrue(not t1 > t2)

        for args in (3, 3, 3), (2, 4, 4), (2, 3, 5):
            t2 = cpy_date(*args)  # this is larger than t1
            self.assertTrue(t1 < t2)
            self.assertTrue(t2 > t1)
            self.assertTrue(t1 <= t2)
            self.assertTrue(t2 >= t1)
            self.assertTrue(t1 != t2)
            self.assertTrue(t2 != t1)
            self.assertTrue(not t1 == t2)
            self.assertTrue(not t2 == t1)
            self.assertTrue(not t1 > t2)
            self.assertTrue(not t2 < t1)
            self.assertTrue(not t1 >= t2)
            self.assertTrue(not t2 <= t1)

        for badarg in OTHERSTUFF:
            self.assertEqual(t1 == badarg, False)
            self.assertEqual(t1 != badarg, True)
            self.assertEqual(badarg == t1, False)
            self.assertEqual(badarg != t1, True)

            self.assertRaises(TypeError, lambda: t1 < badarg)
            self.assertRaises(TypeError, lambda: t1 > badarg)
            self.assertRaises(TypeError, lambda: t1 >= badarg)
            self.assertRaises(TypeError, lambda: badarg <= t1)
            self.assertRaises(TypeError, lambda: badarg < t1)
            self.assertRaises(TypeError, lambda: badarg > t1)
            self.assertRaises(TypeError, lambda: badarg >= t1)

    def test_mixed_compare(self):
        our = cpy_date(2000, 4, 5)
        our2 = cpython_date(2000, 4, 5)

        # Our class can be compared for equality to other classes
        self.assertEqual(our == 1, our2 == 1)
        self.assertEqual(1 == our, 1 == our2)
        self.assertEqual(our != 1, our2 != 1)
        self.assertEqual(1 != our, 1 != our2)

        # But the ordering is undefined
        self.assertRaises(TypeError, lambda: our < 1)
        self.assertRaises(TypeError, lambda: 1 < our)

        # Repeat those tests with a different class

        class SomeClass:
            pass

        their = SomeClass()
        self.assertEqual(our == their, False)
        self.assertEqual(their == our, False)
        self.assertEqual(our != their, True)
        self.assertEqual(their != our, True)
        self.assertRaises(TypeError, lambda: our < their)
        self.assertRaises(TypeError, lambda: their < our)

        # However, if the other class explicitly defines ordering
        # relative to our class, it is allowed to do so

        class LargerThanAnything:
            def __lt__(self, other):
                return False

            def __le__(self, other):
                return isinstance(other, LargerThanAnything)

            def __eq__(self, other):
                return isinstance(other, LargerThanAnything)

            def __ne__(self, other):
                return not isinstance(other, LargerThanAnything)

            def __gt__(self, other):
                return not isinstance(other, LargerThanAnything)

            def __ge__(self, other):
                return True

        their = LargerThanAnything()
        self.assertEqual(our == their, False)
        self.assertEqual(their == our, False)
        self.assertEqual(our != their, True)
        self.assertEqual(their != our, True)
        self.assertEqual(our < their, True)
        self.assertEqual(their < our, False)

    @unittest.skip(
        "Skip for CircuitPython - min/max date attributes not implemented yet."
    )
    def test_bool(self):
        # All dates are considered true.
        self.assertTrue(cpy_date.min)
        self.assertTrue(cpy_date.max)

    @unittest.skip("Skip for CircuitPython - date strftime not implemented yet.")
    def test_strftime_y2k(self):
        for y in (1, 49, 70, 99, 100, 999, 1000, 1970):
            d = cpy_date(y, 1, 1)
            # Issue 13305:  For years < 1000, the value is not always
            # padded to 4 digits across platforms.  The C standard
            # assumes year >= 1900, so it does not specify the number
            # of digits.
            if d.strftime("%Y") != "%04d" % y:
                # Year 42 returns '42', not padded
                self.assertEqual(d.strftime("%Y"), "%d" % y)
                # '0042' is obtained anyway
                self.assertEqual(d.strftime("%4Y"), "%04d" % y)

    @unittest.skip("Skip for CircuitPython - date replace not implemented.")
    def test_replace(self):
        cls = cpy_date
        args = [1, 2, 3]
        base = cls(*args)
        self.assertEqual(base, base.replace())

        i = 0
        for name, newval in (("year", 2), ("month", 3), ("day", 4)):
            newargs = args[:]
            newargs[i] = newval
            expected = cls(*newargs)
            got = base.replace(**{name: newval})
            self.assertEqual(expected, got)
            i += 1

        # Out of bounds.
        base = cls(2000, 2, 29)
        self.assertRaises(ValueError, base.replace, year=2001)

    def test_subclass_date(self):
        class C(cpy_date):
            theAnswer = 42

            def __new__(cls, *args, **kws):
                temp = kws.copy()
                extra = temp.pop("extra")
                result = cpy_date.__new__(cls, *args, **temp)
                result.extra = extra
                return result

            def newmeth(self, start):
                return start + self.year + self.month

        args = 2003, 4, 14

        dt1 = cpy_date(*args)
        dt2 = C(*args, **{"extra": 7})

        self.assertEqual(dt2.__class__, C)
        self.assertEqual(dt2.theAnswer, 42)
        self.assertEqual(dt2.extra, 7)
        self.assertEqual(dt1.toordinal(), dt2.toordinal())
        self.assertEqual(dt2.newmeth(-7), dt1.year + dt1.month - 7)
