#!/usr/bin/env qore
# -*- mode: qore; indent-tabs-mode: nil -*-

%new-style
%enable-all-warnings
%require-types
%strict-args

%requires ../../../../qlib/Util.qm
%requires ../../../../qlib/QUnit.qm

%exec-class DateTest

class DateTest inherits QUnit::Test {
    public {
        const TZ_File = "Europe_Vienna";
    }

    constructor() : QUnit::Test("DateTest", "1.0") {
        addTestCase("subtraction", \subtractionTests());
        addTestCase("DateTests", \dateTests());
        addTestCase("TimeZoneTests", \timeZoneTests());
        addTestCase("WindowsTimeZoneTests", \windowsTimeZoneTests());
        addTestCase("issue 2546", \issue2546());

        set_return_value(main());
    }

    subtractionTests() {
        assertEq(1s, 1970-01-01T00:00:01+01:00 - 1970-01-01+01:00);
        assertEq(1s, 1970-01-01T00:00:01Z - 1970-01-01Z);
    }

    dateTests() {
        # issue #3239: test invalid year masks
        assertThrows("INVALID-DATE", \date(), ("01-03-218", "DD-MM-YYYY"));
        assertThrows("INVALID-DATE", \date(), ("01-03-2", "DD-MM-YY"));
        assertEq(2020-03-01, date("01-03-2020", "DD-MM-YYYY"));
        assertEq(2020-03-01, date("01-03-20", "DD-MM-YY"));
        assertThrows("INVALID-DATE", "invalid digit", \date(), ("01-1-2020", "DD-MM-YYYY"));
        assertThrows("INVALID-DATE", "no more input", \date(), ("2020-01-1", "YYYY-MM-DD"));
        assertThrows("INVALID-DATE", "no more input", \date(), ("2020-01-1", "YYYY-DD-MM"));
        assertThrows("INVALID-DATE", "invalid digit", \date(), ("2020-01-1a", "YYYY-DD-MM"));
        assertThrows("INVALID-DATE", "invalid digit", \date(), ("1-03-20", "DD-MM-YY"));
        assertThrows("INVALID-DATE", "invalid digit", \date(), ("1a-03-20", "DD-MM-YY"));
        assertThrows("INVALID-DATE", "no more input", \date(), ("10:0", "HH:MM"));
        assertThrows("INVALID-DATE", "no more input", \date(), ("10:0", "MM:SS"));
        assertThrows("INVALID-DATE", "invalid digit", \date(), ("10:0a", "HH:MM"));
        assertThrows("INVALID-DATE", "invalid digit", \date(), ("10:0a", "MM:SS"));

        # here are the two formats for directly specifying date/time values:
        # ISO-8601 format (without timezone specification, currently qore does not support time zones)
        date date1  = 2004-02-01T12:30:00;
        # qore-specific date/time specification format ('-' instead of 'T' - more readable but non-standard)
        date ndate = 2004-03-02-12:30:00;
        testAssertionValue("format_date()", format_date("YYYY-MM-DD HH:mm:SS", date1), "2004-02-01 12:30:00");
        testAssertionValue("first date year subtraction", date1 - 5Y,                1999-02-01T12:30:00);
        testAssertionValue("first date month subtraction", date1 - 5M,                2003-09-01T12:30:00);
        testAssertionValue("first date day subtraction", date1 - 10D,               2004-01-22T12:30:00);
        testAssertionValue("first date hour subtraction", date1 - 2h,                2004-02-01T10:30:00);
        testAssertionValue("first date minute subtraction", date1 - 25m,               2004-02-01T12:05:00);
        testAssertionValue("first date second subtraction", date1 - 11s,               2004-02-01T12:29:49);
        testAssertionValue("first date millisecond subtraction", date1 - 251ms,             2004-02-01T12:29:59.749);

        testAssertionValue("first date year addition", date1 + 2Y,                2006-02-01T12:30:00);
        testAssertionValue("first date month addition", date1 + 5M,                2004-07-01T12:30:00);
        testAssertionValue("first date day addition", date1 + 10D,               2004-02-11T12:30:00);
        testAssertionValue("first date hour addition", date1 + 2h,                2004-02-01T14:30:00);
        testAssertionValue("first date minute addition", date1 + 25m,               2004-02-01T12:55:00);
        testAssertionValue("first date second addition", date1 + 11s,               2004-02-01T12:30:11);
        testAssertionValue("first date millisecond addition", date1 + 251ms,             2004-02-01T12:30:00.251);

        testAssertionValue("second date year subtraction", date1 - years(5),          1999-02-01-12:30:00);
        testAssertionValue("second date month subtraction", date1 - months(5),         2003-09-01-12:30:00);
        testAssertionValue("second date day subtraction", date1 - days(10),          2004-01-22-12:30:00);
        testAssertionValue("second date hour subtraction", date1 - hours(2),          2004-02-01-10:30:00);
        testAssertionValue("second date minute subtraction", date1 - minutes(25),       2004-02-01-12:05:00);
        testAssertionValue("second date second subtraction", date1 - seconds(11),       2004-02-01-12:29:49);
        testAssertionValue("second date millisecond subtraction", date1 - milliseconds(500), 2004-02-01-12:29:59.5);

        testAssertionValue("second date year addition", date1 + years(2),          2006-02-01-12:30:00);
        testAssertionValue("second date month addition", date1 + months(5),         2004-07-01-12:30:00);
        testAssertionValue("second date day addition", date1 + days(10),          2004-02-11-12:30:00);
        testAssertionValue("second date hour addition", date1 + hours(2),          2004-02-01-14:30:00);
        testAssertionValue("second date minute addition", date1 + minutes(25),       2004-02-01-12:55:00);
        testAssertionValue("second date second addition", date1 + seconds(11),       2004-02-01-12:30:11);
        testAssertionValue("second date millisecond addition", date1 + milliseconds(578), 2004-02-01-12:30:00.578);

        # testing ISO-8601 alternate period syntax (which is not very readable... :-( )
        # date periods
        testAssertionValue("third date year subtraction", date1 - P0001-00-00T00:00:00, 2003-02-01T12:30:00);
        testAssertionValue("third date month subtraction", date1 - P1M,          2004-01-01T12:30:00);
        testAssertionValue("third date day subtraction", date1 - P0000-00-01,          2004-01-31T12:30:00);
        testAssertionValue("third date year addition", date1 + P1Y,          2005-02-01T12:30:00);
        testAssertionValue("third date month addition", date1 + P0000-01-00,          2004-03-01T12:30:00);
        testAssertionValue("third date day addition", date1 + P0000-00-01,          2004-02-02T12:30:00);

        # time periods
        testAssertionValue("third date hour subtraction", date1 - P0000-00-00T01:00:00, 2004-02-01T11:30:00);
        testAssertionValue("third date minute subtraction", date1 - P00:01:00,            2004-02-01T12:29:00);
        testAssertionValue("third date second subtraction", date1 - PT00:00:01,           2004-02-01T12:29:59);
        testAssertionValue("third date hour addition", date1 + P01:00:00,            2004-02-01T13:30:00);
        testAssertionValue("third date minute addition", date1 + PT00:01:00,           2004-02-01T12:31:00);
        testAssertionValue("third date second addition", date1 + P00:00:01,            2004-02-01T12:30:01);

        # arithmetic on dates with ms overflow
        testAssertionValue("third millisecond addition", 2006-01-02T00:00:00.112, 2006-01-01T23:59:59.800 + 312ms);
        testAssertionValue("third millisecond subtraction", 2006-01-01T23:59:59.800, 2006-01-02T00:00:00.112 - 312ms);

        testAssertionValue("localtime() and mktime()", date1,        localtime(mktime(date1)));
        testAssertionValue("fourth date hour subtraction", date1 - PT1H, 2004-02-01T11:30:00);
        testAssertionValue("fourth date day addition", date1 + 30D,  ndate);
        testAssertionValue("fourth date day subtraction", ndate - 30D, date1);
        testAssertionValue("fourth date month addition", date1 + 23M,  2006-01-01T12:30:00);
        testAssertionValue("fourth date month subtraction", date1 - 4M,   2003-10-01T12:30:00);
        testAssertionValue("date function", date1,        date("20040201123000"));

        assertThrows("INVALID-DATE", \date(), ("2001-01", "YYYY-MM-DD"));
        assertThrows("INVALID-DATE", \date(), ("2001 Jan xx", "YYYY Mon DD"));
        assertEq(2001-01-01, date("2001-01", "YYYY-MM"));
        assertEq(2001-01-01, date("2001", "YYYY"));
        assertEq(2001-01-01T13:01, date("2001 JAN 01 13:01", "YYYY MON DD HH:mm"));

        # times without a date are assumed to be on Jan 1, 1970
        testAssertionValue("direct hour", 11:25:27, 1970-01-01T11:25:27.000);

        # test date conversion/calculation functions against known values
        int i = 1;
        testDate(1068-01-01,              1068, 1, 3, 1,    \i);
        testDate(1783-09-18,              1783, 38, 4, 261, \i);
        testDate(1864-02-29,              1864, 9,  1, 60,  \i);
        testDate(1864-03-01,              1864, 9,  2, 61,  \i);
        testDate(1968-01-01T11:01:20,     1968, 1,  1, 1,   \i);
        testDate(1968-02-29,              1968, 9,  4, 60,  \i);
        testDate(1968-03-01,              1968, 9,  5, 61,  \i);
        testDate(1969-12-31T23:59:59.999, 1970, 1,  3, 365, \i);
        testDate(1969-12-31T00:00:00.100, 1970, 1,  3, 365, \i);
        testDate(1969-01-01T17:25:31.380, 1969, 1,  3, 1,   \i); # 10

        testDate(1970-01-01,              1970, 1,  4, 1,   \i);
        testDate(1970-12-01T00:00:00,     1970, 49, 2, 335, \i);
        testDate(1972-01-01,              1971, 52, 6, 1,   \i);
        testDate(1972-12-30,              1972, 52, 6, 365, \i);
        testDate(1972-12-31,              1972, 52, 7, 366, \i);
        testDate(2004-02-28,              2004, 9,  6, 59,  \i);
        testDate(2004-02-29,              2004, 9,  7, 60,  \i);
        testDate(2004-03-01,              2004, 10, 1, 61,  \i);
        testDate(2004-03-28,              2004, 13, 7, 88,  \i);
        testDate(2006-01-01,              2005, 52, 7, 1,   \i); # 20

        testDate(2006-09-01,              2006, 35, 5, 244, \i);
        testDate(2006-12-01,              2006, 48, 5, 335, \i);
        testDate(2007-12-30,              2007, 52, 7, 364, \i);
        testDate(2007-12-31,              2008, 1,  1, 365, \i);
        testDate(2008-01-01,              2008, 1,  2, 1,   \i);
        testDate(2008-01-06,              2008, 1,  7, 6,   \i);
        testDate(2008-01-07,              2008, 2,  1, 7,   \i);
        testDate(2008-01-08,              2008, 2,  2, 8,   \i);
        testDate(2008-01-09,              2008, 2,  3, 9,   \i);
        testDate(2008-01-10,              2008, 2,  4, 10,  \i); # 30

        testDate(2008-12-28,              2008, 52, 7, 363, \i);
        testDate(2008-12-29,              2009, 1,  1, 364, \i);
        testDate(2008-12-30,              2009, 1,  2, 365, \i);
        testDate(2010-01-03,              2009, 53, 7, 3,   \i);
        testDate(2010-01-04,              2010, 1,  1, 4,   \i);
        testDate(2010-01-09,              2010, 1,  6, 9,   \i);
        testDate(2010-01-10,              2010, 1,  7, 10,  \i);
        testDate(2010-01-11,              2010, 2,  1, 11,  \i);
        testDate(2016-12-01,              2016, 48, 4, 336, \i);
        testDate(2026-08-22,              2026, 34, 6, 234, \i); # 40

        testDate(2036-04-30,              2036, 18, 3, 121, \i);
        testDate(2054-06-19,              2054, 25, 5, 170, \i);
        testDate(2400-12-01,              2400, 48, 5, 336, \i);
        testDate(2970-01-01,              2970, 1,  1, 1,   \i);
        testDate(9999-12-31,              9999, 52, 5, 365, \i);
        testDate(9999-12-31T23:59:59.999, 9999, 52, 5, 365, \i);

        testAssertionValue("date() format parsing test", date("2012-03-02", "YYYY-MM-DD"), 2012-03-02);

        # absolute date difference tests
        testAssertionValue("date difference 1", 2006-01-02T11:34:28.344 - 2006-01-01,              35h + 34m + 28s +344ms);
%ifndef Windows
        # this test fails on Windows due to different DST application for dates outside the UNIX epoch
        testAssertionValue("date difference 2", 2099-04-21T19:20:02.106 - 1804-03-04T20:45:19.956, 2587078h + 34m + 42s + 150ms);
%endif

        SingleValueIterator svi(2012-01-01);
        testAssertionValue("1st SingleValueIterator::next()", svi.next(), True);
        testAssertionValue("2nd SingleValueIterator::next()", svi.next(), False);
        testAssertionValue("3rd SingleValueIterator::next()", svi.next(), True);
        testAssertionValue("SingleValueIterator::getValue()", svi.getValue(), 2012-01-01);
        testAssertionValue("SingleValueIterator::valid()", svi.valid(), True);

        SingleValueIterator ni = svi.copy();
        testAssertionValue("SingleValueIterator::getValue() (copy)", ni.getValue(), 2012-01-01);
        testAssertionValue("SingleValueIterator::next() (copy)", ni.next(), False);
        testAssertionValue("SingleValueIterator::valid() (copy)", ni.valid(), False);

        # time components for dates before the epoch must be returned correctly
        date1 = 1969-12-31T20:15:25;
        assertEq(20, date1.hours());
        assertEq(15, date1.minutes());
        assertEq(25, date1.seconds());
        assertEq(0, date1.microseconds());

        date1 = 1969-12-31T20:15:25.123456;
        assertEq(20, date1.hours());
        assertEq(15, date1.minutes());
        assertEq(25, date1.seconds());
        assertEq(123456, date1.microseconds());

        date1 = 1947-05-10T21:59:12.002341;
        assertEq(21, date1.hours());
        assertEq(59, date1.minutes());
        assertEq(12, date1.seconds());
        assertEq(2341, date1.microseconds());

        date1 = 1947-05-10T23:59:12.002341;
        assertEq(23, date1.hours());
        assertEq(59, date1.minutes());
        assertEq(12, date1.seconds());
        assertEq(2341, date1.microseconds());

        assertEq(2016-12-01, date("20161201", "YYYYMMDD"));
        assertEq(2016-12-31, date("20161231", "YYYYMMDD"));
        assertEq(2016-12-01, date("01-12-2016", "DD-MM-YYYY"));
        assertEq(2016-12-31, date("31-12-2016", "DD-MM-YYYY"));

        assertEq(2016-01-01, date("20160101xxxx"));

        assertThrows("INVALID-DATE", \date(), ("20161301"));
        assertThrows("INVALID-DATE", \date(), ("20161232"));

        assertThrows("INVALID-DATE", \date(), ("20161301", "YYYYMMDD"));
        assertThrows("INVALID-DATE", \date(), ("20161232", "YYYYMMDD"));
        assertThrows("INVALID-DATE", \date(), ("01-13-2016", "DD-MM-YYYY"));
        assertThrows("INVALID-DATE", \date(), ("32-12-2016", "DD-MM-YYYY"));

        assertThrows("INVALID-DATE", \date(), ("2016"));
        assertThrows("INVALID-DATE", \date(), ("201601"));

        date1 = 2018-03-10T05:39:57Z;
        assertEq("2018-03-10 05:39:57+00:00", date1.format("YYYY-MM-DD HH:mm:SSZ"));

        # issue #2716 & #2721
        # test the "IF" format
        assertEq("P2Y1M3DT5H7M9S", P2Y1M3DT5H7M9S.format("IF"));
        assertEq("P2Y1M3DT5H7M9.002S", P2Y1M3DT5H7M9.002S.format("IF"));
        assertEq("2018-03-23T11:18:21+01:00", 2018-03-23T11:18:21+01:00.format("IF"));
        # test fractional date component support
        assertEq(P1.5Y, 1Y + 182D + 12h);
        assertEq(P1.5M, 1M + 15D);
        assertEq(P1.5D, 1D + 12h);
        assertEq(PT1.5H, 1h + 30m);
        assertEq(PT1.5M, 1m + 30s);
        assertEq(PT1.5S, 1.5s);
        assertEq(PT1.5S, 1s + 500ms);
        assertEq(2.1s, 2s + 100ms);
    }

