/*
 * Copyright 2004-2013 H2 Group. Multiple-Licensed under the H2 License,
 * Version 1.0, and under the Eclipse Public License, Version 1.0
 * (http://h2database.com/html/license.html).
 * Initial Developer: H2 Group
 */
package org.h2.test.unit;

import java.sql.Date;
import java.sql.SQLException;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.TimeZone;
import org.h2.constant.ErrorCode;
import org.h2.constant.SysProperties;
import org.h2.store.Data;
import org.h2.test.TestBase;
import org.h2.test.utils.AssertThrows;
import org.h2.util.DateTimeUtils;
import org.h2.util.New;
import org.h2.value.Value;
import org.h2.value.ValueDate;
import org.h2.value.ValueDouble;
import org.h2.value.ValueInt;
import org.h2.value.ValueNull;
import org.h2.value.ValueString;
import org.h2.value.ValueTime;
import org.h2.value.ValueTimestamp;

/**
 * Tests the date parsing. The problem is that some dates are not allowed
 * because of the summer time change. Most countries change at 2 o'clock in the
 * morning to 3 o'clock, but some (for example Chile) change at midnight.
 * Non-lenient parsing would not work in this case.
 */
public class TestDate extends TestBase {

    /**
     * Run just this test.
     *
     * @param a ignored
     */
    public static void main(String... a) throws Exception {
        // System.setProperty("h2.storeLocalTime", "true");
        TestBase.createCaller().init().test();
    }

    @Override
    public void test() throws SQLException {
        testValueDate();
        testValueTime();
        testValueTimestamp();
        testValueTimestampWithTimezone();
        testValidDate();
        testAbsoluteDay();
        testCalculateLocalMillis();
        testTimeOperationsAcrossTimeZones();
        testDateTimeUtils();
    }

    private void testValueDate() {
        assertEquals("2000-01-01", ValueDate.get(Date.valueOf("2000-01-01")).getString());
        assertEquals("0-00-00", ValueDate.fromDateValue(0).getString());
        assertEquals("9999-12-31", ValueDate.parse("9999-12-31").getString());
        assertEquals("-9999-12-31", ValueDate.parse("-9999-12-31").getString());
        assertEquals(
                Integer.MAX_VALUE + "-12-31",
                ValueDate.parse(Integer.MAX_VALUE + "-12-31").getString());
        assertEquals(
                Integer.MIN_VALUE + "-12-31",
                ValueDate.parse(Integer.MIN_VALUE + "-12-31").getString());
        ValueDate d1 = ValueDate.parse("2001-01-01");
        assertEquals("2001-01-01", d1.getDate().toString());
        assertEquals("DATE '2001-01-01'", d1.getSQL());
        assertEquals("DATE '2001-01-01'", d1.toString());
        assertEquals(Value.DATE, d1.getType());
        long dv = d1.getDateValue();
        assertEquals((int) ((dv >>> 32) ^ dv), d1.hashCode());
        assertEquals(d1.getString().length(), d1.getDisplaySize());
        assertEquals(ValueDate.PRECISION, d1.getPrecision());
        assertEquals("java.sql.Date", d1.getObject().getClass().getName());
        ValueDate d1b = ValueDate.parse("2001-01-01");
        assertTrue(d1 == d1b);
        Value.clearCache();
        d1b = ValueDate.parse("2001-01-01");
        assertFalse(d1 == d1b);
        assertTrue(d1.equals(d1));
        assertTrue(d1.equals(d1b));
        assertTrue(d1b.equals(d1));
        assertEquals(0, d1.compareTo(d1b, null));
        assertEquals(0, d1b.compareTo(d1, null));
        ValueDate d2 = ValueDate.parse("2002-02-02");
        assertFalse(d1.equals(d2));
        assertFalse(d2.equals(d1));
        assertEquals(-1, d1.compareTo(d2, null));
        assertEquals(1, d2.compareTo(d1, null));

        // can't convert using java.util.Date
        assertEquals(
                Integer.MAX_VALUE + "-12-31 00:00:00.0",
                ValueDate.parse(Integer.MAX_VALUE + "-12-31").
                convertTo(Value.TIMESTAMP).getString());
        assertEquals(
                Integer.MIN_VALUE + "-12-31 00:00:00.0",
                ValueDate.parse(Integer.MIN_VALUE + "-12-31").
                convertTo(Value.TIMESTAMP).getString());
        assertEquals(
                "00:00:00",
                ValueDate.parse(Integer.MAX_VALUE + "-12-31").
                convertTo(Value.TIME).getString());
        assertEquals(
                "00:00:00",
                ValueDate.parse(Integer.MIN_VALUE + "-12-31").
                convertTo(Value.TIME).getString());
    }

