/* See LICENSE for licensing and NOTICE for copyright. */
package org.ldaptive.transcode;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

/**
 * Unit test for {@link GeneralizedTimeValueTranscoder}.
 *
 * @author  Middleware Services
 */
public class GeneralizedTimeValueTranscoderTest
{

  /** Transcoder to test. */
  private final GeneralizedTimeValueTranscoder transcoder = new GeneralizedTimeValueTranscoder();


  /**
   * Date test data.
   *
   * @return  dates
   *
   * @throws  Exception  if test data cannot be generated
   */
  @DataProvider(name = "dates")
  public Object[][] createDates()
    throws Exception
  {
    return
      new Object[][] {
        // no fraction
        new Object[] {
          createDateTime("Z", 1990, 10, 23, 17),
          "1990102317Z",
          "19901023170000.000Z",
        },
        new Object[] {
          createDateTime("GMT+04", 2007, 10, 27, 13),
          "2007102713+04",
          "20071027090000.000Z",
        },
        new Object[] {
          createDateTime("GMT-1030", 1993, 12, 15, 14),
          "1993121514-1030",
          "19931216003000.000Z",
        },
        // dot fraction
        new Object[] {
          createDateTime("Z", 2006, 8, 18, 23, 35, 9, 600),
          "2006081823.586Z",
          "20060818233509.600Z",
        },
        new Object[] {
          createDateTime("GMT+0100", 2012, 2, 15, 2, 15, 0, 0),
          "2012021502.250+0100",
          "20120215011500.000Z",
        },
        new Object[] {
          createDateTime("GMT-1030", 2005, 6, 4, 5, 52, 55, 200),
          "2005060405.882-1030",
          "20050604162255.200Z",
        },
        // comma fraction
        new Object[] {
          createDateTime("Z", 1992, 3, 1, 7, 20, 38, 400),
          "1992030107,344Z",
          "19920301072038.400Z",
        },
        new Object[] {
          createDateTime("GMT+0100", 2008, 3, 21, 23, 30, 14, 400),
          "2008032123,504+0100",
          "20080321223014.400Z",
        },
        new Object[] {
          createDateTime("GMT-1030", 2018, 4, 8, 6, 45, 43, 200),
          "2018040806,762-1030",
          "20180408171543.200Z",
        },
        // min, no fraction
        new Object[] {
          createDateTime("Z", 1994, 6, 7, 11, 50),
          "199406071150Z",
          "19940607115000.000Z",
        },
        new Object[] {
          createDateTime("GMT+04", 2015, 8, 24, 12, 41),
          "201508241241+04",
          "20150824084100.000Z",
        },
        new Object[] {
          createDateTime("GMT-1030", 2012, 12, 19, 7, 31),
          "201212190731-1030",
          "20121219180100.000Z",
        },
        // min, dot fraction
        new Object[] {
          createDateTime("Z", 2019, 5, 27, 13, 57, 46, 800),
          "201905271357.780Z",
          "20190527135746.800Z",
        },
        new Object[] {
          createDateTime("GMT+0100", 2001, 2, 23, 17, 32, 33, 180),
          "200102231732.553+0100",
          "20010223163233.180Z",
        },
        new Object[] {
          createDateTime("GMT-1030", 1997, 10, 27, 19, 32, 31, 860),
          "199710271932.531-1030",
          "19971028060231.860Z",
        },
        // min, comma fraction
        new Object[] {
          createDateTime("Z", 2005, 1, 6, 7, 5, 19, 260),
          "200501060705,321Z",
          "20050106070519.260Z",
        },
        new Object[] {
          createDateTime("GMT+0100", 2019, 10, 1, 3, 53, 16, 260),
          "201910010353,271+0100",
          "20191001025316.260Z",
        },
        new Object[] {
          createDateTime("GMT-1030", 2003, 2, 4, 10, 56, 49, 560),
          "200302041056,826-1030",
          "20030204212649.560Z",
        },
        // min, sec, no fraction
        new Object[] {
          createDateTime("Z", 1993, 3, 4, 1, 6, 45),
          "19930304010645Z",
          "19930304010645.000Z",
        },
        new Object[] {
          createDateTime("GMT+04", 1996, 11, 15, 2, 1, 33),
          "19961115020133+04",
          "19961114220133.000Z",
        },
        new Object[] {
          createDateTime("GMT-1030", 2012, 11, 4, 10, 37, 40),
          "20121104103740-1030",
          "20121104210740.000Z",
        },
        // min, sec, dot fraction
        new Object[] {
          createDateTime("Z", 2016, 5, 17, 5, 31, 50, 193),
          "20160517053150.193Z",
          "20160517053150.193Z",
        },
        new Object[] {
          createDateTime("GMT+0100", 2006, 9, 28, 9, 50, 56, 142),
          "20060928095056.142+0100",
          "20060928085056.142Z",
        },
        new Object[] {
          createDateTime("GMT-1030", 1997, 7, 15, 17, 41, 7, 418),
          "19970715174107.418-1030",
          "19970716041107.418Z",
        },
        // min, sec, comma fraction
        new Object[] {
          createDateTime("Z", 2003, 5, 11, 3, 55, 24, 11),
          "20030511035524,011Z",
          "20030511035524.011Z",
        },
        new Object[] {
          createDateTime("GMT+0100", 2000, 9, 8, 4, 38, 1, 536),
          "20000908043801,536+0100",
          "20000908033801.536Z",
        },
        new Object[] {
          createDateTime("GMT-1030", 1999, 5, 12, 3, 34, 16, 307),
          "19990512033416.307-1030",
          "19990512140416.307Z",
        },
        // feb 29 in leap year
        new Object[] {
          createDateTime("Z", 2012, 2, 29, 0),
          "2012022900Z",
          "20120229000000.000Z",
        },
      };
  }


