/*
 * 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 2006-2009 Sun Microsystems, Inc.
 *      Portions Copyright 2011-2015 ForgeRock AS
 */
package org.opends.server.schema;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.SearchScope;
import org.opends.server.TestCaseUtils;
import org.opends.server.api.AttributeSyntax;
import org.opends.server.core.AddOperation;
import org.opends.server.protocols.internal.InternalSearchOperation;
import org.opends.server.protocols.internal.SearchRequest;
import org.opends.server.types.Attribute;
import org.opends.server.types.DN;
import org.opends.server.types.Entry;
import org.opends.server.types.SearchResultEntry;
import org.opends.server.util.RemoveOnceSDKSchemaIsUsed;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import static org.assertj.core.api.Assertions.*;
import static org.opends.server.protocols.internal.InternalClientConnection.*;
import static org.opends.server.protocols.internal.Requests.*;
import static org.testng.Assert.*;

/**
 * Test the LDAPSyntaxDescriptionSyntax.
 */
@RemoveOnceSDKSchemaIsUsed
@SuppressWarnings("javadoc")
public class LDAPSyntaxTest extends AttributeSyntaxTest
{

  /** {@inheritDoc} */
  @Override
  protected AttributeSyntax<?> getRule()
  {
    return new LDAPSyntaxDescriptionSyntax();
  }

