/*
 * Firebird Open Source JavaEE Connector - JDBC Driver
 *
 * Distributable under LGPL license.
 * You may obtain a copy of the License at http://www.gnu.org/copyleft/lgpl.html
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * LGPL License for more details.
 *
 * This file was created by members of the firebird development team.
 * All individual contributions remain the Copyright (C) of those
 * individuals.  Contributors to this file are either listed here or
 * can be obtained from a source control history command.
 *
 * All rights reserved.
 */
package org.firebirdsql.jdbc.field;

import org.firebirdsql.gds.ISCConstants;
import org.firebirdsql.gds.ng.tz.TimeZoneDatatypeCoder;
import org.junit.Before;
import org.junit.Test;

import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.Types;
import java.time.*;
import java.util.Calendar;

import static org.firebirdsql.util.ByteArrayHelper.fromHexString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;

public class FBTimestampTzFieldTest extends BaseJUnit4TestFBField<FBTimestampTzField, OffsetDateTime> {

    private static final String TIMESTAMPTZ = "2019-03-09T07:45:51+01:00";
    private static final OffsetDateTime TIMESTAMPTZ_OFFSETDATETIME = OffsetDateTime.parse(TIMESTAMPTZ);
    // Defined using offset
    private static final String TIMESTAMPTZ_OFFSET_NETWORK_HEX = "0000E4B70E83AAF0000005DB";

    @Before
    @Override
    public void setUp() throws Exception {
        super.setUp();
        
        rowDescriptorBuilder.setType(ISCConstants.SQL_TIMESTAMP_TZ);
        fieldDescriptor = rowDescriptorBuilder.toFieldDescriptor();
        field = new FBTimestampTzField(fieldDescriptor, fieldData, Types.TIMESTAMP_WITH_TIMEZONE);
    }

    @Test
    @Override
    public void getObjectNonNull() throws SQLException {
        toReturnNonNullOffsetDateTime();

        assertEquals("Unexpected value for getObject()", getNonNullObject(), field.getObject());
    }

    @Test
    @Override
    public void setObjectNonNull() throws SQLException {
        setNonNullOffsetDateTimeExpectations();

        field.setObject(getNonNullObject());
    }

    @Test
    public void getObject_OffsetDateTime() throws SQLException {
        toReturnNonNullOffsetDateTime();

        assertEquals("Unexpected value for getObject(OffsetDateTime.class)",
                getNonNullObject(), field.getObject(OffsetDateTime.class));
    }

    @Test
    public void getObjectNull_OffsetDateTime() throws SQLException {
        toReturnNullExpectations();

        assertNull("Unexpected value for getObject(OffsetDateTime.class)", field.getObject(OffsetDateTime.class));
    }

    @Test
    public void getObject_OffsetTime() throws SQLException {
        toReturnNonNullOffsetDateTime();

        assertEquals("Unexpected value for getObject(OffsetTime.class)",
                getNonNullObject().toOffsetTime(), field.getObject(OffsetTime.class));
    }

    @Test
    public void getObjectNull_OffsetTime() throws SQLException {
        toReturnNullExpectations();

        assertNull("Unexpected value for getObject(OffsetTime.class)", field.getObject(OffsetTime.class));
    }

    @Test
    public void setObject_OffsetTime() throws SQLException {
        // note: offset time applies current date
        OffsetTime offsetTime = getNonNullObject().toOffsetTime();
        setOffsetTimeExpectations(offsetTime);

        field.setObject(offsetTime);
    }

    @Test
    @Override
    public void getObject_String() throws SQLException {
        toReturnNonNullOffsetDateTime();

        assertEquals("Unexpected value for getObject(String.class)", TIMESTAMPTZ, field.getObject(String.class));
    }

    @Test
    public void setObject_String() throws SQLException {
        setNonNullOffsetDateTimeExpectations();

        field.setString(TIMESTAMPTZ);
    }

    @Test
    @Override
    public void getTimeNonNull() throws SQLException {
        toReturnNonNullOffsetDateTime();

        long expectedMillis = TIMESTAMPTZ_OFFSETDATETIME.toInstant().toEpochMilli();
        
        assertEquals("Unexpected value for getTime()", new java.sql.Time(expectedMillis), field.getTime());
    }

    @Test
    @Override
    public void getTimeCalendarNonNull() throws SQLException {
        toReturnNonNullOffsetDateTime();

        Calendar calendar = Calendar.getInstance(getOneHourBehindTimeZone());
        long expectedMillis = TIMESTAMPTZ_OFFSETDATETIME.toInstant().toEpochMilli();

        assertEquals("Unexpected value for getTime(Calendar)", new java.sql.Time(expectedMillis),
                field.getTime(calendar));
    }

    @Test
    @Override
    public void getObject_java_sql_Time() throws SQLException {
        toReturnNonNullOffsetDateTime();

        long expectedMillis = TIMESTAMPTZ_OFFSETDATETIME.toInstant().toEpochMilli();

        assertEquals("Unexpected value for getObject(java.sql.Time.class)", new java.sql.Time(expectedMillis),
                field.getObject(java.sql.Time.class));
    }

