/*
 * 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 2014-2015 ForgeRock AS
 */
package org.opends.server.backends.pluggable;

import java.io.ByteArrayInputStream;
import java.util.List;
import java.util.Map;

import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ByteStringBuilder;
import org.opends.server.DirectoryServerTestCase;
import org.opends.server.TestCaseUtils;
import org.opends.server.core.DirectoryServer;
import org.opends.server.types.*;
import org.opends.server.util.LDIFReader;
import org.opends.server.util.StaticUtils;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import static org.assertj.core.api.Assertions.*;
import static org.opends.server.util.StaticUtils.*;
import static org.testng.Assert.*;

/**
 * DnKeyFormat Tester.
 */
@SuppressWarnings("javadoc")
public class TestDnKeyFormat extends DirectoryServerTestCase {

  private static final String ldifString =
    "dn: uid=user.1,ou=People,dc=example,dc=com\n"
      + "objectClass: top\n"
      + "objectClass: person\n"
      + "objectClass: organizationalPerson\n"
      + "objectClass: inetOrgPerson\n"
      + "uid: user.1\n"
      + "homePhone: 951-245-7634\n"
      + "description: This is the description for Aaccf Amar.\n"
      + "st: NC\n"
      + "mobile: 027-085-0537\n"
      + "postalAddress: Aaccf Amar$17984 Thirteenth Street"
      + "$Rockford, NC  85762\n"
      + "mail: user.1@example.com\n"
      + "cn: Aaccf Amar\n"
      + "l: Rockford\n"
      + "pager: 508-763-4246\n"
      + "street: 17984 Thirteenth Street\n"
      + "telephoneNumber: 216-564-6748\n"
      + "employeeNumber: 1\n"
      + "sn: Amar\n"
      + "givenName: Aaccf\n"
      + "postalCode: 85762\n"
      + "userPassword: password\n"
      + "initials: AA\n"
      + "\n"
      + "dn:: b3U95Za25qWt6YOoLG89QWlyaXVz\n"
      + "# dn:: ou=<JapaneseOU>,o=Airius\n"
      + "objectclass: top\n"
      + "objectclass: organizationalUnit\n"
      + "ou:: 5Za25qWt6YOo\n"
      + "# ou:: <JapaneseOU>\n"
      + "ou;lang-ja:: 5Za25qWt6YOo\n"
      + "# ou;lang-ja:: <JapaneseOU>\n"
      + "ou;lang-ja;phonetic:: 44GI44GE44GO44KH44GG44G2\n"
      + "# ou;lang-ja:: <JapaneseOU_in_phonetic_representation>\n"
      + "ou;lang-en: Sales\n"
      + "description: Japanese office\n"
      + "\n"
      + "dn:: dWlkPXJvZ2FzYXdhcmEsb3U95Za25qWt6YOoLG89QWlyaXVz\n"
      + "# dn:: uid=<uid>,ou=<JapaneseOU>,o=Airius\n"
      + "userpassword: {SHA}O3HSv1MusyL4kTjP+HKI5uxuNoM=\n"
      + "objectclass: top\n"
      + "objectclass: person\n"
      + "objectclass: organizationalPerson\n"
      + "objectclass: inetOrgPerson\n"
      + "uid: rogasawara\n"
      + "mail: rogasawara@airius.co.jp\n"
      + "givenname;lang-ja:: 44Ot44OJ44OL44O8\n"
      + "# givenname;lang-ja:: <JapaneseGivenname>\n"
      + "sn;lang-ja:: 5bCP56yg5Y6f\n"
      + "# sn;lang-ja:: <JapaneseSn>\n"
      + "cn;lang-ja:: 5bCP56yg5Y6fIOODreODieODi+ODvA==\n"
      + "# cn;lang-ja:: <JapaneseCn>\n"
      + "title;lang-ja:: 5Za25qWt6YOoIOmDqOmVtw==\n"
      + "# title;lang-ja:: <JapaneseTitle>\n"
      + "preferredlanguage: ja\n"
      + "givenname:: 44Ot44OJ44OL44O8\n"
      + "# givenname:: <JapaneseGivenname>\n"
      + "sn:: 5bCP56yg5Y6f\n"
      + "# sn:: <JapaneseSn>\n"
      + "cn:: 5bCP56yg5Y6fIOODreODieODi+ODvA==\n"
      + "# cn:: <JapaneseCn>\n"
      + "title:: 5Za25qWt6YOoIOmDqOmVtw==\n"
      + "# title:: <JapaneseTitle>\n"
      + "givenname;lang-ja;phonetic:: 44KN44Gp44Gr44O8\n"
      + "# givenname;lang-ja;phonetic:: "
      + "<JapaneseGivenname_in_phonetic_representation_kana>\n"
      + "sn;lang-ja;phonetic:: 44GK44GM44GV44KP44KJ\n"
      + "# sn;lang-ja;phonetic:: "
      + "<JapaneseSn_in_phonetic_representation_kana>\n"
      + "cn;lang-ja;phonetic:: 44GK44GM44GV44KP44KJIOOCjeOBqeOBq+ODvA==\n"
      + "# cn;lang-ja;phonetic:: "
      + "<JapaneseCn_in_phonetic_representation_kana>\n"
      + "title;lang-ja;phonetic:: "
      + ""
      + "44GI44GE44GO44KH44GG44G2IOOBtuOBoeOCh+OBhg==\n"
      + "# title;lang-ja;phonetic::\n"
      + "# <JapaneseTitle_in_phonetic_representation_kana>\n"
      + "givenname;lang-en: Rodney\n"
      + "sn;lang-en: Ogasawara\n"
      + "cn;lang-en: Rodney Ogasawara\n"
      + "title;lang-en: Sales, Director\n" + "\n" + "";