    private void testValueTime() {
        assertEquals("10:20:30", ValueTime.get(Time.valueOf("10:20:30")).getString());
        assertEquals("00:00:00", ValueTime.fromNanos(0).getString());
        assertEquals("23:59:59", ValueTime.parse("23:59:59").getString());
        assertEquals("99:59:59", ValueTime.parse("99:59:59").getString());
        assertEquals("-99:02:03.001002003", ValueTime.parse("-99:02:03.001002003").getString());
        assertEquals("-99:02:03.001002", ValueTime.parse("-99:02:03.001002000").getString());
        assertEquals("-99:02:03", ValueTime.parse("-99:02:03.0000000000001").getString());
        assertEquals("1999999:59:59.999999999",
                ValueTime.parse("1999999:59:59.999999999").getString());
        assertEquals("-1999999:59:59.999999999",
                ValueTime.parse("-1999999:59:59.999999999").getString());
        ValueTime t1 = ValueTime.parse("11:11:11");
        assertEquals("11:11:11", t1.getTime().toString());
        assertEquals("1970-01-01", t1.getDate().toString());
        assertEquals("TIME '11:11:11'", t1.getSQL());
        assertEquals("TIME '11:11:11'", t1.toString());
        assertEquals(1, t1.getSignum());
        assertEquals(-1, t1.negate().getSignum());
        assertEquals(0, t1.multiply(ValueInt.get(0)).getSignum());
        assertEquals(0, t1.subtract(t1).getSignum());
        assertEquals("05:35:35.5", t1.multiply(ValueDouble.get(0.5)).getString());
        assertEquals("22:22:22", t1.divide(ValueDouble.get(0.5)).getString());
        assertEquals("-11:11:11", t1.negate().getString());
        assertEquals("11:11:11", t1.negate().negate().getString());
        assertEquals(Value.TIME, t1.getType());
        long nanos = t1.getNanos();
        assertEquals((int) ((nanos >>> 32) ^ nanos), t1.hashCode());
        assertEquals(t1.getString().length(), t1.getDisplaySize());
        assertEquals(ValueTime.PRECISION, t1.getPrecision());
        assertEquals("java.sql.Time", t1.getObject().getClass().getName());
        ValueTime t1b = ValueTime.parse("11:11:11");
        assertTrue(t1 == t1b);
        Value.clearCache();
        t1b = ValueTime.parse("11:11:11");
        assertFalse(t1 == t1b);
        assertTrue(t1.equals(t1));
        assertTrue(t1.equals(t1b));
        assertTrue(t1b.equals(t1));
        assertEquals(0, t1.compareTo(t1b, null));
        assertEquals(0, t1b.compareTo(t1, null));
        ValueTime t2 = ValueTime.parse("22:22:22");
        assertFalse(t1.equals(t2));
        assertFalse(t2.equals(t1));
        assertEquals("33:33:33", t1.add(t2).getString());
        assertEquals("33:33:33", t1.multiply(ValueInt.get(4)).subtract(t1).getString());
        assertEquals(-1, t1.compareTo(t2, null));
        assertEquals(1, t2.compareTo(t1, null));

        // can't convert using java.util.Date
        assertEquals(
                "1969-12-31 23:00:00.0",
                ValueTime.parse("-1:00:00").
                convertTo(Value.TIMESTAMP).getString());
        assertEquals(
                "1970-01-01",
                ValueTime.parse("-1:00:00").
                convertTo(Value.DATE).getString());
    }

