##############################################################################
# Copyright 2009, Gerhard Weis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#  * Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
#  * Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#  * Neither the name of the authors nor the names of its contributors
#    may be used to endorse or promote products derived from this software
#    without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT
##############################################################################
'''
Test cases for the isoduration module.
'''
import unittest
import operator
from datetime import timedelta, date, datetime

from isodate import Duration, parse_duration, ISO8601Error
from isodate import D_DEFAULT, D_WEEK, D_ALT_EXT, duration_isoformat

# the following list contains tuples of ISO duration strings and the expected
# result from the parse_duration method. A result of None means an ISO8601Error
# is expected.
PARSE_TEST_CASES = {'P18Y9M4DT11H9M8S': (Duration(4, 8, 0, 0, 9, 11, 0, 9, 18),
                                         D_DEFAULT, None),
                    'P2W': (timedelta(weeks=2), D_WEEK, None),
                    'P3Y6M4DT12H30M5S': (Duration(4, 5, 0, 0, 30, 12, 0, 6, 3),
                                         D_DEFAULT, None),
                    'P23DT23H': (timedelta(hours=23, days=23),
                                 D_DEFAULT, None),
                    'P4Y': (Duration(years=4), D_DEFAULT, None),
                    'P1M': (Duration(months=1), D_DEFAULT, None),
                    'PT1M': (timedelta(minutes=1), D_DEFAULT, None),
                    'P0.5Y': (Duration(years=0.5), D_DEFAULT, None),
                    'PT36H': (timedelta(hours=36), D_DEFAULT, 'P1DT12H'),
                    'P1DT12H': (timedelta(days=1, hours=12), D_DEFAULT, None),
                    '+P11D': (timedelta(days=11), D_DEFAULT, 'P11D'),
                    '-P2W': (timedelta(weeks=-2), D_WEEK, None),
                    '-P2.2W': (timedelta(weeks=-2.2), D_DEFAULT,
                               '-P15DT9H36M'),
                    'P1DT2H3M4S': (timedelta(days=1, hours=2, minutes=3,
                                            seconds=4), D_DEFAULT, None),
                    'P1DT2H3M': (timedelta(days=1, hours=2, minutes=3),
                                 D_DEFAULT, None),
                    'P1DT2H': (timedelta(days=1, hours=2), D_DEFAULT, None),
                    'PT2H': (timedelta(hours=2), D_DEFAULT, None),
                    'PT2.3H': (timedelta(hours=2.3), D_DEFAULT, 'PT2H18M'),
                    'PT2H3M4S': (timedelta(hours=2, minutes=3, seconds=4),
                                 D_DEFAULT, None),
                    'PT3M4S': (timedelta(minutes=3, seconds=4), D_DEFAULT,
                               None),
                    'PT22S': (timedelta(seconds=22), D_DEFAULT, None),
                    'PT22.22S': (timedelta(seconds=22.22), 'PT%S.%fS',
                                 'PT22.220000S'),
                    '-P2Y': (Duration(years=-2), D_DEFAULT, None),
                    '-P3Y6M4DT12H30M5S': (Duration(-4, -5, 0, 0, -30, -12, 0,
                                                   -6, -3), D_DEFAULT, None),
                    '-P1DT2H3M4S': (timedelta(days=-1, hours=-2, minutes=-3,
                                              seconds=-4), D_DEFAULT, None),
                    # alternative format
                    'P0018-09-04T11:09:08': (Duration(4, 8, 0, 0, 9, 11, 0, 9,
                                                      18), D_ALT_EXT, None),
                    #'PT000022.22': timedelta(seconds=22.22),
                    }