  /**
   * Date test data.
   *
   * @return  dates
   *
   * @throws  Exception  if test data cannot be generated
   */
  @DataProvider(name = "invalid")
  public Object[][] createInvalidSyntax()
    throws Exception
  {
    return
      new Object[][] {
        // invalid month
        new Object[] {"20050003193252Z"},
        new Object[] {"20011315121902Z"},
        new Object[] {"20065205051238Z"},
        new Object[] {"19989912035214Z"},
        // invalid day
        new Object[] {"20130600153448Z"},
        new Object[] {"19970132100613Z"},
        new Object[] {"19990466184045Z"},
        new Object[] {"20180299184327Z"},
        // invalid hour
        new Object[] {"19991104604515Z"},
        new Object[] {"19990611775328Z"},
        new Object[] {"20161126993226Z"},
        // invalid min
        new Object[] {"20150120036044Z"},
        new Object[] {"19950801078428Z"},
        new Object[] {"20141110059918Z"},
        // invalid sec
        new Object[] {"19970507065463Z"},
        new Object[] {"20010804024188Z"},
        new Object[] {"20100401071599Z"},
        // invalid timezone
        new Object[] {"20110118183832Z+"},
        new Object[] {"20140828202734+2400"},
        new Object[] {"20020505162928+9900"},
        new Object[] {"20160404014028+1260"},
        new Object[] {"20020522033010+1299"},
        // leap second
        new Object[] {"19981126110460Z"},
        // too short
        new Object[] {"200"},
        new Object[] {"2009"},
        new Object[] {"20091"},
        new Object[] {"200912"},
        new Object[] {"2009122"},
        new Object[] {"20091220"},
        new Object[] {"200912200"},
        new Object[] {"2009122003"},
        // invalid characters
        new Object[] {"2AA9122003Z"},
        new Object[] {"2009122003Y"},
        new Object[] {"2004072704BBZ"},
        new Object[] {"200407270431CCZ"},
        new Object[] {"20040727043108.DZ"},
        new Object[] {"20040727043108.DDZ"},
        new Object[] {"20040727043108.DDDZ"},
        // missing timezone
        new Object[] {"2017092602"},
        new Object[] {"201709260206"},
        new Object[] {"20170926020649"},
        new Object[] {"20170926020649.5"},
        new Object[] {"20170926020649.56"},
        new Object[] {"20170926020649.567"},
        new Object[] {"20170926020649,2"},
        new Object[] {"20170926020649,23"},
        new Object[] {"20170926020649,234"},
        // invalid timezone character
        new Object[] {"2010112415E"},
        new Object[] {"201011241520E"},
        new Object[] {"20101124152005E"},
        new Object[] {"20101124152005.1E"},
        new Object[] {"20101124152005.12E"},
        new Object[] {"20101124152005.122E"},
        new Object[] {"20130915044541ZF"},
        new Object[] {"20101124152005+0630F"},
        // missing fraction
        new Object[] {"20020511040917.Z"},
        new Object[] {"20020511040917,Z"},
        // feb 29 in non-leap year
        new Object[] {"2011022900Z"},
        new Object[] {""},
        new Object[] {null},
      };
  }


  /**
   * @param  date  to compare
   * @param  generalizedTime  ldap attribute string value
   * @param  formatTime  formatted ldap attribute string value
   *
   * @throws  Exception  On test failure.
   */
  @Test(groups = "transcode", dataProvider = "dates")
  public void decodeGeneralized(final ZonedDateTime date, final String generalizedTime, final String formatTime)
    throws Exception
  {
    Assert.assertEquals(transcoder.decodeStringValue(generalizedTime), date);
  }


  /**
   * @param  date  to compare
   * @param  generalizedTime  ldap attribute string value
   * @param  formatTime  formatted ldap attribute string value
   *
   * @throws  Exception  On test failure.
   */
  @Test(groups = "transcode", dataProvider = "dates")
  public void encodeFormat(final ZonedDateTime date, final String generalizedTime, final String formatTime)
    throws Exception
  {
    Assert.assertEquals(transcoder.encodeStringValue(date), formatTime);
  }


  /**
   * @param  date  to compare
   * @param  generalizedTime  ldap attribute string value
   * @param  formatTime  formatted ldap attribute string value
   *
   * @throws  Exception  On test failure.
   */
  @Test(groups = "transcode", dataProvider = "dates")
  public void decodeFormat(final ZonedDateTime date, final String generalizedTime, final String formatTime)
    throws Exception
  {
    Assert.assertEquals(transcoder.decodeStringValue(formatTime), date.withZoneSameInstant(ZoneOffset.UTC));
  }


  /**
   * @param  generalizedTime  ldap attribute string value
   *
   * @throws  Exception  On test failure.
   */
  @Test(groups = "transcode", dataProvider = "invalid")
  public void testInvalid(final String generalizedTime)
    throws Exception
  {
    try {
      transcoder.decodeStringValue(generalizedTime);
      Assert.fail("Should have thrown exception");
    } catch (Exception e) {
      Assert.assertEquals(e.getClass(), IllegalArgumentException.class);
    }
  }


  /**
   * Creates a date time for testing.
   *
   * @param  timezone  of the date time
   * @param  values  corresponding to date time fields
   *
   * @return  date time
   */
  protected ZonedDateTime createDateTime(final String timezone, final int... values)
  {
    return ZonedDateTime.of(
      LocalDateTime.of(
        values[0],
        values[1],
        values[2],
        values[3],
        values.length > 4 ? values[4] : 0,
        values.length > 5 ? values[5] : 0).plus(values.length > 6 ? values[6] : 0, ChronoUnit.MILLIS),
      ZoneId.of(timezone));
  }
}