    private void testValueTimestampWithTimezone() {
        for (int m = 1; m <= 12; m++) {
            for (int d = 1; d <= 28; d++) {
                for (int h = 0; h <= 23; h++) {
                    String s = "2011-" + (m < 10 ? "0" : "") + m + "-" + (d < 10 ? "0" : "") + d + " " +
                        (h < 10 ? "0" : "") + h + ":00:00";
                    ValueTimestamp ts = ValueTimestamp.parse(s + "Z");
                    String s2 = ts.getString();
                    ValueTimestamp ts2 = ValueTimestamp.parse(s2);
                    assertEquals(ts.getString(), ts2.getString());
                }
            }
        }
    }

    private void testValueTimestamp() {
        assertEquals("2001-02-03 04:05:06.0", ValueTimestamp.get(
                Timestamp.valueOf("2001-02-03 04:05:06")).getString());
        assertEquals("2001-02-03 04:05:06.001002003", ValueTimestamp.get(
                Timestamp.valueOf("2001-02-03 04:05:06.001002003")).getString());
        assertEquals("0-00-00 00:00:00.0", ValueTimestamp.fromDateValueAndNanos(0, 0).getString());
        assertEquals("9999-12-31 23:59:59.0",
                ValueTimestamp.parse("9999-12-31 23:59:59").getString());

        assertEquals(
                Integer.MAX_VALUE + "-12-31 01:02:03.04050607",
                ValueTimestamp.parse(Integer.MAX_VALUE + "-12-31 01:02:03.0405060708").getString());
        assertEquals(
                Integer.MIN_VALUE + "-12-31 01:02:03.04050607",
                ValueTimestamp.parse(Integer.MIN_VALUE + "-12-31 01:02:03.0405060708").getString());

        ValueTimestamp t1 = ValueTimestamp.parse("2001-01-01 01:01:01.111");
        assertEquals("2001-01-01 01:01:01.111", t1.getTimestamp().toString());
        assertEquals("2001-01-01", t1.getDate().toString());
        assertEquals("01:01:01", t1.getTime().toString());
        assertEquals("TIMESTAMP '2001-01-01 01:01:01.111'", t1.getSQL());
        assertEquals("TIMESTAMP '2001-01-01 01:01:01.111'", t1.toString());
        assertEquals(Value.TIMESTAMP, t1.getType());
        long dateValue = t1.getDateValue();
        long nanos = t1.getNanos();
        assertEquals((int) ((dateValue >>> 32) ^ dateValue ^
                (nanos >>> 32) ^ nanos),
                t1.hashCode());
        assertEquals(t1.getString().length(), t1.getDisplaySize());
        assertEquals(ValueTimestamp.PRECISION, t1.getPrecision());
        assertEquals(10, t1.getScale());
        assertEquals("java.sql.Timestamp", t1.getObject().getClass().getName());
        ValueTimestamp t1b = ValueTimestamp.parse("2001-01-01 01:01:01.111");
        assertTrue(t1 == t1b);
        Value.clearCache();
        t1b = ValueTimestamp.parse("2001-01-01 01:01:01.111");
        assertFalse(t1 == t1b);
        assertTrue(t1.equals(t1));
        assertTrue(t1.equals(t1b));
        assertTrue(t1b.equals(t1));
        assertEquals(0, t1.compareTo(t1b, null));
        assertEquals(0, t1b.compareTo(t1, null));
        ValueTimestamp t2 = ValueTimestamp.parse("2002-02-02 02:02:02.222");
        assertFalse(t1.equals(t2));
        assertFalse(t2.equals(t1));
        assertEquals(-1, t1.compareTo(t2, null));
        assertEquals(1, t2.compareTo(t1, null));
        t1 = ValueTimestamp.parse("2001-01-01 01:01:01.123456789");
        assertEquals("2001-01-01 01:01:01.123456789", t1.getString());
        assertEquals("2001-01-01 01:01:01.123456789", t1.convertScale(true, 10).getString());
        assertEquals("2001-01-01 01:01:01.123456789", t1.convertScale(true, 9).getString());
        assertEquals("2001-01-01 01:01:01.12345679", t1.convertScale(true, 8).getString());
        assertEquals("2001-01-01 01:01:01.1234568", t1.convertScale(true, 7).getString());
        assertEquals("2001-01-01 01:01:01.123457", t1.convertScale(true, 6).getString());
        assertEquals("2001-01-01 01:01:01.12346", t1.convertScale(true, 5).getString());
        assertEquals("2001-01-01 01:01:01.1235", t1.convertScale(true, 4).getString());
        assertEquals("2001-01-01 01:01:01.123", t1.convertScale(true, 3).getString());
        assertEquals("2001-01-01 01:01:01.12", t1.convertScale(true, 2).getString());
        assertEquals("2001-01-01 01:01:01.1", t1.convertScale(true, 1).getString());
        assertEquals("2001-01-01 01:01:01.0", t1.convertScale(true, 0).getString());
        t1 = ValueTimestamp.parse("-2001-01-01 01:01:01.123456789");
        assertEquals("-2001-01-01 01:01:01.123457", t1.convertScale(true, 6).getString());
        // classes do not match
        assertFalse(ValueTimestamp.parse("2001-01-01").equals(ValueDate.parse("2001-01-01")));

        assertEquals("2001-01-01 01:01:01.0",
                ValueTimestamp.parse("2001-01-01").add(
                ValueTime.parse("01:01:01")).getString());
        assertEquals("2001-01-02 01:01:01.0",
                ValueTimestamp.parse("2001-01-01").add(
                ValueTime.parse("25:01:01")).getString());
        assertEquals("1010-10-10 00:00:00.0",
                ValueTimestamp.parse("1010-10-10 10:10:10").subtract(
                ValueTime.parse("10:10:10")).getString());
        assertEquals("1010-10-10 10:00:00.0",
                ValueTimestamp.parse("1010-10-11 10:10:10").subtract(
                ValueTime.parse("24:10:10")).getString());
        assertEquals("-2001-01-01 01:01:01.0",
                ValueTimestamp.parse("-2001-01-01").add(
                ValueTime.parse("01:01:01")).getString());
        assertEquals("-1010-10-10 00:00:00.0",
                ValueTimestamp.parse("-1010-10-10 10:10:10").subtract(
                ValueTime.parse("10:10:10")).getString());

        assertEquals(0, DateTimeUtils.absoluteDayFromDateValue(ValueTimestamp.parse("1970-01-01").getDateValue()));
        assertEquals(0, ValueTimestamp.parse("1970-01-01").getNanos());
        assertEquals(0, ValueTimestamp.parse("1970-01-01 00:00:00.000 UTC").getTimestamp().getTime());
        assertEquals(0, ValueTimestamp.parse("+1970-01-01T00:00:00.000Z").getTimestamp().getTime());
        assertEquals(0, ValueTimestamp.parse("1970-01-01T00:00:00.000+00:00").getTimestamp().getTime());
        assertEquals(0, ValueTimestamp.parse("1970-01-01T00:00:00.000-00:00").getTimestamp().getTime());
        new AssertThrows(ErrorCode.INVALID_DATETIME_CONSTANT_2) { @Override
        public void test() {
            ValueTimestamp.parse("1970-01-01 00:00:00.000 ABC");
        }};
        new AssertThrows(ErrorCode.INVALID_DATETIME_CONSTANT_2) { @Override
        public void test() {
            ValueTimestamp.parse("1970-01-01T00:00:00.000+ABC");
        }};
    }