#                       d1                    d2           '+', '-', '>'
# A list of test cases to test addition and subtraction between datetime and
# Duration objects.
# each tuple contains 2 duration strings, and a result string for addition and
# one for subtraction. The last value says, if the first duration is greater
# than the second.
MATH_TEST_CASES = (('P5Y7M1DT9H45M16.72S', 'PT27M24.68S',
                    'P5Y7M1DT10H12M41.4S', 'P5Y7M1DT9H17M52.04S', None),
                   ('PT28M12.73S', 'PT56M29.92S',
                    'PT1H24M42.65S', '-PT28M17.19S', False),
                   ('P3Y7M23DT5H25M0.33S', 'PT1H1.95S',
                    'P3Y7M23DT6H25M2.28S', 'P3Y7M23DT4H24M58.38S', None),
                   ('PT1H1.95S', 'P3Y7M23DT5H25M0.33S',
                    'P3Y7M23DT6H25M2.28S', '-P3Y7M23DT4H24M58.38S', None),
                   ('P1332DT55M0.33S', 'PT1H1.95S',
                    'P1332DT1H55M2.28S', 'P1331DT23H54M58.38S', True),
                   ('PT1H1.95S', 'P1332DT55M0.33S',
                    'P1332DT1H55M2.28S', '-P1331DT23H54M58.38S', False))

# A list of test cases to test addition and subtraction of date/datetime
# and Duration objects. They are tested against the results of an
# equal long timedelta duration.
DATE_TEST_CASES = ( (date(2008, 2, 29),
                     timedelta(days=10, hours=12, minutes=20),
                     Duration(days=10, hours=12, minutes=20)),
                    (date(2008, 1, 31),
                     timedelta(days=10, hours=12, minutes=20),
                     Duration(days=10, hours=12, minutes=20)),
                    (datetime(2008, 2, 29),
                     timedelta(days=10, hours=12, minutes=20),
                     Duration(days=10, hours=12, minutes=20)),
                    (datetime(2008, 1, 31),
                     timedelta(days=10, hours=12, minutes=20),
                     Duration(days=10, hours=12, minutes=20)),
                    (datetime(2008, 4, 21),
                     timedelta(days=10, hours=12, minutes=20),
                     Duration(days=10, hours=12, minutes=20)),
                    (datetime(2008, 5, 5),
                     timedelta(days=10, hours=12, minutes=20),
                     Duration(days=10, hours=12, minutes=20)),
                    (datetime(2000, 1, 1),
                     timedelta(hours=-33),
                     Duration(hours=-33)),
                    (datetime(2008, 5, 5),
                     Duration(years=1, months=1, days=10, hours=12,
                              minutes=20),
                     Duration(months=13, days=10, hours=12, minutes=20)),
                    (datetime(2000, 3, 30),
                     Duration(years=1, months=1, days=10, hours=12,
                              minutes=20),
                     Duration(months=13, days=10, hours=12, minutes=20)),
                     )