    timeZoneTests() {
        testAssertionValue("UTC offset", (new TimeZone("Europe/Prague")).UTCOffset(), 3600);

        string fn = normalize_dir(get_script_dir() + DirSep + TZ_File);
        TimeZone z(fn);
        testAssertionValue("zone path", basename(z.region()), TZ_File);
        testAssertionValue("invalid band", z.date("1946-01-01").toString(), "19460101000000");

        Program p(PO_NEW_STYLE | PO_NO_FILESYSTEM);
        p.parse("TimeZone sub test() {return new TimeZone(\"" + fn + "\");}", "test");
        testAssertion("zone path neg", \p.callFunction(), "test", new TestResultExceptionType("ILLEGAL-FILESYSTEM-ACCESS"));

        # test for issue #259
        assertEq(7200, new TimeZone("Europe/Vienna").date("1980-07-01").info().utc_secs_east);

        # test for issue #584
        assertEq(-14400, new TimeZone("America/New_York").date("1980-07-01").info().utc_secs_east);

        # test for issue #3736
        {
            date now = Qore::now();
            string region = now.info().zone.region();
            assertEq(2, region.split("/").size());
            assertEq(now, Serializable::deserialize(Serializable::serialize(now)));
        }
    }

    windowsTimeZoneTests() {
        if (PlatformOS != "Windows")
            testSkip("skipping because the test is not run on Windows");

        assertEq("Europe/Vienna", new TimeZone("Europe/Vienna").region());
        assertEq("Europe/Belgrade", new TimeZone("Central Europe Standard Time").region());
        assertEq("Europe/Sarajevo", new TimeZone("Central European Standard Time").region());
        assertThrows("TZINFO-ERROR", sub () { TimeZone tz("ABCDEF"); });
    }