    private void testAbsoluteDay() {
        long next = Long.MIN_VALUE;
        for (int y = -2000; y < 3000; y++) {
            for (int m = -3; m <= 14; m++) {
                for (int d = -2; d <= 35; d++) {
                    if (!DateTimeUtils.isValidDate(y, m, d)) {
                        continue;
                    }
                    long date = DateTimeUtils.dateValue(y, m, d);
                    long abs = DateTimeUtils.absoluteDayFromDateValue(date);
                    if (abs != next && next != Long.MIN_VALUE) {
                        assertEquals(abs, next);
                    }
                    next = abs + 1;
                    long d2 = DateTimeUtils.dateValueFromAbsoluteDay(abs);
                    assertEquals(date, d2);
                    assertEquals(y, DateTimeUtils.yearFromDateValue(date));
                    assertEquals(m, DateTimeUtils.monthFromDateValue(date));
                    assertEquals(d, DateTimeUtils.dayFromDateValue(date));
                }
            }
        }
    }

    private void testValidDate() {
        Calendar c = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
        c.setLenient(false);
        for (int y = -2000; y < 3000; y++) {
            for (int m = -3; m <= 14; m++) {
                for (int d = -2; d <= 35; d++) {
                    boolean valid = DateTimeUtils.isValidDate(y, m, d);
                    if (m < 1 || m > 12) {
                        assertFalse(valid);
                    } else if (d < 1 || d > 31) {
                        assertFalse(valid);
                    } else if (y != 1582 && d >= 1 && d <= 27) {
                        assertTrue(valid);
                    } else {
                        if (y <= 0) {
                            c.set(Calendar.ERA, GregorianCalendar.BC);
                            c.set(Calendar.YEAR, 1 - y);
                        } else {
                            c.set(Calendar.ERA, GregorianCalendar.AD);
                            c.set(Calendar.YEAR, y);
                        }
                        c.set(Calendar.MONTH, m - 1);
                        c.set(Calendar.DAY_OF_MONTH, d);
                        boolean expected = true;
                        try {
                            c.getTimeInMillis();
                        } catch (Exception e) {
                            expected = false;
                        }
                        if (expected != valid) {
                            fail(y + "-" + m + "-" + d + " expected: " + expected + " got: " + valid);
                        }
                    }
                }
            }
        }
    }