  /** {@inheritDoc} */
  @Override
  @DataProvider(name="acceptableValues")
  public Object[][] createAcceptableValues()
  {
      return new Object [][] {
              {"( 2.5.4.3 DESC 'full syntax description' " +
                    "X-9EN ('this' 'is' 'a' 'test'))",
                    false},
              {"( 2.5.4.3 DESC 'full syntax description' " +
                    "(X-name 'this",
                    false},
              {"( 2.5.4.3 DESC 'full syntax description' " +
                    "(X-name 'this'",
                    false},
              {"( 2.5.4.3 DESC 'full syntax description' " +
                    "Y-name 'this')",
                    false},
              {"( 2.5.4.3 DESC 'full syntax description' " +
                    "X-name 'this' 'is')",
                    false},
              {"( 2.5.4.3 DESC 'full syntax description' " +
                    "X-name )",
                    false},
              {"( 2.5.4.3 DESC 'full syntax description' " +
                    "X- ('this' 'is' 'a' 'test'))",
                    false},
              {"( 2.5.4.3 DESC 'full syntax description' " +
                    "X-name ('this' 'is' 'a' 'test') X-name-a 'this' X-name-b ('this')",
                    false},
              {"( 2.5.4.3 DESC 'full syntax description' " +
                    "X-name ('this' 'is' 'a' 'test') X-name-a 'this' X-name-b ('this'",
                    false},
              {"( 2.5.4.3 DESC 'full syntax description' " +
                    "X-name ('this' 'is' 'a' 'test') X-name-a 'this' X-name-b ('this'))))",
                    false},
              {"( 2.5.4.3 DESC 'full syntax description' " +
                    "X-name ('this' 'is' 'a' 'test') X-name-a  X-name-b ('this'))))",
                    false},
              {"( 2.5.4.3 DESC 'full syntax description' " +
                    "X-name ('this' 'is' 'a' 'test') X-name-a  'X-name-b' ('this'))))",
                    false},
              {"( 2.5.4.3 DESC 'full syntax description' " +
                    "X-name ('this' 'is' 'a' 'test') X-name-a 'this' X-name-b ('this'))",
                    true},
              {"( 2.5.4.3 DESC 'full syntax description' " +
                    "X-name ('this' 'is' 'a' 'test') X-name-a 'this' X-name-b ( 'this' ))",
                    true},
              {"( 2.5.4.3 DESC 'full syntax description' " +
                    "X-name ('this' 'is' 'a' 'test') X-name-a 'this' X-name-b ('this' 'that'))",
                    true},
              {"( 2.5.4.3 DESC 'full syntax description' " +
                    "X-name ('this' 'is' 'a' 'test') X-name-a 'this' X-name-b ('this' 'that') )",
                    true},
              {"( 2.5.4.3 DESC 'full syntax description' " +
                    "X-a-_eN_- ('this' 'is' 'a' 'test'))",
                    true},
              {"( 2.5.4.3 DESC 'full syntax description' " +
                    "X-name ('this'))",
                    true},
              {"( 2.5.4.3 DESC 'full syntax description' " +
                    "X-name 'this')",
                    true},
              {"( 2.5.4.3 DESC 'full syntax description' " +
                    "X-name 'this' X-name-a 'test')",
                    true},
              {"( 2.5.4.3 DESC 'full syntax description' )", true},
              {"   (    2.5.4.3    DESC  ' syntax description'    )", true},
              {"( 2.5.4.3 DESC 'Test syntax' X-SCHEMA-FILE '00-core.ldif' )", true},
              {"( 2.5.4.3 DESC 'Test X-SUBST Extensions' X-SUBST '1.3.6.1.4.1.1466.115.121.1.15' )", true},
              {"( 2.5.4.3 DESC 'Test X-SUBST Extensions' X-SUBST '1.3.6.1.4.1.1466.115.121.1.15' X-SCHEMA-FILE '00-core.ldif' )", true},
              {"( 2.5.4.3 DESC 'Test X-SUBST Extensions' X-SCHEMA-FILE '00-core.ldif' X-SUBST '1.3.6.1.4.1.1466.115.121.1.15' )", true},
              {"( 2.5.4.3 DESC 'Test X-PATTERN Extensions' X-PATTERN '[0-9]+' )", true},
              {"( 2.5.4.3 DESC 'Test X-PATTERN Extensions' X-PATTERN '[0-9]+' X-SCHEMA-FILE '00-core.ldif' )", true},
              {"( 2.5.4.3 DESC 'Test X-ENUM Extensions' X-ENUM ( 'black' 'white' ) )", true},
              {"( 2.5.4.3 DESC 'Test X-ENUM Extensions' X-ENUM ( 'white' 'white' ) )", false},
              {"( 2.5.4.3 DESC 'Test X-ENUM Extensions' X-ENUM )", false},
              {"( 2.5.4.3 DESC 'Test X-ENUM Extensions' X-ENUM ( 'black' 'white' ) X-SCHEMA-FILE '00-core.ldif' )", true},
              {"( 2.5.4.3 DESC syntax description )", false},
              {"($%^*&!@ DESC 'syntax description' )", false},
              {"(temp-oid DESC 'syntax description' )", true},
              {"2.5.4.3 DESC 'syntax description' )", false},
              {"(2.5.4.3 DESC 'syntax description' ", false},
      };
  }



  /**
   * Tests whether an implemented syntax can't be substituted by another.
   */
  @Test
  public void testSubstitutionSyntaxForInvalidSubstitution() throws Exception
  {
    try
    {
      //Test if we can substitute a directory string syntax by itself.
      int resultCode = TestCaseUtils.applyModifications(true,
        "dn: cn=schema",
        "changetype: modify",
        "add: ldapsyntaxes",
        "ldapsyntaxes: ( 1.3.6.1.4.1.1466.115.121.1.15 " +
        "DESC 'Replacing DirectorySyntax'   " +
        " X-SUBST '1.3.6.1.4.1.1466.115.121.1.15' )");

      //This is not expected to happen
      assertFalse(resultCode==0);

      //Test if we can substitute a directory string syntax by an undefined.
      resultCode = TestCaseUtils.applyModifications(true,
        "dn: cn=schema",
        "changetype: modify",
        "add: ldapsyntaxes",
        "ldapsyntaxes: ( 1.3.6.1.4.1.1466.115.121.1.15 " +
        "DESC 'Replacing DirectorySyntax'   " +
        " X-SUBST '1.1.1' )");

      //This is not expected to happen
      assertFalse(resultCode==0);


      //Test if we can substitute a core syntax with a user-defined
      //syntax
      addSubtitutionSyntax();
      //Replace the IA5Stringsyntax with the custom syntax we just created.
      resultCode = TestCaseUtils.applyModifications(true,
        "dn: cn=schema",
        "changetype: modify",
        "add: ldapsyntaxes",
        "ldapsyntaxes: ( 1.3.6.1.4.1.1466.115.121.1.26 " +
        "DESC 'Replacing DirectorySyntax'   " +
        " X-SUBST '9.9.9' )");

      //This is not expected to happen
      assertFalse(resultCode==0);
    }
    finally
    {
      deleteSubstitutionSyntax();
    }
  }