    testDate(date d, int y, int w, int day, int n, reference ri) {
        string str = sprintf("%04d-W%02d-%d", y, w, day);
        hash h = ( "year" : y, "week" : w, "day" : day );
        date d1;
        # subtract milliseconds from date to compare with timegm value
        if (int ms = get_milliseconds(d))
            d1 = d - milliseconds(ms);
        else
            d1 = d;

        testAssertionValue("get_iso_week_string() " + ri, get_iso_week_string(d), str);
        testAssertionValue("get_iso_week_hash() " + ri, get_iso_week_hash(d), h);
        testAssertionValue("get_iso_day_of_week() " + ri, get_iso_day_of_week(d), day);
        testAssertionValue("get_day_number() " + ri, get_day_number(d), n);
        testAssertionValue("get_date_from_iso_week() " + ri, get_date_from_iso_week(y, w, day), get_midnight(d));

        # not all architectures support the timegm() system call
        #if (d >= 1970-01-01 && d < 2038-01-19)
            #testAssertionValue("qore epoch conversion " + ri, timegm(d), int(d));
        ri++;
    }

    issue2546() {
        assertEq("365", 2017-12-31.format("Dn"));
        assertEq("365", 2017-12-31.format("DN"));
        assertEq("7", 2018-01-07.format("Dn"));
        assertEq("007", 2018-01-07.format("DN"));
        assertEq("25", 2018-01-25.format("Dn"));
        assertEq("025", 2018-01-25.format("DN"));

        assertEq("2018-W04-4", 2018-01-25.format("I"));
        assertEq("2016-W52-7", 2017-01-01.format("I"));
        assertEq("0833-W52-7", 0834-01-01.format("I"));

        assertEq("2018", 2018-01-25.format("Iy"));
        assertEq("2018", 2018-01-25.format("IY"));
        assertEq("2016", 2017-01-01.format("Iy"));
        assertEq("2016", 2017-01-01.format("IY"));
        assertEq("833", 0834-01-01.format("Iy"));
        assertEq("0833", 0834-01-01.format("IY"));

        assertEq("4", 2018-01-25.format("Iw"));
        assertEq("04", 2018-01-25.format("IW"));
        assertEq("52", 2017-01-01.format("Iw"));
        assertEq("52", 2017-01-01.format("IW"));
        assertEq("52", 0834-01-01.format("Iw"));
        assertEq("52", 0834-01-01.format("IW"));

        assertEq("4", 2018-01-25.format("Id"));
        assertEq("4", 2018-01-25.format("ID"));
        assertEq("7", 2017-01-01.format("Id"));
        assertEq("7", 2017-01-01.format("ID"));
        assertEq("7", 0834-01-01.format("Id"));
        assertEq("7", 0834-01-01.format("ID"));
    }
}