    private static void testCalculateLocalMillis() {
        TimeZone defaultTimeZone = TimeZone.getDefault();
        try {
            for (TimeZone tz : TestDate.getDistinctTimeZones()) {
                TimeZone.setDefault(tz);
                for (int y = 1900; y < 2039; y += 10) {
                    if (y == 1993) {
                        // timezone change in Kwajalein
                    } else if (y == 1995) {
                        // timezone change in Enderbury and Kiritimati
                    }
                    for (int m = 1; m <= 12; m++) {
                        if (m != 3 && m != 4 && m != 10 && m != 11) {
                            // only test daylight saving time transitions
                            continue;
                        }
                        for (int day = 1; day < 29; day++) {
                            testDate(y, m, day);
                        }
                    }
                }
            }
        } finally {
            TimeZone.setDefault(defaultTimeZone);
        }
    }

    private static void testDate(int y, int m, int day) {
        long millis = DateTimeUtils.getMillis(TimeZone.getDefault(), y, m, day, 0, 0, 0, 0);
        String st = new java.sql.Date(millis).toString();
        int y2 = Integer.parseInt(st.substring(0, 4));
        int m2 = Integer.parseInt(st.substring(5, 7));
        int d2 = Integer.parseInt(st.substring(8, 10));
        if (y != y2 || m != m2 || day != d2) {
            String s = y + "-" + (m < 10 ? "0" + m : m) + "-" + (day < 10 ? "0" + day : day);
            System.out.println(s + "<>" + st + " " + TimeZone.getDefault().getID());
        }
    }