  /**
    * Tests whether both the virtual and the newly added real substitution
    * sytanx are available when a search is made for ldapsyntaxes attribute.
    *
    * @throws java.lang.Exception
    */
  @Test
  public void testSubstitutionSyntaxSearch() throws Exception
  {
    try
    {
      addSubtitutionSyntax();

      SearchRequest request = newSearchRequest("cn=schema", SearchScope.WHOLE_SUBTREE, "objectclass=ldapsubentry")
          .addAttribute("ldapsyntaxes");
      InternalSearchOperation searchOperation = getRootConnection().processSearch(request);

      assertEquals(searchOperation.getResultCode(), ResultCode.SUCCESS);
      List<SearchResultEntry> entries = searchOperation.getSearchEntries();
      SearchResultEntry e = entries.get(0);
      //An entry must be returned.
      assertNotNull(e);
      Attribute attr = e.getAttribute("ldapsyntaxes").get(0);
      Iterator<ByteString> iter = attr.iterator();

      //There are other ways of doing it but we will extract the OID
      //from the attribute values and then check to see if our
      //OID is found in the result set or not.
      List<String> syntaxList = new ArrayList<>();
      while(iter.hasNext())
      {
        //parse the OIDs.
        syntaxList.add(getOIDFromLdapSyntax(iter.next().toString()));
      }

      //Check if we find our OID.
      assertThat(syntaxList).contains("9.9.9");
      //DirectoryString.
      assertThat(syntaxList).contains("1.3.6.1.4.1.1466.115.121.1.15");
      //IA5String.
      assertThat(syntaxList).contains("1.3.6.1.4.1.1466.115.121.1.26");
    }
    finally
    {
      deleteSubstitutionSyntax();
    }
  }



   /**
    * Tests whether it is possible to add values after an umimplemented syntax
    * has been subsitutited by DirectoryString syntax.
    *
    * @throws java.lang.Exception
    */
  @Test
   public void testSubsitutionSyntaxAddValues() throws Exception
   {
     try
     {
       addSubtitutionSyntax();
       //Add an attribute with undefined syntax.
       int  resultCode = TestCaseUtils.applyModifications(true,
          "dn: cn=schema",
          "changetype: modify",
          "add: attributetypes",
          "attributetypes: ( test-oid NAME 'test-attr' SYNTAX 9.9.9 )",
          "-",
          "add: objectclasses",
          "objectclasses: ( oc-oid NAME 'testOC' SUP top AUXILIARY MUST test-attr)"
        );
      assertThat(resultCode).isEqualTo(0);
      TestCaseUtils.initializeTestBackend(true);

      TestCaseUtils.addEntry(
      "dn: cn=syntax-test,o=test",
      "objectclass: person",
      "objectclass: testOC",
      "cn: syntax-test",
      "sn: xyz",
      "test-attr: test value for unimplemented syntax");
     }
     finally
     {
       deleteSubstitutionSyntax();
     }
   }