    @Test
    @Override
    public void setTimeNonNull() throws SQLException {
        OffsetDateTime expectedTime = ZonedDateTime
                .of(LocalDate.now(), LocalTime.parse("16:12:01"), ZoneId.systemDefault())
                .toOffsetDateTime();
        setOffsetDateTimeExpectations(expectedTime);

        field.setTime(java.sql.Time.valueOf("16:12:01"));
    }

    @Test
    @Override
    public void setTimeCalendarNonNull() throws SQLException {
        OffsetDateTime expectedTime = ZonedDateTime
                .of(LocalDate.now(), LocalTime.parse("16:12:01"), ZoneId.systemDefault())
                .toOffsetDateTime();
        setOffsetDateTimeExpectations(expectedTime);

        Calendar calendar = Calendar.getInstance(getOneHourBehindTimeZone());

        field.setTime(java.sql.Time.valueOf("16:12:01"), calendar);
    }

    @Test
    public void setObject_java_sql_Time() throws SQLException {
        OffsetDateTime expectedTime = ZonedDateTime
                .of(LocalDate.now(), LocalTime.parse("16:12:01"), ZoneId.systemDefault())
                .toOffsetDateTime();
        setOffsetDateTimeExpectations(expectedTime);

        field.setObject(java.sql.Time.valueOf("16:12:01"));
    }

    @Test
    @Override
    public void getTimestampNonNull() throws SQLException {
        toReturnNonNullOffsetDateTime();

        long expectedMillis = TIMESTAMPTZ_OFFSETDATETIME.toInstant().toEpochMilli();

        assertEquals("Unexpected value for getTimestamp()", new java.sql.Timestamp(expectedMillis),
                field.getTimestamp());
    }

    @Test
    @Override
    public void getTimestampCalendarNonNull() throws SQLException {
        toReturnNonNullOffsetDateTime();

        Calendar calendar = Calendar.getInstance(getOneHourBehindTimeZone());
        long expectedMillis = TIMESTAMPTZ_OFFSETDATETIME.toInstant().toEpochMilli();

        assertEquals("Unexpected value for getTimestamp(Calendar)", new java.sql.Timestamp(expectedMillis),
                field.getTimestamp(calendar));
    }

    @Test
    @Override
    public void getObject_java_sql_Timestamp() throws SQLException {
        toReturnNonNullOffsetDateTime();

        long expectedMillis = TIMESTAMPTZ_OFFSETDATETIME.toInstant().toEpochMilli();

        assertEquals("Unexpected value for getObject(java.sql.Timestamp.class)", new java.sql.Timestamp(expectedMillis),
                field.getObject(java.sql.Timestamp.class));
    }

    @Test
    @Override
    public void getObject_java_util_Date() throws SQLException {
        toReturnNonNullOffsetDateTime();

        long expectedMillis = TIMESTAMPTZ_OFFSETDATETIME.toInstant().toEpochMilli();

        // NOTE: This actually returns a java.sql.Timestamp
        assertEquals("Unexpected value for getObject(java.util.Date.class)", new java.sql.Timestamp(expectedMillis),
                field.getObject(java.util.Date.class));
    }

    @Test
    @Override
    public void getObject_Calendar() throws SQLException {
        toReturnNonNullOffsetDateTime();

        long expectedMillis = TIMESTAMPTZ_OFFSETDATETIME.toInstant().toEpochMilli();
        Calendar expectedCalendar = Calendar.getInstance();
        expectedCalendar.setTimeInMillis(expectedMillis);

        assertEquals("Unexpected value for getObject(java.util.Calendar)", expectedCalendar,
                field.getObject(java.util.Calendar.class));
    }

    @Test
    @Override
    public void setTimestampNonNull() throws SQLException {
        OffsetDateTime expectedTime = LocalDateTime.parse("2019-03-09T07:45:51.1234")
                .atZone(ZoneId.systemDefault())
                .toOffsetDateTime();
        setOffsetDateTimeExpectations(expectedTime);

        field.setTimestamp(Timestamp.valueOf("2019-03-09 07:45:51.1234"));
    }

    @Test
    @Override
    public void setTimestampCalendarNonNull() throws SQLException {
        OffsetDateTime expectedTime = LocalDateTime.parse("2019-03-09T07:45:51.1234")
                .atZone(ZoneId.systemDefault())
                .toOffsetDateTime();
        setOffsetDateTimeExpectations(expectedTime);

        Calendar calendar = Calendar.getInstance(getOneHourBehindTimeZone());

        field.setTimestamp(Timestamp.valueOf("2019-03-09 07:45:51.1234"), calendar);
    }