# A list of test cases of additon of date/datetime and Duration. The results
# are compared against a given expected result.
DATE_CALC_TEST_CASES = (
                    (date(2000, 2, 1),
                     Duration(years=1, months=1),
                     date(2001, 3, 1)),
                    (date(2000, 2, 29),
                     Duration(years=1, months=1),
                     date(2001, 3, 29)),
                    (date(2000, 2, 29),
                     Duration(years=1),
                     date(2001, 2, 28)),
                    (date(1996, 2, 29),
                     Duration(years=4),
                     date(2000, 2, 29)),
                    (date(2096, 2, 29),
                     Duration(years=4),
                     date(2100, 2, 28)),
                    (date(2000, 2, 1),
                     Duration(years=-1, months=-1),
                     date(1999, 1, 1)),
                    (date(2000, 2, 29),
                     Duration(years=-1, months=-1),
                     date(1999, 1, 29)),
                    (date(2000, 2, 1),
                     Duration(years=1, months=1, days=1),
                     date(2001, 3, 2)),
                    (date(2000, 2, 29),
                     Duration(years=1, months=1, days=1),
                     date(2001, 3, 30)),
                    (date(2000, 2, 29),
                     Duration(years=1, days=1),
                     date(2001, 3, 1)),
                    (date(1996, 2, 29),
                     Duration(years=4, days=1),
                     date(2000, 3, 1)),
                    (date(2096, 2, 29),
                     Duration(years=4, days=1),
                     date(2100, 3, 1)),
                    (date(2000, 2, 1),
                     Duration(years=-1, months=-1, days=-1),
                     date(1998, 12, 31)),
                    (date(2000, 2, 29),
                     Duration(years=-1, months=-1, days=-1),
                     date(1999, 1, 28)),
                    (date(2001, 4, 1),
                     Duration(years=-1, months=-1, days=-1),
                     date(2000, 2, 29)),
                    (date(2000, 4, 1),
                     Duration(years=-1, months=-1, days=-1),
                     date(1999, 2, 28)),
                    (Duration(years=1, months=2),
                     Duration(years=0, months=0, days=1),
                     Duration(years=1, months=2, days=1)),
                    (Duration(years=-1, months=-1, days=-1),
                     date(2000, 4, 1),
                     date(1999, 2, 28)),
                    (Duration(years=1, months=1, weeks=5),
                     date(2000, 1, 30),
                     date(2001, 4, 4)),
                    (parse_duration("P1Y1M5W"),
                     date(2000, 1, 30),
                     date(2001, 4, 4)),
                    (parse_duration("P0.5Y"),
                     date(2000, 1, 30),
                     None),
                    (Duration(years=1, months=1, hours=3),
                     datetime(2000, 1, 30, 12, 15, 00),
                     datetime(2001, 2, 28, 15, 15, 00)),
                    (parse_duration("P1Y1MT3H"),
                     datetime(2000, 1, 30, 12, 15, 00),
                     datetime(2001, 2, 28, 15, 15, 00)),
                    (Duration(years=1, months=2),
                     timedelta(days=1),
                     Duration(years=1, months=2, days=1)),
                    (timedelta(days=1),
                     Duration(years=1, months=2),
                     Duration(years=1, months=2, days=1)),
                    (datetime(2008, 1, 1, 0, 2),
                     Duration(months=1),
                     datetime(2008, 2, 1, 0, 2)),
                    (datetime.strptime("200802", "%Y%M"),
                     parse_duration("P1M"),
                     datetime(2008, 2, 1, 0, 2)),
                    (datetime(2008, 2, 1),
                     Duration(months=1),
                     datetime(2008, 3, 1)),
                    (datetime.strptime("200802", "%Y%m"),
                     parse_duration("P1M"),
                     datetime(2008, 3, 1)),
                    # (date(2000, 1, 1),
                    #  Duration(years=1.5),
                    #  date(2001, 6, 1)),
                    # (date(2000, 1, 1),
                    #  Duration(years=1, months=1.5),
                    #  date(2001, 2, 14)),
                    )