  /**
   * Encodes this entry using the V3 encoding.
   *
   * @param  buffer  The buffer to encode into.
   *
   * @throws  DirectoryException  If a problem occurs while attempting
   *                              to encode the entry.
   */
  private void encodeV1(Entry entry, ByteStringBuilder buffer)
         throws DirectoryException
  {
    // The version number will be one byte.
    buffer.appendByte(0x01);

    // TODO: Can we encode the DN directly into buffer?
    byte[] dnBytes  = getBytes(entry.getName().toString());
    buffer.appendBERLength(dnBytes.length);
    buffer.appendBytes(dnBytes);


    // Encode number of OCs and 0 terminated names.
    int i = 1;
    ByteStringBuilder bsb = new ByteStringBuilder();
    for (String ocName : entry.getObjectClasses().values())
    {
      bsb.appendUtf8(ocName);
      if(i < entry.getObjectClasses().values().size())
      {
        bsb.appendByte(0x00);
      }
      i++;
    }
    buffer.appendBERLength(bsb.length());
    buffer.appendBytes(bsb);


    // Encode the user attributes in the appropriate manner.
    encodeV1Attributes(buffer, entry.getUserAttributes());


    // The operational attributes will be encoded in the same way as
    // the user attributes.
    encodeV1Attributes(buffer, entry.getOperationalAttributes());
  }

  private void encodeV1Attributes(ByteStringBuilder buffer,
                                Map<AttributeType,List<Attribute>> attributes)
  {
    int numAttributes = 0;

    // First count how many attributes are there to encode.
    for (List<Attribute> attrList : attributes.values())
    {
      for (Attribute a : attrList)
      {
        if (a.isVirtual() || a.isEmpty())
        {
          continue;
        }

        numAttributes++;
      }
    }

    // Encoded one-to-five byte number of attributes
    buffer.appendBERLength(numAttributes);

    append(buffer, attributes);
  }

    /**
   * Encodes this entry using the V3 encoding.
   *
   * @param  buffer  The buffer to encode into.
   *
   * @throws  DirectoryException  If a problem occurs while attempting
   *                              to encode the entry.
   */
  private void encodeV2(Entry entry, ByteStringBuilder buffer,
                        EntryEncodeConfig config)
         throws DirectoryException
  {
    // The version number will be one byte.
    buffer.appendByte(0x02);

    // Get the encoded respresentation of the config.
    config.encode(buffer);

    // If we should include the DN, then it will be encoded as a
    // one-to-five byte length followed by the UTF-8 byte
    // representation.
    if (! config.excludeDN())
    {
      // TODO: Can we encode the DN directly into buffer?
      byte[] dnBytes  = getBytes(entry.getName().toString());
      buffer.appendBERLength(dnBytes.length);
      buffer.appendBytes(dnBytes);
    }


    // Encode the object classes in the appropriate manner.
    if (config.compressObjectClassSets())
    {
      config.getCompressedSchema().encodeObjectClasses(buffer,
          entry.getObjectClasses());
    }
    else
    {
      // Encode number of OCs and 0 terminated names.
      int i = 1;
      ByteStringBuilder bsb = new ByteStringBuilder();
      for (String ocName : entry.getObjectClasses().values())
      {
        bsb.appendUtf8(ocName);
        if(i < entry.getObjectClasses().values().size())
        {
          bsb.appendByte(0x00);
        }
        i++;
      }
      buffer.appendBERLength(bsb.length());
      buffer.appendBytes(bsb);
    }


    // Encode the user attributes in the appropriate manner.
    encodeV2Attributes(buffer, entry.getUserAttributes(), config);


    // The operational attributes will be encoded in the same way as
    // the user attributes.
    encodeV2Attributes(buffer, entry.getOperationalAttributes(), config);
  }

