/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2009-2010 Sun Microsystems, Inc.
 *      Portions Copyright 2010-2015 ForgeRock AS.
 */
package org.opends.server.schema;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Set;
import java.util.TimeZone;

import org.forgerock.opendj.ldap.Assertion;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ConditionResult;
import org.forgerock.opendj.ldap.DecodeException;
import org.forgerock.opendj.ldap.schema.MatchingRule;
import org.opends.server.TestCaseUtils;
import org.opends.server.core.DirectoryServer;
import org.opends.server.types.Attribute;
import org.opends.server.types.AttributeType;
import org.opends.server.types.DN;
import org.opends.server.types.Entry;
import org.opends.server.types.FilterType;
import org.opends.server.types.SearchFilter;
import org.opends.server.util.TimeThread;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import static org.assertj.core.api.Assertions.*;
import static org.opends.server.schema.GeneralizedTimeSyntax.*;
import static org.opends.server.schema.SchemaConstants.*;
import static org.testng.Assert.*;

/** This class tests various time-based matching rules. */
@SuppressWarnings("javadoc")
public final class TimeBasedMatchingRuleTest
        extends SchemaTestCase
{
  /** User DNs to be used in tests. */
  private DN user1;
  private DN user2;
  private DN user3;
  private DN user4;
  private DN user5;

  private static final String TIME_ATTR = "test-time";
  private static final String DATE_ATTR = "test-date";


  /**
   * Ensures that the Directory Server is running before executing the
   * testcases.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @BeforeClass
  public void startServer()
         throws Exception
  {
    user1 = DN.valueOf("cn=user1,dc=example,dc=com");
    user2 = DN.valueOf("cn=user2,dc=example,dc=com");
    user3 = DN.valueOf("cn=user3,dc=example,dc=com");
    user4 = DN.valueOf("cn=user4,dc=example,dc=com");
    user5 = DN.valueOf("cn=user5,dc=example,dc=com");

    /*
    Extend the schema and add an attribute which is based on
    generalizedTimeSyntax. Since all the existing attributes based
    on that syntax are read-only, let us create a new attribute and
    add it.*/
   int resultCode = TestCaseUtils.applyModifications(true,
    "dn: cn=schema",
    "changetype: modify",
    "add: attributeTypes",
    "attributeTypes: ( test-time-oid NAME 'test-time' DESC 'Test time attribute'  EQUALITY   " +
    "generalizedTimeMatch ORDERING  generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE )",
    "attributeTypes: ( test-date-oid NAME 'test-date' DESC 'Test date attribute'  EQUALITY   " +
    "generalizedTimeMatch ORDERING  generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE )",
    "-",
    "add: objectclasses",
    "objectclasses: ( testoc-oid NAME 'testOC' SUP top AUXILIARY MUST test-time)",
    "objectclasses: ( testoc2-oid NAME 'testOC2' SUP top AUXILIARY MUST test-date)"
    );
    assertEquals(0, resultCode);
  }

  @DataProvider
  public Object[][] relativeTime()
  {
    return new Object[][] {
      // relativeTime less than expired events
      { TIME_ATTR + ":" + EXT_OMR_RELATIVE_TIME_LT_OID + ":=-60m", new DN[] { user1, user2, } },
      // relativeTime less than future events
      { TIME_ATTR + ":" + EXT_OMR_RELATIVE_TIME_LT_OID + ":=1d", new DN[] { user1, user2, user3, user5, } },
      // relativeTime greater than expired events
      { TIME_ATTR + ":" + EXT_OMR_RELATIVE_TIME_GT_OID + ":=-1h", new DN[] { user3, user4, user5, } },
      // relativeTime greater than future events
      { TIME_ATTR + ":" + EXT_OMR_RELATIVE_TIME_GT_OID + ":=0s", new DN[] { user3, user4, } },
    };
  }

  @Test(dataProvider = "relativeTime")
  public void testRelativeTimeUsingAssertion(String filterString, DN[] expectedDNs) throws Exception
  {
    SearchFilter filter = SearchFilter.createFilterFromString(filterString);
    assertThat(getMatchingEntryDNs(filter)).containsOnly(expectedDNs);
  }

  private Collection<DN> getMatchingEntryDNs(SearchFilter filter) throws Exception
  {
    AttributeType attrType = filter.getAttributeType();
    MatchingRule rule = DirectoryServer.getMatchingRule(filter.getMatchingRuleID());
    Assertion assertion = rule.getAssertion(filter.getAssertionValue());

    Collection<DN> results = new ArrayList<>();
    for (Entry entry : makeEntries())
    {
      Attribute attribute = entry.getExactAttribute(attrType, Collections.<String> emptySet());
      if (attribute != null)
      {
        ByteString attrValue = rule.normalizeAttributeValue(attribute.iterator().next());
        if (assertion.matches(attrValue).toBoolean())
        {
          results.add(entry.getName());
        }
      }
    }
    return results;
  }

  /** Test to search using the relative time matching rule with index. */
  @Test(dataProvider = "relativeTime")
  public void testRelativeTimeWithIndex(String filterString, DN[] expectedDNs) throws Exception
  {
    FakeEntryIndex index = new FakeEntryIndex(TIME_ATTR);
    index.addAll(makeEntries());
    Collection<Entry> entries = index.evaluateFilter(filterString);
    assertThat(toNames(entries)).containsOnly(expectedDNs);
  }

  private List<DN> toNames(Collection<? extends Entry> entries)
  {
    List<DN> results = new ArrayList<>();
    for (Entry entry : entries)
    {
      results.add(entry.getName());
    }
    return results;
  }

  /**
   * Test to match the attribute and the assertion values using a partial date and time
   * matching rule.
   */
  @Test(dataProvider="partialDateTimeValues")
  public void testPartialDateNTimeMatch(long timeInMillis, String generalizedTime, String assertionValue)
      throws Exception
  {
    MatchingRule partialTimeRule = DirectoryServer.getMatchingRule(
            EXT_PARTIAL_DATE_TIME_NAME.toLowerCase());
    Assertion assertion = partialTimeRule.getAssertion(ByteString.valueOfUtf8(assertionValue));
    assertEquals(assertion.matches(ByteString.valueOfLong(timeInMillis)), ConditionResult.TRUE);
  }

  @Test(dataProvider="partialDateTimeValues")
  public void testPartialDateNTimeMatchViaIndex(long timeInMillis, String generalizedTime, String assertionValue)
      throws Exception
  {
    ByteString attrValue = ByteString.valueOfUtf8(generalizedTime);
    ByteString assertValue = ByteString.valueOfUtf8(assertionValue);

    FakeByteStringIndex fakeIndex = new FakeByteStringIndex(EXT_PARTIAL_DATE_TIME_NAME);
    fakeIndex.add(attrValue);
    Set<ByteString> attrValues = fakeIndex.evaluateAssertionValue(assertValue, FilterType.EXTENSIBLE_MATCH);
    assertThat(attrValues).containsOnly(attrValue);
  }



  /** Tests the assertion syntax of the relative time matching rules. */
  @Test(dataProvider= "relativeTimeValues")
  public void testRelativeTimeMatchingRuleAssertionSyntax(String assertion,boolean isValid)
  {
    MatchingRule relativeTimeLTRule =
            DirectoryServer.getMatchingRule(EXT_OMR_RELATIVE_TIME_LT_ALT_NAME.toLowerCase());
    try
    {
      relativeTimeLTRule.getAssertion(ByteString.valueOfUtf8(assertion));
      // An invalid value can't get away without throwing exception.
      assertTrue(isValid);
    }
    catch (DecodeException e)
    {
      //invalid values will throw an exception.
      assertFalse(isValid);
    }
  }



  /** Tests the assertion syntax of the partial date and time matching rules. */
  @Test(dataProvider= "partialDateTimeSyntaxes")
  public void testPartialDateTimeMatchingRuleAssertionSyntax(String assertion,boolean isValid)
  {
    MatchingRule partialDTRule =
            DirectoryServer.getMatchingRule(EXT_PARTIAL_DATE_TIME_OID);
    try
    {
      partialDTRule.getAssertion(ByteString.valueOfUtf8(assertion));
      assertTrue(isValid);
    }
    catch (DecodeException e)
    {
      //invalid values will throw an exception.
      assertFalse(isValid);
    }
  }



  /** Generates data for testing relative time matching rule assertion syntax. */
  @DataProvider
  public Object[][] relativeTimeValues()
  {
    return new Object[][] {
      {"1s",true},
      {"1s0d",false},
      {"-1d",true},
      {"2h",true},
      {"+2w",true},
      {"0",true},
      {"0s",true},
      {"0d",true},
      {"xyz",false},
      {"12w-2d",false},
      {"1s2s",false},
      {"1d4s5d",false}
    };
  }


  /** Generates the data for testing partial time date and time values. */
  @DataProvider
  public Object[][] partialDateTimeValues()
  {
    SimpleDateFormat sdf = new SimpleDateFormat("YYYYMMddHHmmssZ");
    GregorianCalendar c = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
    c.setLenient(false);
    c.clear();
    sdf.setCalendar(c);
    c.set(Calendar.HOUR_OF_DAY,23);
    c.set(Calendar.MINUTE,0);
    c.set(Calendar.SECOND,0);
    long time1 = c.getTimeInMillis();
    String format1 = sdf.format(c.getTime());
    c.set(Calendar.HOUR_OF_DAY,00);
    c.set(Calendar.MINUTE,59);
    c.set(Calendar.SECOND,59);
    long time2 = c.getTimeInMillis();
    String format2 = sdf.format(c.getTime());

    return new Object[][] {
      { time1, format1, "0s" },
      { time1, format1, "0m" },
      { time1, format1, "23h" },
      { time2, format2, "59m59s" },
      { time2, format2, "0h59m59s" },
      { time2, format2, "01D01M" },
    };
  }



  /** Generates data for testing partial date and time assertion syntax. */
  @DataProvider
  public Object[][] partialDateTimeSyntaxes()
  {
   //Get the current time.
   GregorianCalendar cal =
           new GregorianCalendar(TimeZone.getTimeZone("UTC"));
   cal.setLenient(false);

    //Get the date today.
    int second = cal.get(Calendar.SECOND);
    int minute = cal.get(Calendar.MINUTE);
    int hour = cal.get(Calendar.HOUR);
    int date = cal.get(Calendar.DATE);
    int month = cal.get(Calendar.MONTH) + 1;
    int year = cal.get(Calendar.YEAR);

    return new Object[][] {
      {"20MM30DD1978YY",false},
      {"02MM29DD2009YY",false},
      {"02MM31DD2010YY",false},
      {"-1s",false},
      {"02M29D2008Y",true},
      {"DDYY",false},
      {"02D",true},
      {"12M",true},
      {"1978Y",true},
      {"0MM",false},
      {"20MM03DD10MM",false},
      {"00s12m13h",true},
      {"00s12m14h1M3D1978Y",true},
      {"1s",true},
      {"12m",true},
      {"23h",true},
      {"61s",false},
      {"60m",false},
      {"24h",false},
      {second+"s",true},
      {minute+"m",true},
      {hour+"h",true},
      {date+"D",true},
      {month+"M",true},
      {year+"Y",true},
      {month+"M"+date+"D",true},
      {year+"Y"+date+"D",true},
      {month+"M"+year+"Y"+date+"D",true}
    };
  }

  private List<Entry> makeEntries() throws Exception
  {
    // Get the current time from the TimeThread. Using the current time from new
    // calendar may fail if the time thread using a stale time.
    long currentTime = TimeThread.getTime();

    return TestCaseUtils.makeEntries(
      "dn: cn=user1,dc=example,dc=com",
      "objectclass: person",
      "objectclass: testoc",
      "cn: user1",
      "sn: user1",
      TIME_ATTR + ": "+ format(currentTime-4000*1000), //more than 1 hour old.
      "",
      "dn: cn=user2,dc=example,dc=com",
      "objectclass: person",
      "objectclass: testoc",
      "cn: user2",
      "sn: user2",
      TIME_ATTR + ": " + format(currentTime-25*3600*1000), //more than  a day old.
      "",
      "dn: cn=user3,dc=example,dc=com",
      "objectclass: person",
      "objectclass: testoc",
      "cn: user3",
      "sn: user3",
      TIME_ATTR + ": " + format(currentTime+4000*1000),  //more than 1 hour in advance.
      "",
      "dn: cn=user4,dc=example,dc=com",
      "objectclass: person",
      "objectclass: testoc",
      "cn: user4",
      "sn: user4",
      TIME_ATTR + ": " + format(currentTime+25*3600*1000),  // more than 1 day in advance
      "",
      "dn: cn=user5,dc=example,dc=com",
      "objectclass: person",
      "objectclass: testoc",
      "cn: user5",
      "sn: user5",
      TIME_ATTR + ": " + format(currentTime), // now.
      "",
      "dn: cn=user6,dc=example,dc=com",
      "objectclass: person",
      "objectclass: testoc2",
      "cn: user6",
      "sn: user6",
      DATE_ATTR + ": 19651101000000Z", // Nov 1st 1965
      "",
      "dn: cn=user7,dc=example,dc=com",
      "objectclass: person",
      "objectclass: testoc2",
      "cn: user7",
      "sn: user7",
      DATE_ATTR + ": 20101104000000Z", // Nov 4th 2010
      "",
      "dn: cn=user8,dc=example,dc=com",
      "objectclass: person",
      "objectclass: testoc2",
      "cn: user8",
      "sn: user8",
      DATE_ATTR + ": 20000101000000Z" // Jan 1st 2000
    );
  }
}