class DurationTest(unittest.TestCase):
    '''
    This class tests various other aspects of the isoduration module,
    which are not covered with the test cases listed above.
    '''

    def test_associative(self):
        '''
        Adding 2 durations to a date is not associative.
        '''
        days1 = Duration(days=1)
        months1 = Duration(months=1)
        start = date(2000, 3, 30)
        res1 = start + days1 + months1
        res2 = start + months1 + days1
        self.assertNotEqual(res1, res2)

    def test_typeerror(self):
        '''
        Test if TypError is raised with certain parameters.
        '''
        self.assertRaises(TypeError, parse_duration, date(2000, 1, 1))
        self.assertRaises(TypeError, operator.sub, Duration(years=1),
                          date(2000, 1, 1))
        self.assertRaises(TypeError, operator.sub, 'raise exc',
                          Duration(years=1))
        self.assertRaises(TypeError, operator.add,
                          Duration(years=1, months=1, weeks=5),
                          'raise exception')
        self.assertRaises(TypeError, operator.add, 'raise exception',
                          Duration(years=1, months=1, weeks=5))

    def test_parseerror(self):
        '''
        Test for unparseable duration string.
        '''
        self.assertRaises(ISO8601Error, parse_duration, 'T10:10:10')

    def test_repr(self):
        '''
        Test __repr__ and __str__ for Duration obqects.
        '''
        dur = Duration(10, 10, years=10, months=10)
        self.assertEqual('10 years, 10 months, 10 days, 0:00:10', str(dur))
        self.assertEqual('isodate.duration.Duration(10, 10, 0,'
                         ' years=10, months=10)', repr(dur))

    def test_neg(self):
        '''
        Test __neg__ for Duration objects.
        '''
        self.assertEqual(-Duration(0), Duration(0))
        self.assertEqual(-Duration(years=1, months=1),
                         Duration(years=-1, months=-1))
        self.assertEqual(-Duration(years=1, months=1), Duration(months=-13))
        self.assertNotEqual(-Duration(years=1), timedelta(days=-365))
        self.assertNotEqual(-timedelta(days=365), Duration(years=-1))
        # FIXME: this test fails in python 3... it seems like python3
        #        treats a == b the same b == a
        #self.assertNotEqual(-timedelta(days=10), -Duration(days=10))

    def test_format(self):
        '''
        Test various other strftime combinations.
        '''
        self.assertEqual(duration_isoformat(Duration(0)), 'P0D')
        self.assertEqual(duration_isoformat(-Duration(0)), 'P0D')
        self.assertEqual(duration_isoformat(Duration(seconds=10)), 'PT10S')
        self.assertEqual(duration_isoformat(Duration(years=-1, months=-1)),
                         '-P1Y1M')
        self.assertEqual(duration_isoformat(-Duration(years=1, months=1)),
                         '-P1Y1M')
        self.assertEqual(duration_isoformat(-Duration(years=-1, months=-1)),
                         'P1Y1M')
        self.assertEqual(duration_isoformat(-Duration(years=-1, months=-1)),
                         'P1Y1M')
        dur = Duration(years=3, months=7, days=23, hours=5, minutes=25,
                       milliseconds=330)
        self.assertEqual(duration_isoformat(dur), 'P3Y7M23DT5H25M0.33S')
        self.assertEqual(duration_isoformat(-dur), '-P3Y7M23DT5H25M0.33S')

    def test_equal(self):
        '''
        Test __eq__ and __ne__ methods.
        '''
        self.assertEqual(Duration(years=1, months=1),
                         Duration(years=1, months=1))
        self.assertEqual(Duration(years=1, months=1), Duration(months=13))
        self.assertNotEqual(Duration(years=1, months=2),
                            Duration(years=1, months=1))
        self.assertNotEqual(Duration(years=1, months=1), Duration(months=14))
        self.assertNotEqual(Duration(years=1), timedelta(days=365))
        self.assertFalse(Duration(years=1, months=1) !=
                         Duration(years=1, months=1))
        self.assertFalse(Duration(years=1, months=1) != Duration(months=13))
        self.assertTrue(Duration(years=1, months=2) !=
                        Duration(years=1, months=1))
        self.assertTrue(Duration(years=1, months=1) != Duration(months=14))
        self.assertTrue(Duration(years=1) != timedelta(days=365))
        self.assertEqual(Duration(days=1), timedelta(days=1))
        # FIXME: this test fails in python 3... it seems like python3
        #        treats a != b the same b != a
        #self.assertNotEqual(timedelta(days=1), Duration(days=1))

    def test_totimedelta(self):
        '''
        Test conversion form Duration to timedelta.
        '''
        dur = Duration(years=1, months=2, days=10)
        self.assertEqual(dur.totimedelta(datetime(1998, 2, 25)), timedelta(434))
        # leap year has one day more in february
        self.assertEqual(dur.totimedelta(datetime(2000, 2, 25)), timedelta(435))
        dur = Duration(months=2)
        # march is longer than february, but april is shorter than march (cause only one day difference compared to 2)
        self.assertEqual(dur.totimedelta(datetime(2000, 2, 25)), timedelta(60))
        self.assertEqual(dur.totimedelta(datetime(2001, 2, 25)), timedelta(59))
        self.assertEqual(dur.totimedelta(datetime(2001, 3, 25)), timedelta(61))


def create_parsetestcase(durationstring, expectation, format, altstr):
    """
    Create a TestCase class for a specific test.

    This allows having a separate TestCase for each test tuple from the
    PARSE_TEST_CASES list, so that a failed test won't stop other tests.
    """

    class TestParseDuration(unittest.TestCase):
        '''
        A test case template to parse an ISO duration string into a
        timedelta or Duration object.
        '''

        def test_parse(self):
            '''
            Parse an ISO duration string and compare it to the expected value.
            '''
            result = parse_duration(durationstring)
            self.assertEqual(result, expectation)

        def test_format(self):
            '''
            Take duration/timedelta object and create ISO string from it.
            This is the reverse test to test_parse.
            '''
            if altstr:
                self.assertEqual(duration_isoformat(expectation, format),
                                 altstr)
            else:
                # if durationstring == '-P2W':
                #     import pdb; pdb.set_trace()
                self.assertEqual(duration_isoformat(expectation, format),
                                 durationstring)

    return unittest.TestLoader().loadTestsFromTestCase(TestParseDuration)