  private void encodeV2Attributes(ByteStringBuilder buffer,
                                Map<AttributeType,List<Attribute>> attributes,
                                EntryEncodeConfig config)
      throws DirectoryException
  {
    int numAttributes = 0;

    // First count how many attributes are there to encode.
    for (List<Attribute> attrList : attributes.values())
    {
      for (Attribute a : attrList)
      {
        if (a.isVirtual() || a.isEmpty())
        {
          continue;
        }

        numAttributes++;
      }
    }

    // Encoded one-to-five byte number of attributes
    buffer.appendBERLength(numAttributes);

    if (config.compressAttributeDescriptions())
    {
      for (List<Attribute> attrList : attributes.values())
      {
        for (Attribute a : attrList)
        {
          if (a.isVirtual() || a.isEmpty())
          {
            continue;
          }

          ByteStringBuilder bsb = new ByteStringBuilder();
          config.getCompressedSchema().encodeAttribute(bsb, a);
          buffer.appendBERLength(bsb.length());
          buffer.appendBytes(bsb);
        }
      }
    }
    else
    {
      append(buffer, attributes);
    }
  }

  /**
   * The attributes will be encoded as a sequence of:
   * - A UTF-8 byte representation of the attribute name.
   * - A zero delimiter
   * - A one-to-five byte number of values for the attribute
   * - A sequence of:
   *   - A one-to-five byte length for the value
   *   - A UTF-8 byte representation for the value
   */
  private void append(ByteStringBuilder buffer,
      Map<AttributeType, List<Attribute>> attributes)
  {
    for (List<Attribute> attrList : attributes.values())
    {
      for (Attribute a : attrList)
      {
        byte[] nameBytes = getBytes(a.getNameWithOptions());
        buffer.appendBytes(nameBytes);
        buffer.appendByte(0x00);

        buffer.appendBERLength(a.size());
        for (ByteString v : a)
        {
          buffer.appendBERLength(v.length());
          buffer.appendBytes(v);
        }
      }
    }
  }

  /**
   * Test entry.
   *
   * @throws Exception
   *           If the test failed unexpectedly.
   */
  @Test
  public void testEntryToAndFromDatabase() throws Exception {
    ensureServerIsUpAndRunning();

    // Convert the test LDIF string to a byte array
    byte[] originalLDIFBytes = StaticUtils.getBytes(ldifString);

    try (final LDIFReader reader = new LDIFReader(new LDIFImportConfig(new ByteArrayInputStream(originalLDIFBytes))))
    {
      Entry entryBefore, entryAfter;
      while ((entryBefore = reader.readEntry(false)) != null) {
        ByteString bytes = ID2Entry.entryToDatabase(entryBefore,
            new DataConfig(false, false, null));

        entryAfter = ID2Entry.entryFromDatabase(bytes,
                          DirectoryServer.getDefaultCompressedSchema());

        // check DN and number of attributes
        assertEquals(entryBefore.getAttributes().size(), entryAfter
            .getAttributes().size());

        assertEquals(entryBefore.getName(), entryAfter.getName());

        // check the object classes were not changed
        for (String ocBefore : entryBefore.getObjectClasses().values()) {
          ObjectClass objectClass = DirectoryServer.getObjectClass(ocBefore
              .toLowerCase());
          if (objectClass == null) {
            objectClass = DirectoryServer.getDefaultObjectClass(ocBefore);
          }
          String ocAfter = entryAfter.getObjectClasses().get(objectClass);

          assertEquals(ocBefore, ocAfter);
        }

        // check the user attributes were not changed
        for (AttributeType attrType : entryBefore.getUserAttributes()
            .keySet()) {
          List<Attribute> listBefore = entryBefore.getAttribute(attrType);
          List<Attribute> listAfter = entryAfter.getAttribute(attrType);

          assertNotNull(listAfter);
          assertEquals(listBefore.size(), listAfter.size());

          for (Attribute attrBefore : listBefore) {
            boolean found = false;

            for (Attribute attrAfter : listAfter) {
              if (attrAfter.optionsEqual(attrBefore.getOptions())) {
                // Found the corresponding attribute

                assertEquals(attrBefore, attrAfter);
                found = true;
              }
            }

            assertTrue(found);
          }
        }
      }
    }
  }