  /**
    * Tests whether it is possible to add values after a regex syntax
    * has been added.
    *
    * @throws java.lang.Exception
    */
  @Test
  public void testRegexSyntaxAddValues() throws Exception
  {
    try
    {
      addRegexSyntax();
      TestCaseUtils.initializeTestBackend(true);

      //This addition should fail because it doesn't match the pattern.
      Entry entry = TestCaseUtils.makeEntry(
      "dn: cn=syntax-test,o=test",
      "objectclass: person",
      "objectclass: testOC",
      "cn: syntax-test",
      "sn: xyz",
      "test-attr-regex: invalid regex");
      AddOperation addOperation = getRootConnection().processAdd(entry);
    assertEquals(addOperation.getResultCode(),
            ResultCode.INVALID_ATTRIBUTE_SYNTAX);

      //This addition should go through.
      TestCaseUtils.addEntry(
        "dn: cn=syntax-test,o=test",
        "objectclass: person",
        "objectclass: testOC",
        "cn: syntax-test",
        "sn: xyz",
        "test-attr-regex: host:0.0.0");
    }
    finally
    {
     deleteRegexSyntax();
    }
  }



  /**
   * Tests the search using regex syntax.
   *
   * @throws java.lang.Exception
   */
  @Test
  public void testRegexSyntaxSearch() throws Exception
  {
    try
    {
      addRegexSyntax();
      TestCaseUtils.initializeTestBackend(true);
      //This addition should go through.
      TestCaseUtils.addEntry(
        "dn: cn=test,o=test",
        "objectclass: person",
        "objectclass: testOC",
        "cn: test",
        "sn: xyz",
        "test-attr-regex: host:0.0.0");

      SearchRequest request =
          newSearchRequest("cn=test,o=test", SearchScope.WHOLE_SUBTREE, "test-attr-regex=host:0.0.0");
      InternalSearchOperation searchOperation = getRootConnection().processSearch(request);
      assertEquals(searchOperation.getResultCode(), ResultCode.SUCCESS);
      List<SearchResultEntry> entries = searchOperation.getSearchEntries();
      SearchResultEntry e = entries.get(0);
      //An entry must be returned.
      assertNotNull(e);
    }
    finally
    {
      deleteRegexSyntax();
    }
  }

  /**
    * Tests whether it is possible to add values after an enum syntax
    * has been added.
    *
    * @throws java.lang.Exception
    */
  @Test
  public void testEnumSyntaxAddValues() throws Exception
  {
    try
    {
      addEnumSyntax();
      TestCaseUtils.initializeTestBackend(true);

      //This addition should fail because it doesn't match the pattern.
      Entry entry = TestCaseUtils.makeEntry(
      "dn: cn=syntax-test,o=test",
      "objectclass: person",
      "objectclass: testOC",
      "cn: syntax-test",
      "sn: xyz",
      "test-attr-enum: arbit-day");
      AddOperation addOperation = getRootConnection().processAdd(entry);
    assertEquals(addOperation.getResultCode(),
            ResultCode.INVALID_ATTRIBUTE_SYNTAX);

      //This addition should go through.
      TestCaseUtils.addEntry(
        "dn: cn=syntax-test,o=test",
        "objectclass: person",
        "objectclass: testOC",
        "cn: syntax-test",
        "sn: xyz",
        "test-attr-enum: sunday");
    }
    finally
    {
      deleteEnumSyntax();
    }
  }



  /**
   * Tests the equality-based search using enum syntax.
   */
  @Test
  public void testEnumSyntaxEqualitySearch() throws Exception
  {
    try
    {
      addEnumSyntax();
      //This addition should go through.
      TestCaseUtils.initializeTestBackend(true);
      TestCaseUtils.addEntry(
        "dn: cn=test,o=test",
        "objectclass: person",
        "objectclass: testOC",
        "cn: test",
        "sn: xyz",
        "test-attr-enum: wednesday");

      SearchRequest request = newSearchRequest("cn=test,o=test", SearchScope.WHOLE_SUBTREE, "test-attr-enum=wednesday");
      InternalSearchOperation searchOperation = getRootConnection().processSearch(request);
      assertEquals(searchOperation.getResultCode(), ResultCode.SUCCESS);
      List<SearchResultEntry> entries = searchOperation.getSearchEntries();
      SearchResultEntry e = entries.get(0);
      //An entry must be returned.
      assertNotNull(e);
    }
    finally
    {
      deleteEnumSyntax();
    }
  }