    private void testTimeOperationsAcrossTimeZones() {
        if (!SysProperties.STORE_LOCAL_TIME) {
            return;
        }
        TimeZone defaultTimeZone = TimeZone.getDefault();
        ArrayList<TimeZone> distinct = TestDate.getDistinctTimeZones();
        Data d = Data.create(null, 10240);
        try {
            for (TimeZone tz : distinct) {
                TimeZone.setDefault(tz);
                DateTimeUtils.resetCalendar();
                d.reset();
                for (int m = 1; m <= 12; m++) {
                    for (int h = 0; h <= 23; h++) {
                        if (h == 0 || h == 2 || h == 3) {
                            // those hours may not exist for all days in all
                            // timezones because of daylight saving
                            continue;
                        }
                        String s = "2000-" + (m < 10 ? "0" + m : m) + "-01 " + (h < 10 ? "0" + h : h) + ":00:00.0";
                        d.writeValue(ValueString.get(s));
                        d.writeValue(ValueTimestamp.get(Timestamp.valueOf(s)));
                    }
                }
                d.writeValue(ValueNull.INSTANCE);
                d.reset();
                for (TimeZone target : distinct) {
                    if ("Pacific/Kiritimati".equals(target.getID())) {
                        // there is a problem with this time zone, but it seems
                        // unrelated to this database (possibly wrong timezone
                        // information?)
                        continue;
                    }
                    TimeZone.setDefault(target);
                    DateTimeUtils.resetCalendar();
                    while (true) {
                        Value v = d.readValue();
                        if (v == ValueNull.INSTANCE) {
                            break;
                        }
                        String a = v.getString();
                        String b = d.readValue().getString();
                        if (!a.equals(b)) {
                            assertEquals("source: " + tz.getID() + " target: " + target.getID(), a, b);
                        }
                    }
                }
            }
        } finally {
            TimeZone.setDefault(defaultTimeZone);
            DateTimeUtils.resetCalendar();
        }
    }

    /**
     * Get the list of timezones with distinct rules.
     *
     * @return the list
     */
    public static ArrayList<TimeZone> getDistinctTimeZones() {
        ArrayList<TimeZone> distinct = New.arrayList();
        for (String id : TimeZone.getAvailableIDs()) {
            TimeZone t = TimeZone.getTimeZone(id);
            for (TimeZone d : distinct) {
                if (t.hasSameRules(d)) {
                    t = null;
                    break;
                }
            }
            if (t != null) {
                distinct.add(t);
            }
        }
        return distinct;
    }

    private void testDateTimeUtils() {
        ValueTimestamp ts1 = ValueTimestamp.parse("-999-08-07 13:14:15.16");
        ValueTimestamp ts2 = ValueTimestamp.parse("19999-08-07 13:14:15.16");
        ValueTime t1 = (ValueTime) ts1.convertTo(Value.TIME);
        ValueTime t2 = (ValueTime) ts2.convertTo(Value.TIME);
        ValueDate d1 = (ValueDate) ts1.convertTo(Value.DATE);
        ValueDate d2 = (ValueDate) ts2.convertTo(Value.DATE);
        assertEquals("-999-08-07 13:14:15.16", ts1.getString());
        assertEquals("-999-08-07", d1.getString());
        assertEquals("13:14:15.16", t1.getString());
        assertEquals("19999-08-07 13:14:15.16", ts2.getString());
        assertEquals("19999-08-07", d2.getString());
        assertEquals("13:14:15.16", t2.getString());
        ValueTimestamp ts1a = DateTimeUtils.convertTimestamp(ts1.getTimestamp(), Calendar.getInstance());
        ValueTimestamp ts2a = DateTimeUtils.convertTimestamp(ts2.getTimestamp(), Calendar.getInstance());
        assertEquals("-999-08-07 13:14:15.16", ts1a.getString());
        assertEquals("19999-08-07 13:14:15.16", ts2a.getString());
    }

}