  /**
   * Tests the entry encoding and decoding process the version 1 encoding.
   *
   * @throws Exception
   *           If the test failed unexpectedly.
   */
  @Test
  public void testEntryToAndFromDatabaseV1() throws Exception {
    ensureServerIsUpAndRunning();

    // Convert the test LDIF string to a byte array
    byte[] originalLDIFBytes = StaticUtils.getBytes(ldifString);

    try (final LDIFReader reader = new LDIFReader(new LDIFImportConfig(new ByteArrayInputStream(originalLDIFBytes))))
    {
      Entry entryBefore, entryAfterV1;
      while ((entryBefore = reader.readEntry(false)) != null) {
        ByteStringBuilder bsb = new ByteStringBuilder();
        encodeV1(entryBefore, bsb);
        entryAfterV1 = Entry.decode(bsb.asReader());

        assertEquals(entryBefore, entryAfterV1);
      }
    }
  }

  /**
   * Retrieves a set of entry encode configurations that may be used to test the
   * entry encoding and decoding capabilities.
   */
  @DataProvider(name = "encodeConfigs")
  public Object[][] getEntryEncodeConfigs()
  {
    return new Object[][]
    {
      new Object[] { new EntryEncodeConfig() },
      new Object[] { new EntryEncodeConfig(false, false, false) },
      new Object[] { new EntryEncodeConfig(true, false, false) },
      new Object[] { new EntryEncodeConfig(false, true, false) },
      new Object[] { new EntryEncodeConfig(false, false, true) },
      new Object[] { new EntryEncodeConfig(true, true, false) },
      new Object[] { new EntryEncodeConfig(true, false, true) },
      new Object[] { new EntryEncodeConfig(false, true, true) },
      new Object[] { new EntryEncodeConfig(true, true, true) },
    };
  }

  /**
   * Tests the entry encoding and decoding process the version 1 encoding.
   *
   * @throws Exception
   *           If the test failed unexpectedly.
   */
  @Test(dataProvider = "encodeConfigs")
  public void testEntryToAndFromDatabaseV2(EntryEncodeConfig config)
         throws Exception {
    ensureServerIsUpAndRunning();

    // Convert the test LDIF string to a byte array
    byte[] originalLDIFBytes = StaticUtils.getBytes(ldifString);

    try (final LDIFReader reader = new LDIFReader(new LDIFImportConfig(new ByteArrayInputStream(originalLDIFBytes))))
    {
      Entry entryBefore, entryAfterV2;
      while ((entryBefore = reader.readEntry(false)) != null) {
        ByteStringBuilder bsb = new ByteStringBuilder();
        encodeV2(entryBefore, bsb, config);
        entryAfterV2 = Entry.decode(bsb.asReader());
        if (config.excludeDN())
        {
          entryAfterV2.setDN(entryBefore.getName());
        }
        assertEquals(entryBefore, entryAfterV2);
      }
    }
  }

  /**
   * Tests the entry encoding and decoding process the version 1 encoding.
   *
   * @throws Exception
   *           If the test failed unexpectedly.
   */
  @Test(dataProvider = "encodeConfigs")
  public void testEntryToAndFromDatabaseV3(EntryEncodeConfig config)
         throws Exception {
    ensureServerIsUpAndRunning();

    // Convert the test LDIF string to a byte array
    byte[] originalLDIFBytes = StaticUtils.getBytes(ldifString);

    try (final LDIFReader reader = new LDIFReader(new LDIFImportConfig(new ByteArrayInputStream(originalLDIFBytes))))
    {
      Entry entryBefore, entryAfterV3;
      while ((entryBefore = reader.readEntry(false)) != null) {
        ByteStringBuilder bsb = new ByteStringBuilder();
        entryBefore.encode(bsb, config);
        entryAfterV3 = Entry.decode(bsb.asReader());
        if (config.excludeDN())
        {
          entryAfterV3.setDN(entryBefore.getName());
        }
        assertEquals(entryBefore, entryAfterV3);
      }
    }
  }

  @DataProvider
  private Object[][] findDnKeyParentData()
  {
    return new Object[][]
    {
      // dn, expected length of parent
      { "dc=example", 0 },
      { "dc=example,dc=com", 7 },
      { "dc=example,dc=com\\,org", 11 },
    };
  }

  @Test(dataProvider="findDnKeyParentData")
  public void testFindDnKeyParent(String dn, int expectedLength) throws Exception
  {
    ensureServerIsUpAndRunning();
    ByteString dnKey = DnKeyFormat.dnToDNKey(DN.valueOf(dn), 0);
    assertThat(DnKeyFormat.findDNKeyParent(dnKey)).isEqualTo(expectedLength);
  }

  private void ensureServerIsUpAndRunning() throws Exception
  {
    TestCaseUtils.startServer();
  }
}
