/*
 * Derby - Class org.apache.derbyTesting.functionTests.tests.jdbcapi.DaylightSavingTest
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific
 * language governing permissions and limitations under the License.
 */

package org.apache.derbyTesting.functionTests.tests.jdbcapi;

import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.Calendar;
import java.util.TimeZone;
import junit.framework.Test;
import org.apache.derbyTesting.junit.BaseJDBCTestCase;
import org.apache.derbyTesting.junit.JDBC;
import org.apache.derbyTesting.junit.TestConfiguration;
import org.apache.derbyTesting.junit.TimeZoneTestSetup;

/**
 * This class contains tests that verify the correct handling of
 * {@code java.sql.Date}, {@code java.sql.Time} and {@code java.sql.Timestamp}
 * across DST changes.
 */
public class DaylightSavingTest extends BaseJDBCTestCase {
    public DaylightSavingTest(String name) {
        super(name);
    }

    public static Test suite() {
        // Run the test in a fixed timezone so that we know exactly what time
        // DST is observed.
        return new TimeZoneTestSetup(
                TestConfiguration.defaultSuite(DaylightSavingTest.class),
                "America/Chicago");
    }

    /**
     * Regression test case for DERBY-4582. Timestamps that were converted
     * to GMT before they were stored in the database used to come out wrong
     * on the network client if the timestamp represented a time near the
     * switch to DST in the local timezone.
     */
    public void testConversionToGMTAroundDSTChange() throws SQLException {
        Statement s = createStatement();
        s.execute("CREATE TABLE DERBY4582(" +
                "ID INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, " +
                "TS TIMESTAMP, T TIME, D DATE, T2 TIME, D2 DATE, " +
                "TS_STR VARCHAR(100), T_STR VARCHAR(100), D_STR VARCHAR(100))");

        Calendar localCal = Calendar.getInstance();

        // Switch from CST to CDT in 2010 happened at 2010-03-14 02:00:00 CST,
        // or 2010-03-14 08:00:00 GMT, so create some times/dates around that
        // time.
        Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        cal.set(Calendar.YEAR, 2010);
        cal.set(Calendar.MONTH, Calendar.MARCH);
        cal.set(Calendar.DAY_OF_MONTH, 12);
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.MINUTE, 1);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);

        // Create times for each hour in 2010-03-12 -- 2010-03-15 (GMT).
        Timestamp[] timestamps = new Timestamp[24 * 4];
        Time[] times = new Time[timestamps.length];
        Date[] dates = new Date[timestamps.length];
        for (int i = 0; i < timestamps.length; i++) {
            long time = cal.getTimeInMillis();
            timestamps[i] = new Timestamp(time);
            times[i] = new Time(time);
            dates[i] = new Date(time);
            cal.setTimeInMillis(time + 3600000); // move one hour forward
        }

        // Store the GMT representations of the times.
        PreparedStatement insert = prepareStatement(
                "INSERT INTO DERBY4582 " +
                "(TS, T, D, T2, D2, TS_STR, T_STR, D_STR) " +
                "VALUES (?,?,?,?,?,?,?,?)");
        for (int i = 0; i < timestamps.length; i++) {
            Timestamp ts = timestamps[i];
            Time t = times[i];
            Date d = dates[i];

            // Set the TIMESTAMP/TIME/DATE values TS/T/D with their respective
            // setter methods.
            insert.setTimestamp(1, ts, cal);
            insert.setTime(2, t, cal);
            insert.setDate(3, d, cal);

            // Set the TIME/DATE values T2/D2 with setTimestamp() to verify
            // that this alternative code path also works.
            insert.setTimestamp(4, ts, cal);
            insert.setTimestamp(5, ts, cal);

            // Also insert the values into VARCHAR columns so that we can
            // check that they are converted correctly.
            insert.setTimestamp(6, ts, cal);
            insert.setTime(7, t, cal);
            insert.setDate(8, d, cal);

            insert.execute();
        }

        // Now see that we get the same values back.
        ResultSet rs = s.executeQuery("SELECT * FROM DERBY4582 ORDER BY ID");
        for (int i = 0; i < timestamps.length; i++) {
            assertTrue("found only " + i + " rows", rs.next());
            assertEquals("ID", i + 1, rs.getInt(1));
            assertEquals("TS", timestamps[i], rs.getTimestamp(2, cal));
            assertEquals("T", stripDate(times[i], cal), rs.getTime(3, cal));
            assertEquals("D", stripTime(dates[i], cal), rs.getDate(4, cal));
            // T2 and D2 should have the same values as T and D.
            assertEquals("T2", stripDate(times[i], cal), rs.getTime(5, cal));
            assertEquals("D2", stripTime(dates[i], cal), rs.getDate(6, cal));
            // The VARCHAR columns should have the same values as TS, T and D.
            assertEquals("TS_STR", timestamps[i], rs.getTimestamp(7, cal));
            assertEquals("T_STR", stripDate(times[i], cal), rs.getTime(8, cal));
            assertEquals("D_STR", stripTime(dates[i], cal), rs.getDate(9, cal));
        }
        JDBC.assertEmpty(rs);

        // Also check that we get the expected values when we get TIME or DATE
        // with getTimestamp(), or TIMESTAMP with getTime() or getDate()
        rs = s.executeQuery("SELECT ID,T,D,TS,TS FROM DERBY4582 ORDER BY ID");
        for (int i = 0; i < timestamps.length; i++) {
            assertTrue("found only " + i + " rows", rs.next());
            assertEquals("ID", i + 1, rs.getInt(1));
            assertEquals("TIME AS TIMESTAMP",
                    timeToTimestamp(stripDate(times[i], cal), cal),
                    rs.getTimestamp(2, cal));
            assertEquals("DATE AS TIMESTAMP",
                    dateToTimestamp(stripTime(dates[i], cal), cal),
                    rs.getTimestamp(3, cal));
            assertEquals("TIMESTAMP AS TIME",
                    stripDate(timestamps[i], cal),
                    rs.getTime(4, cal));
            assertEquals("TIMESTAMP AS DATE",
                    stripTime(timestamps[i], cal),
                    rs.getDate(5, cal));
        }
        JDBC.assertEmpty(rs);

        // Now verify that we can successfully get values set in with an
        // updatable result set. Note that updateTimestamp(), updateTime() and
        // updateDate() don't take a Calendar argument, so the updated values
        // will be stored in the local timezone. What we test here, is that
        // updateX(col, val) followed by getX(col, val, cal) performs the
        // correct translation from local calendar to GMT calendar.
        Statement updStmt = createStatement(
                ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE);
        rs = updStmt.executeQuery("SELECT TS, T, D FROM DERBY4582");
        for (int i = 0; i < timestamps.length; i++) {
            assertTrue("found only " + i + " rows", rs.next());
            Timestamp ts1 = timestamps[i];
            rs.updateTimestamp(1, ts1);
            assertEquals("TS (default calendar)", ts1, rs.getTimestamp(1));
            Timestamp ts2 = rs.getTimestamp(1, cal);
            cal.clear();
            cal.setTime(ts2);
            localCal.clear();
            localCal.setTime(ts1);
            assertEquals("TS.YEAR",
                    localCal.get(Calendar.YEAR), cal.get(Calendar.YEAR));
            assertEquals("TS.MONTH",
                    localCal.get(Calendar.MONTH), cal.get(Calendar.MONTH));
            assertEquals("TS.DATE",
                    localCal.get(Calendar.DAY_OF_MONTH),
                    cal.get(Calendar.DAY_OF_MONTH));
            assertEquals("TS.HOURS",
                    localCal.get(Calendar.HOUR_OF_DAY),
                    cal.get(Calendar.HOUR_OF_DAY));
            assertEquals("TS.MINUTES",
                    localCal.get(Calendar.MINUTE), cal.get(Calendar.MINUTE));
            assertEquals("TS.SECONDS",
                    localCal.get(Calendar.SECOND), cal.get(Calendar.SECOND));
            assertEquals("TS.NANOS",
                    ts1.getNanos(), ts2.getNanos());

            Time t1 = times[i];
            rs.updateTime(2, t1);
            assertEquals("T (default calendar)",
                    stripDate(t1, localCal), rs.getTime(2));
            Time t2 = rs.getTime(2, cal);
            cal.clear();
            cal.setTime(t2);
            localCal.clear();
            localCal.setTime(t1);
            assertEquals("T.HOURS",
                    localCal.get(Calendar.HOUR_OF_DAY),
                    cal.get(Calendar.HOUR_OF_DAY));
            assertEquals("T.MINUTES",
                    localCal.get(Calendar.MINUTE), cal.get(Calendar.MINUTE));
            assertEquals("T.SECONDS",
                    localCal.get(Calendar.SECOND), cal.get(Calendar.SECOND));

            Date d1 = dates[i];
            rs.updateDate(3, d1);
            assertEquals("D (default calendar)",
                    stripTime(d1, localCal), rs.getDate(3));
            Date d2 = rs.getDate(3, cal);
            cal.clear();
            cal.setTime(d2);
            localCal.clear();
            localCal.setTime(d1);
            assertEquals("D.YEAR",
                    localCal.get(Calendar.YEAR), cal.get(Calendar.YEAR));
            assertEquals("D.MONTH",
                    localCal.get(Calendar.MONTH), cal.get(Calendar.MONTH));
            assertEquals("D.DATE",
                    localCal.get(Calendar.DAY_OF_MONTH),
                    cal.get(Calendar.DAY_OF_MONTH));

            rs.updateRow();
        }
        JDBC.assertEmpty(rs);

        // Verify that the values touched by the updatable result set made it
        // into the database.
        rs = s.executeQuery("SELECT TS, T, D FROM DERBY4582 ORDER BY TS");
        for (int i = 0; i < timestamps.length; i++) {
            assertTrue("found only " + i + " rows", rs.next());
            assertEquals("TS", timestamps[i], rs.getTimestamp(1));
            assertEquals("T", stripDate(times[i], localCal), rs.getTime(2));
            assertEquals("D", stripTime(dates[i], localCal), rs.getDate(3));
        }
        JDBC.assertEmpty(rs);

    }

    /**
     * Strip away the date component from a {@code java.util.Date} and return
     * it as a {@code java.sql.Time}, so that it can be compared with a time
     * value returned by Derby. Derby will set the date component of the time
     * value to 1970-01-01, so let's do the same here.
     *
     * @param time the time value whose date component to strip away
     * @param cal the calendar used to store the time in the database originally
     * @return a time value that represents the same time of the day as
     * {@code time} in the calendar {@code cal}, but with the date component
     * normalized to 1970-01-01
     */
    private static Time stripDate(java.util.Date time, Calendar cal) {
        cal.clear();
        cal.setTime(time);
        cal.set(1970, Calendar.JANUARY, 1);
        return new Time(cal.getTimeInMillis());
    }

    /**
     * Strip away the time component from a {@code java.util.Date} and return
     * it as a {@code java.sql.Date}, so that it can be compared with a date
     * value returned by Derby. Derby will set the time component of the date
     * value to 00:00:00.0, so let's do the same here.
     *
     * @param date the date whose time component to strip away
     * @param cal the calendar used to store the date in the database originally
     * @return a date value that represents the same day as {@code date} in the
     * calendar {@code cal}, but with the time component normalized to
     * 00:00:00.0
     */
    private static Date stripTime(java.util.Date date, Calendar cal) {
        cal.clear();
        cal.setTime(date);
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);
        return new Date(cal.getTimeInMillis());
    }

    /**
     * Convert a time value to a timestamp. The date component of the timestamp
     * should be set to the current date in the specified calendar, see
     * DERBY-889 and DERBY-1811.
     *
     * @param time the time value to convert
     * @param cal the calendar in which the conversion should be performed
     * @return a timestamp
     */
    private static Timestamp timeToTimestamp(Time time, Calendar cal) {
        // Get the current date in the specified calendar.
        cal.clear();
        cal.setTimeInMillis(System.currentTimeMillis());
        int year = cal.get(Calendar.YEAR);
        int month = cal.get(Calendar.MONTH);
        int day = cal.get(Calendar.DAY_OF_MONTH);

        // Construct a timestamp based on the current date and the specified
        // time value.
        cal.clear();
        cal.setTime(time);
        cal.set(year, month, day);

        return new Timestamp(cal.getTimeInMillis());
    }

    /**
     * Convert a date value to a timestamp. The time component of the timestamp
     * will be set to 00:00:00.0.
     *
     * @param date the date value to convert
     * @param cal the calendar in which the conversion should be performed
     * @return a timestamp
     */
    private static Timestamp dateToTimestamp(Date date, Calendar cal) {
        cal.clear();
        cal.setTime(date);
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);
        return new Timestamp(cal.getTimeInMillis());
    }
}