  /**
   * Tests the ordering-based search using enum syntax.
   */
  @Test
  public void testEnumSyntaxOrderingSearch() throws Exception
  {
    try
    {
      addEnumSyntax();
      TestCaseUtils.initializeTestBackend(true);
      //This addition should go through.
      TestCaseUtils.addEntries(
        "dn: cn=test1,o=test",
        "objectclass: person",
        "objectclass: testOC",
        "cn: test1",
        "sn: xyz",
        "test-attr-enum: sunday",
        "",
        "dn: cn=test2,o=test",
        "objectclass: person",
        "objectclass: testOC",
        "cn: test2",
        "sn: xyz",
        "test-attr-enum: monday",
        "",
        "dn: cn=test3,o=test",
        "objectclass: person",
        "objectclass: testOC",
        "cn: test3",
        "sn: xyz",
        "test-attr-enum: tuesday");

      SearchRequest request = newSearchRequest("o=test", SearchScope.WHOLE_SUBTREE, "test-attr-enum>=tuesday");
      InternalSearchOperation searchOperation = getRootConnection().processSearch(request);
      assertEquals(searchOperation.getResultCode(), ResultCode.SUCCESS);
      List<SearchResultEntry> entries = searchOperation.getSearchEntries();
      assertThat(entries).as("expected one entry to be returned").isNotEmpty();
      assertThat(entries.get(0).getName()).isEqualTo(DN.valueOf("cn=test1,o=test"));
    }
    finally
    {
      deleteEnumSyntax();
    }
  }



  /** Parses the OID from the syntax defitions. */
  private String getOIDFromLdapSyntax(String valueStr)
  {
    int pos    = 0;
    int length = valueStr.length();

    while (pos < length && valueStr.charAt(pos) == ' ')
    {
      pos++;
    }
    // The next character must be an open parenthesis.  If it is not, then that
    // is an error.
    assertEquals(valueStr.charAt(pos++), '(');

    // Skip over any spaces immediately following the opening parenthesis.
    while (pos < length && valueStr.charAt(pos) == ' ')
    {
      pos++;
    }
    int oidStartPos = pos;

    while (pos < length
        && valueStr.charAt(pos) != ' '
        && valueStr.charAt(pos) != ')')
    {
      pos++;
    }
    return valueStr.substring(oidStartPos, pos);
  }


  /** Adds a substitutionsyntax to the schema. */
  private void addSubtitutionSyntax() throws Exception
  {
    //Add the substitution syntax for an unimplemented syntax.
    int resultCode = TestCaseUtils.applyModifications(true,
    "dn: cn=schema",
    "changetype: modify",
    "add: ldapsyntaxes",
    "ldapsyntaxes: ( 9.9.9 DESC 'Unimplemented Syntax'   " +
    " X-SUBST '1.3.6.1.4.1.1466.115.121.1.15' )");
    assertThat(resultCode).isEqualTo(0);
  }

  /** Deletes the substitutionSyntax from the schema. */
  private void deleteSubstitutionSyntax() throws Exception
  {
    //delete the substitution syntax.
    int resultCode = TestCaseUtils.applyModifications(true,
    "dn: cn=schema",
    "changetype: modify",
    "delete: ldapsyntaxes",
    "ldapsyntaxes: ( 9.9.9 DESC 'Unimplemented Syntax'   " +
    " X-SUBST '1.3.6.1.4.1.1466.115.121.1.15' )");
    assertThat(resultCode).isEqualTo(0);
  }

