/**
 * 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.hadoop.hive.ql.udf.generic;

import java.sql.Date;
import java.sql.Timestamp;

import org.apache.hadoop.hive.ql.metadata.HiveException;
import org.apache.hadoop.hive.ql.udf.generic.GenericUDF.DeferredJavaObject;
import org.apache.hadoop.hive.ql.udf.generic.GenericUDF.DeferredObject;
import org.apache.hadoop.hive.serde2.io.DateWritable;
import org.apache.hadoop.hive.serde2.io.TimestampWritable;
import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory;
import org.apache.hadoop.hive.serde2.typeinfo.TypeInfoFactory;
import org.apache.hadoop.io.BooleanWritable;
import org.apache.hadoop.io.DoubleWritable;
import org.apache.hadoop.io.Text;

import junit.framework.TestCase;

public class TestGenericUDFMonthsBetween extends TestCase {

  public void testMonthsBetweenForString() throws HiveException {
    // Default run
    GenericUDFMonthsBetween udf = new GenericUDFMonthsBetween();
    ObjectInspector valueOI1 = PrimitiveObjectInspectorFactory.writableStringObjectInspector;
    ObjectInspector valueOI2 = PrimitiveObjectInspectorFactory.writableStringObjectInspector;
    ObjectInspector[] arguments = { valueOI1, valueOI2 };
    udf.initialize(arguments);

    testMonthsBetweenForString(udf);

    // Run without round-off
    GenericUDFMonthsBetween udfWithoutRoundOff = new GenericUDFMonthsBetween();
    ObjectInspector vOI1 = PrimitiveObjectInspectorFactory.writableStringObjectInspector;
    ObjectInspector vOI2 = PrimitiveObjectInspectorFactory.writableStringObjectInspector;
    ObjectInspector vOI3 = PrimitiveObjectInspectorFactory
        .getPrimitiveWritableConstantObjectInspector(TypeInfoFactory.booleanTypeInfo,
            new BooleanWritable(false));
    ObjectInspector[] args = { vOI1, vOI2, vOI3 };
    udfWithoutRoundOff.initialize(args);

    testMonthsBetweenForString(udf);
  }

  public void testMonthsBetweenForString(GenericUDFMonthsBetween udf) throws HiveException {
    // test month diff with fraction considering time components
    runTestStr("1995-02-02", "1995-01-01", 1.03225806, udf);
    runTestStr("2003-07-17", "2005-07-06", -23.64516129, udf);
    // test the last day of month
    runTestStr("2001-06-30", "2000-05-31", 13.0, udf);
    // test the same day of month
    runTestStr("2000-06-01", "2004-07-01", -49.0, udf);
    // test February of non-leap year, 2/28
    runTestStr("2002-02-28", "2002-03-01", -0.12903226, udf);
    // test February of non-leap year, 2/31 is viewd as 3/3 due to 3 days diff
    // from 2/31 to 2/28
    runTestStr("2002-02-31", "2002-03-01", 0.06451613, udf);

    // test Feb of leap year, 2/29
    runTestStr("2012-02-29", "2012-03-01", -0.09677419, udf);
    // test february of leap year, 2/31 is viewed as 3/2 due to 2 days diff from
    // 2/31 to 2/29
    runTestStr("2012-02-31", "2012-03-01", 0.03225806, udf);

    // time part
    // test that there is no lead second adjustment
    runTestStr("1976-01-01 00:00:00", "1975-12-31 23:59:59", 0.00000037, udf);
    // test UDF considers the difference in time components date1 and date2
    runTestStr("1997-02-28 10:30:00", "1996-10-30", 3.94959677, udf);
    runTestStr("1996-10-30", "1997-02-28 10:30:00", -3.94959677, udf);

    // if both are last day of the month then time part should be ignored
    runTestStr("2002-03-31", "2002-02-28", 1.0, udf);
    runTestStr("2002-03-31", "2002-02-28 10:30:00", 1.0, udf);
    runTestStr("2002-03-31 10:30:00", "2002-02-28", 1.0, udf);
    // if the same day of the month then time part should be ignored
    runTestStr("2002-03-24", "2002-02-24", 1.0, udf);
    runTestStr("2002-03-24", "2002-02-24 10:30:00", 1.0, udf);
    runTestStr("2002-03-24 10:30:00", "2002-02-24", 1.0, udf);

    // partial time. time part will be skipped
    runTestStr("1995-02-02 10:39", "1995-01-01", 1.03225806, udf);
    runTestStr("1995-02-02", "1995-01-01 10:39", 1.03225806, udf);
    // no leading 0 for month and day should work
    runTestStr("1995-02-2", "1995-1-01", 1.03225806, udf);
    runTestStr("1995-2-02", "1995-01-1", 1.03225806, udf);
    // short year should work
    runTestStr("495-2-02", "495-01-1", 1.03225806, udf);
    runTestStr("95-2-02", "95-01-1", 1.03225806, udf);
    runTestStr("5-2-02", "5-01-1", 1.03225806, udf);

    // Test with null args
    runTestStr(null, "2002-03-01", null, udf);
    runTestStr("2002-02-28", null, null, udf);
    runTestStr(null, null, null, udf);

    // string dates without day should be parsed to null
    runTestStr("2002-03", "2002-02-24", null, udf);
    runTestStr("2002-03-24", "2002-02", null, udf);

    runTestStr("2003-04-23", "2002-04-24", 11.96774194, udf);
  }

  public void testMonthsBetweenForTimestamp() throws HiveException {
    GenericUDFMonthsBetween udf = new GenericUDFMonthsBetween();
    ObjectInspector valueOI1 = PrimitiveObjectInspectorFactory.writableTimestampObjectInspector;
    ObjectInspector valueOI2 = PrimitiveObjectInspectorFactory.writableTimestampObjectInspector;
    ObjectInspector[] arguments = { valueOI1, valueOI2 };
    udf.initialize(arguments);

    testMonthsBetweenForTimestamp(udf);

    // Run without round-off
    GenericUDFMonthsBetween udfWithoutRoundOff = new GenericUDFMonthsBetween();
    ObjectInspector vOI1 = PrimitiveObjectInspectorFactory.writableTimestampObjectInspector;
    ObjectInspector vOI2 = PrimitiveObjectInspectorFactory.writableTimestampObjectInspector;
    ObjectInspector vOI3 = PrimitiveObjectInspectorFactory
        .getPrimitiveWritableConstantObjectInspector(TypeInfoFactory.booleanTypeInfo,
            new BooleanWritable(false));
    ObjectInspector[] args = { vOI1, vOI2, vOI3 };
    udfWithoutRoundOff.initialize(args);

    testMonthsBetweenForTimestamp(udfWithoutRoundOff);
  }

  public void testMonthsBetweenForTimestamp(GenericUDFMonthsBetween udf) throws HiveException {
    // test month diff with fraction considering time components
    runTestTs("1995-02-02 00:00:00", "1995-01-01 00:00:00", 1.03225806, udf);
    runTestTs("2003-07-17 00:00:00", "2005-07-06 00:00:00", -23.64516129, udf);
    // test the last day of month
    runTestTs("2001-06-30 00:00:00", "2000-05-31 00:00:00", 13.0, udf);
    // test the same day of month
    runTestTs("2000-06-01 00:00:00", "2004-07-01 00:00:00", -49.0, udf);
    // test February of non-leap year, 2/28
    runTestTs("2002-02-28 00:00:00", "2002-03-01 00:00:00", -0.12903226, udf);
    // test February of non-leap year, 2/31 is viewd as 3/3 due to 3 days diff
    // from 2/31 to 2/28
    runTestTs("2002-02-31 00:00:00", "2002-03-01 00:00:00", 0.06451613, udf);

    // test Feb of leap year, 2/29
    runTestTs("2012-02-29 00:00:00", "2012-03-01 00:00:00", -0.09677419, udf);
    // test february of leap year, 2/31 is viewed as 3/2 due to 2 days diff from
    // 2/31 to 2/29
    runTestTs("2012-02-31 00:00:00", "2012-03-01 00:00:00", 0.03225806, udf);

    // time part
    // test that there is no lead second adjustment
    runTestTs("1976-01-01 00:00:00", "1975-12-31 23:59:59", 0.00000037, udf);
    // test UDF considers the difference in time components date1 and date2
    runTestTs("1997-02-28 10:30:00", "1996-10-30 00:00:00", 3.94959677, udf);
    runTestTs("1996-10-30 00:00:00", "1997-02-28 10:30:00", -3.94959677, udf);

    // if both are last day of the month then time part should be ignored
    runTestTs("2002-03-31 00:00:00", "2002-02-28 00:00:00", 1.0, udf);
    runTestTs("2002-03-31 00:00:00", "2002-02-28 10:30:00", 1.0, udf);
    runTestTs("2002-03-31 10:30:00", "2002-02-28 00:00:00", 1.0, udf);
    // if the same day of the month then time part should be ignored
    runTestTs("2002-03-24 00:00:00", "2002-02-24 00:00:00", 1.0, udf);
    runTestTs("2002-03-24 00:00:00", "2002-02-24 10:30:00", 1.0, udf);
    runTestTs("2002-03-24 10:30:00", "2002-02-24 00:00:00", 1.0, udf);

    runTestTs("2003-04-23 23:59:59", "2003-03-24 00:0:0", 0.99999963, udf);

    // Test with null args
    runTestTs(null, "2002-03-01 00:00:00", null, udf);
    runTestTs("2002-02-28 00:00:00", null, null, udf);
    runTestTs(null, null, null, udf);
  }

  public void testMonthsBetweenForDate() throws HiveException {
    GenericUDFMonthsBetween udf = new GenericUDFMonthsBetween();
    ObjectInspector valueOI1 = PrimitiveObjectInspectorFactory.writableDateObjectInspector;
    ObjectInspector valueOI2 = PrimitiveObjectInspectorFactory.writableDateObjectInspector;
    ObjectInspector[] arguments = { valueOI1, valueOI2 };
    udf.initialize(arguments);

    testMonthsBetweenForDate(udf);

    // Run without round-off
    GenericUDFMonthsBetween udfWithoutRoundOff = new GenericUDFMonthsBetween();
    ObjectInspector vOI1 = PrimitiveObjectInspectorFactory.writableDateObjectInspector;
    ObjectInspector vOI2 = PrimitiveObjectInspectorFactory.writableDateObjectInspector;
    ObjectInspector vOI3 = PrimitiveObjectInspectorFactory
        .getPrimitiveWritableConstantObjectInspector(TypeInfoFactory.booleanTypeInfo,
            new BooleanWritable(false));
    ObjectInspector[] args = { vOI1, vOI2, vOI3 };
    udfWithoutRoundOff.initialize(args);

    testMonthsBetweenForDate(udfWithoutRoundOff);
  }

  public void testMonthsBetweenForDate(GenericUDFMonthsBetween udf) throws HiveException {
    // test month diff with fraction considering time components
    runTestDt("1995-02-02", "1995-01-01", 1.03225806, udf);
    runTestDt("2003-07-17", "2005-07-06", -23.64516129, udf);
    // test the last day of month
    runTestDt("2001-06-30", "2000-05-31", 13.0, udf);
    // test the same day of month
    runTestDt("2000-06-01", "2004-07-01", -49.0, udf);
    // test February of non-leap year, 2/28
    runTestDt("2002-02-28", "2002-03-01", -0.12903226, udf);
    // test February of non-leap year, 2/31 is viewd as 3/3 due to 3 days diff
    // from 2/31 to 2/28
    runTestDt("2002-02-31", "2002-03-01", 0.06451613, udf);

    // test Feb of leap year, 2/29
    runTestDt("2012-02-29", "2012-03-01", -0.09677419, udf);
    // test february of leap year, 2/31 is viewed as 3/2 due to 2 days diff from
    // 2/31 to 2/29
    runTestDt("2012-02-31", "2012-03-01", 0.03225806, udf);
    // Test with null args
    runTestDt(null, "2002-03-01", null, udf);
    runTestDt("2002-02-28", null, null, udf);
    runTestDt(null, null, null, udf);
  }

  protected void runTestStr(String date1, String date2, Double expDiff, GenericUDFMonthsBetween udf)
      throws HiveException {
    DeferredJavaObject valueObj1 = new DeferredJavaObject(date1 == null ? null : new Text(date1));
    DeferredJavaObject valueObj2 = new DeferredJavaObject(date2 == null ? null : new Text(date2));
    DeferredObject[] args = new DeferredObject[] { valueObj1, valueObj2 };
    DoubleWritable output = (DoubleWritable) udf.evaluate(args);
    if (expDiff == null) {
      assertNull("months_between() test for NULL STRING failed", output);
    } else {
      assertNotNull("months_between() test for NOT NULL STRING failed", output);
      assertEquals("months_between() test for STRING failed", expDiff, output.get(), 0.00000001D);
    }
  }

  protected void runTestTs(String ts1, String ts2, Double expDiff, GenericUDFMonthsBetween udf)
      throws HiveException {
    TimestampWritable tsWr1 = ts1 == null ? null : new TimestampWritable(Timestamp.valueOf(ts1));
    TimestampWritable tsWr2 = ts2 == null ? null : new TimestampWritable(Timestamp.valueOf(ts2));
    DeferredJavaObject valueObj1 = new DeferredJavaObject(tsWr1);
    DeferredJavaObject valueObj2 = new DeferredJavaObject(tsWr2);
    DeferredObject[] args = new DeferredObject[] { valueObj1, valueObj2 };
    DoubleWritable output = (DoubleWritable) udf.evaluate(args);
    if (expDiff == null) {
      assertNull("months_between() test for NULL TIMESTAMP failed", output);
    } else {
      assertNotNull("months_between() test for NOT NULL TIMESTAMP failed", output);
      assertEquals("months_between() test for TIMESTAMP failed", expDiff, output.get(), 0.00000001D);
    }
  }

  protected void runTestDt(String dt1, String dt2, Double expDiff, GenericUDFMonthsBetween udf)
      throws HiveException {
    DateWritable dtWr1 = dt1 == null ? null : new DateWritable(Date.valueOf(dt1));
    DateWritable dtWr2 = dt2 == null ? null : new DateWritable(Date.valueOf(dt2));
    DeferredJavaObject valueObj1 = new DeferredJavaObject(dtWr1);
    DeferredJavaObject valueObj2 = new DeferredJavaObject(dtWr2);
    DeferredObject[] args = new DeferredObject[] { valueObj1, valueObj2 };
    DoubleWritable output = (DoubleWritable) udf.evaluate(args);
    if (expDiff == null) {
      assertNull("months_between() test for NULL DATE failed", output);
    } else {
      assertNotNull("months_between() test for NOT NULL DATE failed", output);
      assertEquals("months_between() test for DATE failed", expDiff, output.get(), 0.00000001D);
    }
  }
}