def create_mathtestcase(dur1, dur2, resadd, ressub, resge):
    """
    Create a TestCase class for a specific test.

    This allows having a separate TestCase for each test tuple from the
    MATH_TEST_CASES list, so that a failed test won't stop other tests.
    """

    dur1 = parse_duration(dur1)
    dur2 = parse_duration(dur2)
    resadd = parse_duration(resadd)
    ressub = parse_duration(ressub)

    class TestMathDuration(unittest.TestCase):
        '''
        A test case template test addition, subtraction and >
        operators for Duration objects.
        '''

        def test_add(self):
            '''
            Test operator + (__add__, __radd__)
            '''
            self.assertEqual(dur1 + dur2, resadd)

        def test_sub(self):
            '''
            Test operator - (__sub__, __rsub__)
            '''
            self.assertEqual(dur1 - dur2, ressub)

        def test_ge(self):
            '''
            Test operator > and <
            '''
            def dogetest():
                ''' Test greater than.'''
                return dur1 > dur2

            def doletest():
                ''' Test less than.'''
                return dur1 < dur2
            if resge is None:
                self.assertRaises(TypeError, dogetest)
                self.assertRaises(TypeError, doletest)
            else:
                self.assertEqual(dogetest(), resge)
                self.assertEqual(doletest(), not resge)

    return unittest.TestLoader().loadTestsFromTestCase(TestMathDuration)


def create_datetestcase(start, tdelta, duration):
    """
    Create a TestCase class for a specific test.

    This allows having a separate TestCase for each test tuple from the
    DATE_TEST_CASES list, so that a failed test won't stop other tests.
    """

    class TestDateCalc(unittest.TestCase):
        '''
        A test case template test addition, subtraction
        operators for Duration objects.
        '''

        def test_add(self):
            '''
            Test operator +.
            '''
            self.assertEqual(start + tdelta, start + duration)

        def test_sub(self):
            '''
            Test operator -.
            '''
            self.assertEqual(start - tdelta, start - duration)

    return unittest.TestLoader().loadTestsFromTestCase(TestDateCalc)


def create_datecalctestcase(start, duration, expectation):
    """
    Create a TestCase class for a specific test.

    This allows having a separate TestCase for each test tuple from the
    DATE_CALC_TEST_CASES list, so that a failed test won't stop other tests.
    """

    class TestDateCalc(unittest.TestCase):
        '''
        A test case template test addition operators for Duration objects.
        '''

        def test_calc(self):
            '''
            Test operator +.
            '''
            if expectation is None:
                self.assertRaises(ValueError, operator.add, start, duration)
            else:
                self.assertEqual(start + duration, expectation)

    return unittest.TestLoader().loadTestsFromTestCase(TestDateCalc)


def test_suite():
    '''
    Return a test suite containing all test defined above.
    '''
    suite = unittest.TestSuite()
    for durationstring, (expectation, format, altstr) in list(PARSE_TEST_CASES.items()):
        suite.addTest(create_parsetestcase(durationstring, expectation,
                                           format, altstr))
    for testdata in MATH_TEST_CASES:
        suite.addTest(create_mathtestcase(*testdata))
    for testdata in DATE_TEST_CASES:
        suite.addTest(create_datetestcase(*testdata))
    for testdata in DATE_CALC_TEST_CASES:
        suite.addTest(create_datecalctestcase(*testdata))
    suite.addTest(unittest.TestLoader().loadTestsFromTestCase(DurationTest))
    return suite

# load_tests Protocol
def load_tests(loader, tests, pattern):
    return test_suite()

if __name__ == '__main__':
    unittest.main(defaultTest='test_suite')