    @Test
    public void setObject_java_sql_Timestamp() throws SQLException {
        OffsetDateTime expectedTime = LocalDateTime.parse("2019-03-09T07:45:51.1234")
                .atZone(ZoneId.systemDefault())
                .toOffsetDateTime();
        setOffsetDateTimeExpectations(expectedTime);

        field.setObject(Timestamp.valueOf("2019-03-09 07:45:51.1234"));
    }

    @Test
    @Override
    public void getDateNonNull() throws SQLException {
        toReturnNonNullOffsetDateTime();

        long expectedMillis = TIMESTAMPTZ_OFFSETDATETIME.toInstant().toEpochMilli();

        assertEquals("Unexpected value for getDate()", new java.sql.Date(expectedMillis), field.getDate());
    }

    @Test
    @Override
    public void getDateCalendarNonNull() throws SQLException {
        toReturnNonNullOffsetDateTime();

        Calendar calendar = Calendar.getInstance(getOneHourBehindTimeZone());
        long expectedMillis = TIMESTAMPTZ_OFFSETDATETIME.toInstant().toEpochMilli();

        assertEquals("Unexpected value for getDate(Calendar)", new java.sql.Date(expectedMillis),
                field.getDate(calendar));
    }

    @Test
    @Override
    public void getObject_java_sql_Date() throws SQLException {
        toReturnNonNullOffsetDateTime();

        long expectedMillis = TIMESTAMPTZ_OFFSETDATETIME.toInstant().toEpochMilli();

        assertEquals("Unexpected value for getObject(java.sql.Date.class)", new java.sql.Date(expectedMillis),
                field.getObject(java.sql.Date.class));
    }

    @Test
    @Override
    public void setDateNonNull() throws SQLException {
        OffsetDateTime expectedTime = LocalDateTime.parse("2019-03-09T00:00:00")
                .atZone(ZoneId.systemDefault())
                .toOffsetDateTime();
        setOffsetDateTimeExpectations(expectedTime);

        field.setDate(java.sql.Date.valueOf("2019-03-09"));
    }

    @Test
    @Override
    public void setDateCalendarNonNull() throws SQLException {
        OffsetDateTime expectedTime = LocalDateTime.parse("2019-03-09T00:00:00")
                .atZone(ZoneId.systemDefault())
                .toOffsetDateTime();
        setOffsetDateTimeExpectations(expectedTime);

        Calendar calendar = Calendar.getInstance(getOneHourBehindTimeZone());

        field.setDate(java.sql.Date.valueOf("2019-03-09"), calendar);
    }

    @Test
    @Override
    public void getStringNonNull() throws SQLException {
        toReturnNonNullOffsetDateTime();

        assertEquals("Unexpected value for getString()", TIMESTAMPTZ, field.getString());
    }

    @Test
    @Override
    public void setStringNonNull() throws SQLException {
        setNonNullOffsetDateTimeExpectations();

        field.setString(TIMESTAMPTZ);
    }

    @Test
    public void getStringNull() throws SQLException {
        toReturnNullExpectations();

        assertNull("Unexpected value for getString()", field.getString());
    }

    @Test
    public void setStringNull() throws SQLException {
        setNullExpectations();

        field.setString(null);
    }

    @Test
    public void setString_acceptsOffsetTimeString() throws SQLException {
        String offsetTimeString = "07:45:51+01:00";
        OffsetTime offsetTime = OffsetTime.parse(offsetTimeString);
        setOffsetTimeExpectations(offsetTime);

        field.setString(offsetTimeString);
    }

    @Test
    public void setString_illegalFormat_throwsTypeConversionException() throws SQLException {
        expectedException.expect(TypeConversionException.class);

        field.setString("GARBAGE");
    }

    @Test
    public void setString_illegalFormatWithT_throwsTypeConversionException() throws SQLException {
        // Presence of T in string is used to determine flow used for parsing; implementation artifact
        expectedException.expect(TypeConversionException.class);

        field.setString("GARBAGE WITH T");
    }

    @Override
    protected OffsetDateTime getNonNullObject() {
        return TIMESTAMPTZ_OFFSETDATETIME;
    }

    private void toReturnNonNullOffsetDateTime() {
        toReturnValueExpectations(fromHexString(TIMESTAMPTZ_OFFSET_NETWORK_HEX));
    }

    private void setNonNullOffsetDateTimeExpectations() {
        setValueExpectations(fromHexString(TIMESTAMPTZ_OFFSET_NETWORK_HEX));
    }

    private void setOffsetDateTimeExpectations(OffsetDateTime offsetDateTime) {
        setValueExpectations(TimeZoneDatatypeCoder.getInstanceFor(datatypeCoder).encodeTimestampTz(offsetDateTime));
    }

    private void setOffsetTimeExpectations(OffsetTime offsetTime) {
        ZoneOffset offset = offsetTime.getOffset();
        OffsetDateTime today = OffsetDateTime.now(offset);
        OffsetDateTime timeToday = OffsetDateTime
                .of(today.toLocalDate(), offsetTime.toLocalTime(), offset);
        setOffsetDateTimeExpectations(timeToday);
    }
}