  /** Adds a regex syntax to the schema. */
  private void addRegexSyntax() throws Exception
  {
    //Add the substitution syntax for an unimplemented syntax.
    int resultCode = TestCaseUtils.applyModifications(true,
    "dn: cn=schema",
    "changetype: modify",
    "add: ldapsyntaxes",
    "ldapSyntaxes: ( 1.1.1 DESC 'Host and Port in the format of HOST:PORT'  " +
            "X-PATTERN '^[a-z-A-Z]+:[0-9.]+\\d$' )");
    assertThat(resultCode).isEqualTo(0);

    resultCode = TestCaseUtils.applyModifications(true,
          "dn: cn=schema",
          "changetype: modify",
          "add: attributetypes",
          "attributetypes: ( test-attr-oid NAME 'test-attr-regex' SYNTAX 1.1.1 )",
          "-",
          "add: objectclasses",
          "objectclasses: ( oc-oid NAME 'testOC' SUP top AUXILIARY MUST test-attr-regex)"
        );
    assertThat(resultCode).isEqualTo(0);
  }

  /** Deletes the regex syntax from the schema. */
  private void deleteRegexSyntax() throws Exception
  {
    //delete the substitution syntax.
    int resultCode = TestCaseUtils.applyModifications(true,
      "dn: cn=schema",
      "changetype: modify",
      "delete: objectclasses",
      "objectclasses: ( oc-oid NAME 'testOC' SUP top AUXILIARY MUST test-attr-regex)",
      "-",
      "delete: attributetypes",
      "attributetypes: ( test-attr-oid NAME 'test-attr-regex' SYNTAX 1.1.1 )"
    );
    assertThat(resultCode).isEqualTo(0);

    resultCode = TestCaseUtils.applyModifications(true,
    "dn: cn=schema",
    "changetype: modify",
    "delete: ldapsyntaxes",
    "ldapSyntaxes: ( 1.1.1 DESC 'Host and Port in the format of HOST:PORT'  " +
            "X-PATTERN '^[a-z-A-Z]+:[0-9.]+\\d$' )");
    assertThat(resultCode).isEqualTo(0);
  }

  /** Adds an enum syntax to the schema. */
  private void addEnumSyntax() throws Exception
  {
    //Add the enum syntax.
    int resultCode = TestCaseUtils.applyModifications(true,
    "dn: cn=schema",
    "changetype: modify",
    "add: ldapsyntaxes",
    "ldapSyntaxes: ( 3.3.3  DESC 'Day Of The Week'  " +
            "X-ENUM  ( 'monday' 'tuesday'   'wednesday'  'thursday'  'friday'  'saturday' 'sunday') )");
    assertThat(resultCode).isEqualTo(0);

    resultCode = TestCaseUtils.applyModifications(true,
          "dn: cn=schema",
          "changetype: modify",
          "add: attributetypes",
          "attributetypes: ( test-attr-oid NAME 'test-attr-enum' SYNTAX 3.3.3 )",
          "-",
          "add: objectclasses",
          "objectclasses: ( oc-oid NAME 'testOC' SUP top AUXILIARY MUST test-attr-enum)"
        );
    assertThat(resultCode).isEqualTo(0);
  }

  /** Deletes the enum syntax from the schema. */
  private void deleteEnumSyntax() throws Exception
  {
    int resultCode = TestCaseUtils.applyModifications(true,
      "dn: cn=schema",
      "changetype: modify",
      "delete: objectclasses",
      "objectclasses: ( oc-oid NAME 'testOC' SUP top AUXILIARY MUST test-attr-enum)",
      "-",
      "delete: attributetypes",
      "attributetypes: ( test-attr-oid NAME 'test-attr-enum' SYNTAX 3.3.3 )"
    );
    assertThat(resultCode).isEqualTo(0);

    resultCode = TestCaseUtils.applyModifications(true,
    "dn: cn=schema",
    "changetype: modify",
    "delete: ldapsyntaxes",
    "ldapSyntaxes: ( 3.3.3  DESC 'Day Of The Week'  " +
            "X-ENUM  ( 'monday' 'tuesday'   'wednesday'  'thursday'  'friday'  'saturday' 'sunday') )");
    assertThat(resultCode).isEqualTo(0);
  }
}
