/**
 * 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.cassandra.serializers;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.junit.Test;

import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

public class TimestampSerializerTest
{
    public static final long ONE_SECOND = 1000L;
    public static final long ONE_MINUTE = 60 * ONE_SECOND;
    public static final long ONE_HOUR = 60 * ONE_MINUTE;
    public static final long ONE_DAY = 24 * ONE_HOUR;
    public static final long BASE_OFFSET = TimestampSerializer.dateStringToTimestamp("1970-01-01");

    @Test
    public void testFormatResults() throws MarshalException
    {
        validateStringTimestamp("1970-01-01 00:00", BASE_OFFSET);
        validateStringTimestamp("1970-01-01 00:01", BASE_OFFSET + ONE_MINUTE);
        validateStringTimestamp("1970-01-01 01:00", BASE_OFFSET + ONE_HOUR);
        validateStringTimestamp("1970-01-02 00:00", BASE_OFFSET + ONE_DAY);
        validateStringTimestamp("1970-01-02 00:00 UTC", ONE_DAY);
        validateStringTimestamp("1970-01-01 00:01+01", ONE_MINUTE - ONE_HOUR);
        validateStringTimestamp("1970-01-01 01:00+0100", ONE_HOUR - ONE_HOUR);
        validateStringTimestamp("1970-01-02 00:00+01:00", ONE_DAY - ONE_HOUR);
        validateStringTimestamp("1970-01-01 01:00-0200", ONE_HOUR + 2 * ONE_HOUR);
        validateStringTimestamp("1970-01-01 01:00Z", ONE_HOUR);

        validateStringTimestamp("1970-01-01 00:00:00", BASE_OFFSET);
        validateStringTimestamp("1970-01-01 00:00:01", BASE_OFFSET + ONE_SECOND);
        validateStringTimestamp("1970-01-01 00:01:00", BASE_OFFSET + ONE_MINUTE);
        validateStringTimestamp("1970-01-01 01:00:00", BASE_OFFSET + ONE_HOUR);
        validateStringTimestamp("1970-01-02 00:00:00", BASE_OFFSET + ONE_DAY);
        validateStringTimestamp("1970-01-02 00:00:00 UTC", ONE_DAY);
        validateStringTimestamp("1970-01-01 00:01:00+01", ONE_MINUTE - ONE_HOUR);
        validateStringTimestamp("1970-01-01 01:00:00+0100", ONE_HOUR - ONE_HOUR);
        validateStringTimestamp("1970-01-02 00:00:00+01:00", ONE_DAY - ONE_HOUR);
        validateStringTimestamp("1970-01-01 01:00:00-0200", ONE_HOUR + 2 * ONE_HOUR);
        validateStringTimestamp("1970-01-01 01:00:00Z", ONE_HOUR);

        validateStringTimestamp("1970-01-01 00:00:00.000", BASE_OFFSET);
        validateStringTimestamp("1970-01-01 00:00:00.000", BASE_OFFSET);
        validateStringTimestamp("1970-01-01 00:00:01.000", BASE_OFFSET + ONE_SECOND);
        validateStringTimestamp("1970-01-01 00:01:00.000", BASE_OFFSET + ONE_MINUTE);
        validateStringTimestamp("1970-01-01 01:00:00.000", BASE_OFFSET + ONE_HOUR);
        validateStringTimestamp("1970-01-02 00:00:00.000", BASE_OFFSET + ONE_DAY);
        validateStringTimestamp("1970-01-02 00:00:00.000 UTC", ONE_DAY);
        validateStringTimestamp("1970-01-01 00:00:00.100 UTC", 100L);
        validateStringTimestamp("1970-01-01 00:01:00.001+01", ONE_MINUTE - ONE_HOUR + 1);
        validateStringTimestamp("1970-01-01 01:00:00.002+0100", ONE_HOUR - ONE_HOUR + 2);
        validateStringTimestamp("1970-01-02 00:00:00.003+01:00", ONE_DAY - ONE_HOUR + 3);
        validateStringTimestamp("1970-01-01 01:00:00.004-0200", ONE_HOUR + 2 * ONE_HOUR + 4);
        validateStringTimestamp("1970-01-01 01:00:00.004Z", ONE_HOUR + 4);

        validateStringTimestamp("1970-01-01T00:00", BASE_OFFSET);
        validateStringTimestamp("1970-01-01T00:01", BASE_OFFSET + ONE_MINUTE);
        validateStringTimestamp("1970-01-01T01:00", BASE_OFFSET + ONE_HOUR);
        validateStringTimestamp("1970-01-02T00:00", BASE_OFFSET + ONE_DAY);
        validateStringTimestamp("1970-01-02T00:00 UTC", ONE_DAY);
        validateStringTimestamp("1970-01-01T00:01+01", ONE_MINUTE - ONE_HOUR);
        validateStringTimestamp("1970-01-01T01:00+0100", ONE_HOUR - ONE_HOUR);
        validateStringTimestamp("1970-01-02T00:00+01:00", ONE_DAY - ONE_HOUR);
        validateStringTimestamp("1970-01-01T01:00-0200", ONE_HOUR + 2 * ONE_HOUR);
        validateStringTimestamp("1970-01-01T01:00Z", ONE_HOUR);

        validateStringTimestamp("1970-01-01T00:00:00", BASE_OFFSET);
        validateStringTimestamp("1970-01-01T00:00:01", BASE_OFFSET + ONE_SECOND);
        validateStringTimestamp("1970-01-01T00:01:00", BASE_OFFSET + ONE_MINUTE);
        validateStringTimestamp("1970-01-01T01:00:00", BASE_OFFSET + ONE_HOUR);
        validateStringTimestamp("1970-01-02T00:00:00", BASE_OFFSET + ONE_DAY);
        validateStringTimestamp("1970-01-02T00:00:00 UTC", ONE_DAY);
        validateStringTimestamp("1970-01-01T00:01:00+01", ONE_MINUTE - ONE_HOUR);
        validateStringTimestamp("1970-01-01T01:00:00+0100", ONE_HOUR - ONE_HOUR);
        validateStringTimestamp("1970-01-02T00:00:00+01:00", ONE_DAY - ONE_HOUR);
        validateStringTimestamp("1970-01-01T01:00:00-0200", ONE_HOUR + 2 * ONE_HOUR);
        validateStringTimestamp("1970-01-01T01:00:00Z", ONE_HOUR);

        validateStringTimestamp("1970-01-01T00:00:00.000", BASE_OFFSET);
        validateStringTimestamp("1970-01-01T00:00:00.000", BASE_OFFSET);
        validateStringTimestamp("1970-01-01T00:00:01.000", BASE_OFFSET + ONE_SECOND);
        validateStringTimestamp("1970-01-01T00:01:00.000", BASE_OFFSET + ONE_MINUTE);
        validateStringTimestamp("1970-01-01T01:00:00.000", BASE_OFFSET + ONE_HOUR);
        validateStringTimestamp("1970-01-02T00:00:00.000", BASE_OFFSET + ONE_DAY);
        validateStringTimestamp("1970-01-02T00:00:00.000 UTC", ONE_DAY);
        validateStringTimestamp("1970-01-01T00:00:00.100 UTC", 100L);
        validateStringTimestamp("1970-01-01T00:01:00.001+01", ONE_MINUTE - ONE_HOUR + 1);
        validateStringTimestamp("1970-01-01T01:00:00.002+0100", ONE_HOUR - ONE_HOUR + 2);
        validateStringTimestamp("1970-01-02T00:00:00.003+01:00", ONE_DAY - ONE_HOUR + 3);
        validateStringTimestamp("1970-01-01T01:00:00.004-0200", ONE_HOUR + 2 * ONE_HOUR + 4);
        validateStringTimestamp("1970-01-01T01:00:00.004Z", ONE_HOUR + 4);

        validateStringTimestamp("1970-01-01", BASE_OFFSET);
        validateStringTimestamp("1970-01-02 UTC", ONE_DAY);
        validateStringTimestamp("1970-01-01+01", -ONE_HOUR);
        validateStringTimestamp("1970-01-01+0100", -ONE_HOUR);
        validateStringTimestamp("1970-01-02+01:00", ONE_DAY - ONE_HOUR);
        validateStringTimestamp("1970-01-01-0200", 2 * ONE_HOUR);
        validateStringTimestamp("1970-01-01Z", 0L);
    }

    @Test
    public void testGeneralTimeZoneFormats()
    {
        // Central Standard Time; CST, GMT-06:00
        final long cstOffset = 6 * ONE_HOUR;
        validateStringTimestamp("1970-01-01 00:00:00 Central Standard Time", cstOffset);
        validateStringTimestamp("1970-01-01 00:00:00 CST", cstOffset);
        validateStringTimestamp("1970-01-01 00:00:00 -0600", cstOffset);
        validateStringTimestamp("1970-01-01 00:00:00-0600", cstOffset);
        validateStringTimestamp("1970-01-01T00:00:00 GMT-06:00", cstOffset);
        validateStringTimestamp("1970-01-01T00:00:00 -0600", cstOffset);
        validateStringTimestamp("1970-01-01T00:00:00-0600", cstOffset);
    }

    @Test
    public void testVaryingFractionalPrecision() // CASSANDRA-15976
    {
        validateStringTimestamp("1970-01-01 00:00:00.10 UTC", 100L);
        validateStringTimestamp("1970-01-01 00:00:00.1+00", 100L);
        validateStringTimestamp("1970-01-01T00:00:00.10-0000", 100L);
        validateStringTimestamp("1970-01-01T00:00:00.1Z", 100L);
        validateStringTimestamp("1970-01-01 00:00:00.1 Central Standard Time", 6 * ONE_HOUR + 100L);
        validateStringTimestamp("1970-01-01 00:00:00.10+00:00", 100L);
        validateStringTimestamp("1970-01-01T00:00:00.1", BASE_OFFSET + 100L);
        validateStringTimestamp("1970-01-01T00:00:00.10-00:00", 100L);
    }

    @Test
    public void testZeroPadding() // CASSANDRA-16105
    {
        long expected = ONE_HOUR + ONE_MINUTE + ONE_SECOND;
        validateStringTimestamp("1970-01-01 01:01:01Z", expected);
        validateStringTimestamp("1970-1-1 1:1:1Z", expected);
        validateStringTimestamp("1970-01-01 01:01:01", BASE_OFFSET + expected);
        validateStringTimestamp("1970-1-1 1:1:1", BASE_OFFSET + expected);

        expected = -31556905139000L;
        validateStringTimestamp("0970-01-01 01:01:01Z", expected);
        validateStringTimestamp("970-1-1 1:1:1Z", expected);

        expected = 10*ONE_MINUTE + 100L;
        validateStringTimestamp("1970-01-01T0:0000000010:0.1Z", expected);
        validateStringTimestamp("0001970-01-01T0:010:0.1Z", expected);
        validateStringTimestamp("1970-1-1T0:10:0.1Z", expected);
        validateStringTimestamp("1970-0001-0001T0:010:0.1Z", expected);

        validateStringTimestamp("1970-1-01T1:1Z", ONE_HOUR + ONE_MINUTE);
        validateStringTimestamp("1970-1-01T1:1", BASE_OFFSET + ONE_HOUR + ONE_MINUTE);
    }

    @Test
    public void testInvalidTimezones()
    {
        List<String> timestamps = new ArrayList<String>(
            Arrays.asList(
                "1970-01-01 00:00:00 xentral Standard Time",
                "1970-01-01 00:00:00-060"
            )
        );
        expectStringTimestampsExcept(timestamps);
    }

    @Test
    public void testInvalidFormat()
    {
        List<String> timestamps = new ArrayList<String>(
            Arrays.asList(
                "1970-01-01 00:00:00andsomeextra",
                "prefix1970-01-01 00:00:00",
                "1970-01-01 00.56",
                "1970-01-0100:00:00"
            )
        );
        expectStringTimestampsExcept(timestamps);
    }

    @Test
    public void testNumeric()
    {
        // now (positive
        final long now = currentTimeMillis();
        assertEquals(now, TimestampSerializer.dateStringToTimestamp(Long.toString(now)));

        // negative
        final long then = -11234L;
        assertEquals(then, TimestampSerializer.dateStringToTimestamp(Long.toString(then)));

        // overflows
        for (Long l: Arrays.asList(Long.MAX_VALUE, Long.MIN_VALUE))
        {
            try
            {
                TimestampSerializer.dateStringToTimestamp(Long.toString(l) + "1");
                fail("Expected exception was not raised while parsing overflowed long.");
            }
            catch (MarshalException e)
            {
                continue;
            }
        }
    }

    @Test
    public void testNow()
    {
        final long threshold = 5;
        final long now = currentTimeMillis();
        final long parsed = TimestampSerializer.dateStringToTimestamp("now");
        assertTrue("'now' timestamp not within expected tolerance.", now <= parsed && parsed <= now + threshold);
    }

    private void validateStringTimestamp(String s, long expected)
    {
        try
        {
            long ts = TimestampSerializer.dateStringToTimestamp(s);
            assertEquals( "Failed to parse expected timestamp value from " + s, expected, ts);
        }
        catch (MarshalException e)
        {
            fail(String.format("Failed to parse \"%s\" as timestamp.", s));
        }
    }

    private void expectStringTimestampsExcept(List<String> timeStrings)
    {
        for (String s: timeStrings)
        {
            try
            {
                TimestampSerializer.dateStringToTimestamp(s);
                fail(String.format("Parsing succeeded while expecting failure from \"%s\"", s));
            }
            catch(MarshalException e)
            {
                continue;
            }

        }
    }
}
