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

import static org.assertj.core.api.Assertions.assertThat;
import static org.forgerock.opendj.ldap.ModificationType.ADD;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.opends.server.protocols.internal.InternalClientConnection.getRootConnection;
import static org.opends.server.protocols.internal.Requests.newSearchRequest;
import static org.opends.server.types.Attributes.create;
import static org.opends.server.types.IndexType.*;
import static org.opends.server.util.CollectionUtils.*;
import static org.testng.Assert.*;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ConditionResult;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.SearchScope;
import org.forgerock.util.Reject;
import org.opends.server.DirectoryServerTestCase;
import org.opends.server.TestCaseUtils;
import org.opends.server.admin.std.meta.BackendIndexCfgDefn.IndexType;
import org.opends.server.admin.std.meta.BackendVLVIndexCfgDefn.Scope;
import org.opends.server.admin.std.server.BackendIndexCfg;
import org.opends.server.admin.std.server.BackendVLVIndexCfg;
import org.opends.server.admin.std.server.PluggableBackendCfg;
import org.opends.server.api.Backend.BackendOperation;
import org.opends.server.api.ClientConnection;
import org.opends.server.backends.RebuildConfig;
import org.opends.server.backends.VerifyConfig;
import org.opends.server.backends.RebuildConfig.RebuildMode;
import org.opends.server.backends.pluggable.spi.AccessMode;
import org.opends.server.backends.pluggable.spi.ReadOnlyStorageException;
import org.opends.server.backends.pluggable.spi.ReadOperation;
import org.opends.server.backends.pluggable.spi.ReadableTransaction;
import org.opends.server.backends.pluggable.spi.Storage;
import org.opends.server.backends.pluggable.spi.TreeName;
import org.opends.server.backends.pluggable.spi.WriteOperation;
import org.opends.server.backends.pluggable.spi.WriteableTransaction;
import org.opends.server.core.DirectoryServer;
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.protocols.internal.InternalSearchOperation;
import org.opends.server.protocols.internal.SearchRequest;
import org.opends.server.types.AttributeType;
import org.opends.server.types.BackupConfig;
import org.opends.server.types.BackupDirectory;
import org.opends.server.types.DN;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.Entry;
import org.opends.server.types.LDIFExportConfig;
import org.opends.server.types.LDIFImportConfig;
import org.opends.server.types.Modification;
import org.opends.server.types.RestoreConfig;
import org.opends.server.types.SearchResultEntry;
import org.opends.server.workflowelement.localbackend.LocalBackendSearchOperation;
import org.testng.Reporter;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

/**
 * Unit tests for pluggable backend implementations. The test methods have side-effects and must be run in-order.
 */
@SuppressWarnings("javadoc")
@Test(groups = { "precommit", "pluggablebackend" }, sequential = true)
public abstract class PluggableBackendImplTestCase<C extends PluggableBackendCfg> extends DirectoryServerTestCase
{
  private BackendImpl<C> backend;
  private List<Entry> topEntries;
  private List<Entry> entries;
  private List<Entry> workEntries;
  private DN testBaseDN;
  private DN dnToDel;
  private DN searchDN;
  private DN badEntryDN;
  private String backupID;

  private Map<String, IndexType[]> backendIndexes = new HashMap<>();
  {
    backendIndexes.put("entryUUID", new IndexType[] { IndexType.EQUALITY });
    backendIndexes.put("cn", new IndexType[] { IndexType.SUBSTRING });
    backendIndexes.put("sn", new IndexType[] { IndexType.PRESENCE, IndexType.EQUALITY, IndexType.SUBSTRING });
    backendIndexes.put("uid", new IndexType[] { IndexType.EQUALITY });
    backendIndexes.put("telephoneNumber", new IndexType[] { IndexType.EQUALITY, IndexType.SUBSTRING });
    backendIndexes.put("mail", new IndexType[] { IndexType.SUBSTRING });
  }

  private String[] backendVlvIndexes = { "people" };
  private AttributeType modifyAttribute;
  private final ByteString modifyValue = ByteString.valueOfUtf8("foo");
  private BackupDirectory backupDirectory;

  /**
   * Factory method for creating a new unconfigured backend instance.
   *
   * @return the unconfigured backend instance.
   * @see #setUp()
   */
  protected abstract BackendImpl<C> createBackend();

  /**
   * Factory method for creating a new backend configuration. All methods specific to the sub-class
   * should be stubbed out.
   *
   * @return the new backend configuration.
   * @see #setUp()
   */
  protected abstract C createBackendCfg();

  @BeforeClass
  public void setUp() throws Exception
  {
    // Need the schema to be available, so make sure the server is started.
    TestCaseUtils.startServer();

    testBaseDN = DN.valueOf("dc=test,dc=com");

    C backendCfg = createBackendCfg();
    when(backendCfg.dn()).thenReturn(testBaseDN);
    when(backendCfg.getBaseDN()).thenReturn(newTreeSet(testBaseDN));
    when(backendCfg.listBackendIndexes()).thenReturn(backendIndexes.keySet().toArray(new String[0]));
    when(backendCfg.listBackendVLVIndexes()).thenReturn(backendVlvIndexes);

    for (Map.Entry<String, IndexType[]> index : backendIndexes.entrySet())
    {
      final String attributeName = index.getKey().toLowerCase();
      final AttributeType attribute = DirectoryServer.getAttributeTypeOrNull(attributeName);
      Reject.ifNull(attribute, "Attribute type '" + attributeName + "' doesn't exists.");

      BackendIndexCfg indexCfg = mock(BackendIndexCfg.class);
      when(indexCfg.getIndexType()).thenReturn(newTreeSet(index.getValue()));
      when(indexCfg.getAttribute()).thenReturn(attribute);
      when(indexCfg.getIndexEntryLimit()).thenReturn(4000);
      when(indexCfg.getSubstringLength()).thenReturn(6);
      when(backendCfg.getBackendIndex(index.getKey())).thenReturn(indexCfg);
    }

    BackendVLVIndexCfg vlvIndexCfg = mock(BackendVLVIndexCfg.class);
    when(vlvIndexCfg.getName()).thenReturn("people");
    when(vlvIndexCfg.getBaseDN()).thenReturn(testBaseDN);
    when(vlvIndexCfg.getFilter()).thenReturn("(objectClass=person)");
    when(vlvIndexCfg.getScope()).thenReturn(Scope.WHOLE_SUBTREE);
    when(vlvIndexCfg.getSortOrder()).thenReturn("sn -employeeNumber +uid");
    when(backendCfg.getBackendVLVIndex(backendVlvIndexes[0])).thenReturn(vlvIndexCfg);

    backend = createBackend();
    backend.setBackendID(backendCfg.getBackendId());
    backend.configureBackend(backendCfg, DirectoryServer.getInstance().getServerContext());
    backend.openBackend();

    topEntries = TestCaseUtils.makeEntries(
                "dn: " + testBaseDN,
                "objectclass: top",
                "objectclass: domain",
                "",
                "dn: ou=People," + testBaseDN,
                "objectclass: top",
                "objectclass: organizationalUnit",
                "ou: People");
    entries = TestCaseUtils.makeEntries(
                "dn: uid=user.0,ou=People," + testBaseDN,
                "objectClass: top",
                "objectClass: person",
                "objectClass: organizationalPerson",
                "objectClass: inetOrgPerson",
                "givenName: Aaccf",
                "sn: Amar",
                "cn: Aaccf Amar",
                "initials: AQA",
                "employeeNumber: 0",
                "uid: user.0",
                "mail: user.0@example.com",
                "userPassword: password",
                "telephoneNumber: 380-535-2354",
                "homePhone: 707-626-3913",
                "pager: 456-345-7750",
                "mobile: 366-674-7274",
                "street: 99262 Eleventh Street",
                "l: Salem",
                "st: NM",
                "postalCode: 36530",
                "postalAddress: Aaccf Amar$99262 Eleventh Street$Salem, NM  36530",
                "description: This is the description for Aaccf Amar.",
                "",
                "dn: uid=user.1,ou=People," + testBaseDN,
                "objectClass: top",
                "objectClass: person",
                "objectClass: organizationalPerson",
                "objectClass: inetOrgPerson",
                "givenName: Aaren",
                "givenName;lang-fr: test2",
                "givenName;lang-cn: test2",
                "givenName;lang-es: test3",
                "sn: Atp",
                "cn: Aaren Atp",
                "initials: APA",
                "employeeNumber: 1",
                "uid: user.1",
                "mail: user.1@example.com",
                "userPassword: password",
                "telephoneNumber: 643-278-6134",
                "homePhone: 546-786-4099",
                "pager: 508-261-3187",
                "mobile: 377-267-7824",
                "carLicense: 377-267-7824",
                "street: 78113 Fifth Street",
                "l: Chico",
                "st: TN",
                "postalCode: 72322",
                "postalAddress: Aaren Atp$78113 Fifth Street$Chico, TN  72322",
                "description: This is the description for Aaren Atp.",
                "",
                "dn: uid=user.2,ou=People," + testBaseDN,
                "objectClass: top",
                "objectClass: person",
                "objectClass: organizationalPerson",
                "objectClass: inetOrgPerson",
                "givenName: Aarika",
                "sn: Atpco",
                "cn: Aarika Atpco",
                "initials: ARA",
                "employeeNumber: 2",
                "uid: user.2",
                "mail: user.2@example.com",
                "userPassword: password",
                "telephoneNumber: 547-504-3498",
                "homePhone: 955-899-7308",
                "pager: 710-832-9316",
                "mobile: 688-388-4525",
                "carLicense: 688-388-4525",
                "street: 59208 Elm Street",
                "l: Youngstown",
                "st: HI",
                "postalCode: 57377",
                "postalAddress: Aarika Atpco$59208 Elm Street$Youngstown, HI  57377",
                "description: This is the description for Aarika Atpco.",
                "",
                "dn: uid=user.3,ou=People," + testBaseDN,
                "objectClass: top",
                "objectClass: person",
                "objectClass: organizationalPerson",
                "objectClass: inetOrgPerson",
                "givenName: Aaron",
                "sn: Atrc",
                "cn: Aaron Atrc",
                "initials: AIA",
                "employeeNumber: 3",
                "uid: user.3",
                "mail: user.3@example.com",
                "userPassword: password",
                "telephoneNumber: 128-108-4939",
                "homePhone: 512-782-9966",
                "pager: 322-646-5118",
                "mobile: 360-957-9137",
                "carLicense: 360-957-9137",
                "street: 25074 Hill Street",
                "l: Toledo",
                "st: OR",
                "postalCode: 55237",
                "postalAddress: Aaron Atrc$25074 Hill Street$Toledo, OR  55237",
                "description: This is the description for Aaron Atrc.",
                "",
                "dn: uid=user.4,ou=People," + testBaseDN,
                "objectClass: top",
                "objectClass: person",
                "objectClass: organizationalPerson",
                "objectClass: inetOrgPerson",
                "givenName: Aartjan",
                "sn: Aalders",
                "cn: Aartjan Aalders",
                "initials: ALA",
                "employeeNumber: 4",
                "uid: user.4",
                "mail: user.4@example.com",
                "userPassword: password",
                "telephoneNumber: 981-148-3303",
                "homePhone: 196-877-2684",
                "pager: 910-998-4607",
                "mobile: 123-239-8262",
                "carLicense: 123-239-8262",
                "street: 81512 Sunset Street",
                "l: Chattanooga",
                "st: WV",
                "postalCode: 29530",
                "postalAddress: Aartjan Aalders$81512 Sunset Street$Chattanooga, WV  29530",
                "description: This is the description for Aartjan Aalders.",
                "",
                "dn: uid=user.5,ou=People," + testBaseDN,
                "objectClass: top",
                "objectClass: person",
                "objectClass: organizationalPerson",
                "objectClass: inetOrgPerson",
                "givenName: Abagael",
                "sn: Aasen",
                "cn: Abagael Aasen",
                "initials: AKA",
                "employeeNumber: 5",
                "uid: user.5",
                "mail: user.5@example.com",
                "userPassword: password",
                "telephoneNumber: 930-493-2391",
                "homePhone: 078-254-3960",
                "pager: 281-936-8197",
                "mobile: 559-822-7712",
                "carLicense: 559-822-7712",
                "street: 31988 Central Street",
                "l: Chico",
                "st: MS",
                "postalCode: 20135",
                "postalAddress: Abagael Aasen$31988 Central Street$Chico, MS  20135",
                "description: This is the description for Abagael Aasen.",
                "",
                "dn: uid=user.6,ou=People," + testBaseDN,
                "objectClass: top",
                "objectClass: person",
                "objectClass: organizationalPerson",
                "objectClass: inetOrgPerson",
                "givenName: Abagail",
                "sn: Abadines",
                "cn: Abagail Abadines",
                "initials: AQA",
                "employeeNumber: 6",
                "uid: user.6",
                "mail: user.6@example.com",
                "userPassword: password",
                "telephoneNumber: 110-761-3861",
                "homePhone: 459-123-0553",
                "pager: 799-151-2688",
                "mobile: 953-582-7252",
                "carLicense: 953-582-7252",
                "street: 60100 Dogwood Street",
                "l: Hartford",
                "st: NE",
                "postalCode: 79353",
                "postalAddress: Abagail Abadines$60100 Dogwood Street$Hartford, NE  79353",
                "description: This is the description for Abagail Abadines.",
                "",
                "dn: uid=user.7,ou=People," + testBaseDN,
                "objectClass: top",
                "objectClass: person",
                "objectClass: organizationalPerson",
                "objectClass: inetOrgPerson",
                "givenName: Abahri",
                "sn: Abazari",
                "cn: Abahri Abazari",
                "initials: AXA",
                "employeeNumber: 7",
                "uid: user.7",
                "mail: user.7@example.com",
                "userPassword: password",
                "telephoneNumber: 594-537-4292",
                "homePhone: 174-724-6390",
                "pager: 733-217-8194",
                "mobile: 879-706-0172",
                "carLicense: 879-706-0172",
                "street: 77693 Oak Street",
                "l: Philadelphia",
                "st: MN",
                "postalCode: 78550",
                "postalAddress: Abahri Abazari$77693 Oak Street$Philadelphia, MN  78550",
                "description: This is the description for Abahri Abazari.",
                "",
                "dn: uid=user.8,ou=People," + testBaseDN,
                "objectClass: top",
                "objectClass: person",
                "objectClass: organizationalPerson",
                "objectClass: inetOrgPerson",
                "givenName: Abbas",
                "sn: Abbatantuono",
                "cn: Abbas Abbatantuono",
                "initials: AVA",
                "employeeNumber: 8",
                "uid: user.8",
                "mail: user.8@example.com",
                "userPassword: password",
                "telephoneNumber: 246-674-8407",
                "homePhone: 039-769-3372",
                "pager: 226-950-2371",
                "mobile: 587-709-2996",
                "carLicense: 587-709-2996",
                "street: 23230 Hill Street",
                "l: Little Rock",
                "st: AR",
                "",
                "dn: uid=user.9,ou=People," + testBaseDN,
                "objectClass: top",
                "objectClass: person",
                "objectClass: organizationalPerson",
                "objectClass: inetOrgPerson",
                "givenName: Abbe",
                "sn: Abbate",
                "cn: Abbe Abbate",
                "initials: AWA",
                "employeeNumber: 9",
                "uid: user.9",
                "mail: user.9@example.com",
                "userPassword: password",
                "telephoneNumber: 205-805-3357",
                "homePhone: 770-780-5917",
                "pager: 537-074-8005",
                "mobile: 120-204-7597",
                "carLicense: 120-204-7597",
                "street: 47952 Center Street",
                "l: Butte",
                "st: TN",
                "postalCode: 69384",
                "postalAddress: Abbe Abbate$47952 Center Street$Butte, TN  69384",
                "description: This is the description for Abbe Abbate.",
                "",
                "dn: uid=user.10,ou=People," + testBaseDN,
                "objectClass: top",
                "objectClass: person",
                "objectClass: organizationalPerson",
                "objectClass: inetOrgPerson",
                "givenName: Abbey",
                "sn: Abbie",
                "cn: Abbey Abbie",
                "initials: AZA",
                "employeeNumber: 10",
                "uid: user.10",
                "mail: user.10@example.com",
                "userPassword: password",
                "telephoneNumber: 457-819-0832",
                "homePhone: 931-305-5452",
                "pager: 118-165-7194",
                "mobile: 553-729-5572",
                "carLicense: 553-729-5572",
                "street: 54262 Highland Street",
                "l: Spartanburg",
                "st: PA",
                "postalCode: 38151",
                "postalAddress: Abbey Abbie$54262 Highland Street$Spartanburg, PA  38151",
                "description: This is the description for Abbey Abbie.",
                "",
                "dn: uid=user.539,ou=People," + testBaseDN,
                "objectClass: top",
                "objectClass: person",
                "objectClass: organizationalPerson",
                "objectClass: inetOrgPerson",
                "givenName: Ardyth",
                "sn: Bainton",
                "cn: Ardyth Bainton",
                "initials: AIB",
                "employeeNumber: 539",
                "uid: user.539",
                "mail: user.539@example.com",
                "userPassword: password",
                "telephoneNumber: 641-433-7404",
                "homePhone: 524-765-8780",
                "pager: 985-331-1308",
                "mobile: 279-423-0188",
                "carLicense: 279-423-0188",
                "street: 81170 Taylor Street",
                "l: Syracuse",
                "st: WV",
                "postalCode: 93507",
                "postalAddress: Ardyth Bainton$81170 Taylor Street$Syracuse, WV  93507",
                "description: This is the description for Ardyth Bainton.");

    workEntries = TestCaseUtils.makeEntries(
                "dn: uid=user.11,ou=People," + testBaseDN,
                "objectClass: top",
                "objectClass: person",
                "objectClass: organizationalPerson",
                "objectClass: inetOrgPerson",
                "givenName: Annalee",
                "sn: Avard",
                "cn: Annalee Avard",
                "initials: ANA",
                "employeeNumber: 11",
                "uid: user.11",
                "mail: user.11@example.com",
                "userPassword: password",
                "telephoneNumber: 875-335-2712",
                "homePhone: 181-995-6635",
                "pager: 586-905-4185",
                "mobile: 826-857-7592",
                "carLicense: 826-857-7592",
                "street: 46168 Mill Street",
                "l: Charleston",
                "st: CO",
                "postalCode: 60948",
                "postalAddress: Annalee Avard$46168 Mill Street$Charleston, CO  60948",
                "description: This is the description for Annalee Avard.",
                "",
                "dn: uid=user.12,ou=People," + testBaseDN,
                "objectClass: top",
                "objectClass: person",
                "objectClass: organizationalPerson",
                "objectClass: inetOrgPerson",
                "givenName: Andaree",
                "sn: Asawa",
                "cn: Andaree Asawa",
                "initials: AEA",
                "employeeNumber: 12",
                "uid: user.12",
                "mail: user.12@example.com",
                "userPassword: password",
                "telephoneNumber: 399-788-7334",
                "homePhone: 798-076-5683",
                "pager: 034-026-9411",
                "mobile: 948-743-9197",
                "carLicense: 948-743-9197",
                "street: 81028 Forest Street",
                "l: Wheeling",
                "st: IA",
                "postalCode: 60905",
                "postalAddress: Andaree Asawa$81028 Forest Street$Wheeling, IA  60905",
                "description: This is the description for Andaree Asawa.");

    dnToDel = workEntries.get(1).getName();
    searchDN = entries.get(1).getName();
    badEntryDN = testBaseDN.child(DN.valueOf("ou=bogus")).child(DN.valueOf("ou=dummy"));
    backupID = "backupID1";

    addEntriesToBackend(topEntries);
    addEntriesToBackend(entries);
    addEntriesToBackend(workEntries);
  }

  private void addEntriesToBackend(List<Entry> entries) throws Exception
  {
    for (Entry newEntry : entries)
    {
      backend.addEntry(newEntry, null);
    }
  }

  @AfterClass
  public void cleanUp() throws Exception
  {
    try
    {
      if (backupDirectory != null)
      {
        backend.removeBackup(backupDirectory, backupID);
      }
    }
    catch (DirectoryException ignore)
    {
      /*
       *  Due to test sequencing (Import, backup, restore, export) in case of intermediate failure
       *  Backup may or may not be present. -> ignore.
       */
      Reporter.log(ignore.getMessage(), true);
    }
    finally
    {
      backend.finalizeBackend();
      backend = null;
    }
  }

  /**
   * Tests the storage API for resource checking. The tested method has no return value, but an
   * exception, while not systematic, may be thrown, in which case the test must fail.
   *
   * @throws Exception
   *           if resources are low.
   */
  @Test
  public void testCheckForEnoughResources() throws Exception
  {
    backend.getRootContainer().checkForEnoughResources(null);
  }

  @Test(expectedExceptions = DirectoryException.class)
  public void testAddNoParent() throws Exception
  {
    Entry newEntry = TestCaseUtils.makeEntry("dn: " + badEntryDN, "objectclass: ou", "");
    backend.addEntry(newEntry, null);
  }

  @Test
  public void testUtilityAPIs()
  {
    assertEquals(backend.getEntryCount(), getTotalNumberOfLDIFEntries());
    assertFalse(backend.isIndexed(modifyAttribute, EQUALITY));
    for (Map.Entry<String, IndexType[]> index : backendIndexes.entrySet())
    {
      for (IndexType type : index.getValue())
      {
        final AttributeType attributeType = DirectoryServer.getAttributeTypeOrNull(index.getKey().toLowerCase());
        assertTrue(backend.isIndexed(attributeType,
            org.opends.server.types.IndexType.valueOf(type.toString().toUpperCase())));
      }
    }
  }

  private int getTotalNumberOfLDIFEntries()
  {
    return topEntries.size() + entries.size() + workEntries.size();
  }

  @Test
  public void testHasSubordinates() throws Exception
  {
    assertEquals(backend.hasSubordinates(testBaseDN), ConditionResult.TRUE,
        "Base DN should have subordinates.");

    // Testing ConditionResult.UNDEFINED needs either no entry container or a big DIT...
    assertEquals(backend.hasSubordinates(DN.valueOf("dc=a")), ConditionResult.UNDEFINED,
        "Subordinates query on unknown baseDN should return UNDEFINED.");

    assertEquals(backend.getNumberOfChildren(testBaseDN), 1);
    assertEquals(backend.getNumberOfEntriesInBaseDN(testBaseDN), getTotalNumberOfLDIFEntries(), "Wrong DIT count.");
    assertEquals(backend.hasSubordinates(searchDN), ConditionResult.FALSE,
        "Leaf entry should not have any subordinates.");
  }

  private List<SearchResultEntry> runSearch(SearchRequest request, boolean useInternalConnection) throws Exception
  {
    InternalClientConnection conn = getRootConnection();

    if (useInternalConnection)
    {
      InternalSearchOperation search = conn.processSearch(request);
      return search.getSearchEntries();
    }
    else
    {
      InternalSearchOperation search = new InternalSearchOperation(conn, -1, -1, request);
      backend.search(new LocalBackendSearchOperation(search));
      return search.getSearchEntries();
    }
  }

  @Test
  public void testModifyEntry() throws Exception
  {
    Entry oldEntry = workEntries.get(0);
    Entry newEntry = oldEntry.duplicate(false);

    modifyAttribute = DirectoryServer.getAttributeTypeOrNull("jpegphoto");
    newEntry.applyModifications(Arrays.asList(new Modification(ADD, create(modifyAttribute, modifyValue))));

    backend.replaceEntry(oldEntry, newEntry, null);
    assertTrue(backend.getEntry(oldEntry.getName()).hasValue(modifyAttribute, null, modifyValue));
  }

  @Test
  public void testRenameEntry() throws Exception
  {
    // Move the entire subtree to another name and move it back.
    DN prevDN = DN.valueOf("ou=People," + testBaseDN);
    DN newDN = DN.valueOf("ou=users," + testBaseDN);
    Entry renameEntry = backend.getEntry(prevDN).duplicate(false);

    renameEntry.setDN(newDN);
    backend.renameEntry(prevDN, renameEntry, null);
    Entry dbEntry = backend.getEntry(newDN);
    assertEquals(dbEntry.getName(), newDN, "Renamed entry is missing.");

    renameEntry.setDN(prevDN);
    backend.renameEntry(newDN, renameEntry, null);
    dbEntry = backend.getEntry(prevDN);
    assertEquals(dbEntry.getName(), prevDN, "Original entry has not been renamed");
  }

  @Test(description = "OPENDJ-2404")
  public void testRenameEntrySameDNDifferentCase() throws Exception
  {
    DN prevDN = DN.valueOf("uid=user.0,ou=People," + testBaseDN);
    DN newDN = DN.valueOf("uid=USER.0,ou=People," + testBaseDN);
    Entry renameEntry = backend.getEntry(prevDN).duplicate(false);

    renameEntry.setDN(newDN);
    backend.renameEntry(prevDN, renameEntry, null);
    Entry dbEntry = backend.getEntry(newDN);
    assertEquals(dbEntry.getName().rdn().toString(), "uid=USER.0");

    renameEntry.setDN(prevDN);
    backend.renameEntry(newDN, renameEntry, null);
    dbEntry = backend.getEntry(prevDN);
    assertEquals(dbEntry.getName().rdn().toString(), "uid=user.0");
  }

  @Test
  public void testDeleteEntry() throws Exception
  {
    Entry deletedEntry = backend.getEntry(dnToDel).duplicate(false);
    deleteEntry(dnToDel);
    try
    {
      deleteEntry(dnToDel);
      fail("Should have generated a DirectoryException");
    }
    catch (DirectoryException de)
    {
      // Expected exception, do nothing, test succeeds.
    }
    finally
    {
      backend.addEntry(deletedEntry, null);
    }
  }

  private void deleteEntry(DN dn) throws Exception
  {
    backend.deleteEntry(dn, null);
    assertNull(backend.getEntry(workEntries.get(1).getName()));
  }

  @Test
  public void testBaseSearch() throws Exception
  {
    baseSearch(false);
    baseSearch(true);
  }

  private void baseSearch(boolean useInternalConnection) throws Exception
  {
    SearchRequest request = newSearchRequest(testBaseDN, SearchScope.BASE_OBJECT, "objectclass=*");
    List<SearchResultEntry> result = runSearch(request, useInternalConnection);

    assertEquals(result.size(), 1, "Base Search should return only one Entry");
    assertEquals(result.get(0).getName(), testBaseDN, "Base Search on the suffix should return the suffix itself");
  }

  @Test
  public void testOneLevelSearch() throws Exception
  {
    oneLevelSearch(false);
    oneLevelSearch(true);
  }

  private void oneLevelSearch(boolean useInternalConnection) throws Exception
  {
    SearchRequest request = newSearchRequest(testBaseDN, SearchScope.SINGLE_LEVEL, "objectclass=*");
    List<SearchResultEntry> result = runSearch(request, useInternalConnection);

    assertEquals(result.size(), 1, "One Level search should return a single child entry");
    SearchResultEntry resEntry = result.get(0);
    assertEquals(topEntries.get(1).getName(), resEntry.getName(),
        "One Level search should return the expected child");
  }

  @Test
  public void testSubTreeSearch() throws Exception
  {
    subTreeSearch(false);
    subTreeSearch(true);
  }

  @Test
  public void testSubTreeSearchAgainstAnIndexWithUnrecognizedMatchingRule() throws Exception
  {
    SearchRequest request = newSearchRequest(testBaseDN, SearchScope.WHOLE_SUBTREE, "entryUUID=xxx*");
    assertThat(runSearch(request, false)).isEmpty();
  }

  @Test
  public void testSearchIsConsideredUnindexedBasedOnLookThroughLimit() throws DirectoryException {
    final int nbEntries = topEntries.size() + entries.size() + workEntries.size();

    final SearchRequest request = newSearchRequest(testBaseDN, SearchScope.WHOLE_SUBTREE, "objectclass=*");
    final ClientConnection connection = new ClientConnectionStub();
    connection.setLookthroughLimit(0);
    InternalSearchOperation searchOperation = new InternalSearchOperation(connection, 1, 1, request, null);
    searchOperation.run();
    assertThat(searchOperation.getEntriesSent()).isEqualTo(nbEntries);

    connection.setLookthroughLimit(nbEntries);
    searchOperation = new InternalSearchOperation(connection, 1, 1, request, null);
    searchOperation.run();
    assertThat(searchOperation.getEntriesSent()).isEqualTo(nbEntries);

    connection.setLookthroughLimit(nbEntries - 1);
    searchOperation = new InternalSearchOperation(connection, 1, 1, request, null);
    searchOperation.run();
    assertThat(searchOperation.getResultCode()).isEqualTo(ResultCode.INSUFFICIENT_ACCESS_RIGHTS);
    assertThat(searchOperation.getErrorMessage().toString()).contains("not have sufficient privileges", "unindexed search");
    assertThat(searchOperation.getEntriesSent()).isEqualTo(0);
  }

  private void subTreeSearch(boolean useInternalConnection) throws Exception
  {
    SearchRequest request = newSearchRequest(testBaseDN, SearchScope.WHOLE_SUBTREE, "objectclass=*");
    List<SearchResultEntry> result = runSearch(request, useInternalConnection);

    // Sum of all entry sets minus a delete
    assertEquals(result.size(), getTotalNumberOfLDIFEntries(),
        "Subtree search should return a correct number of entries");
  }


  @DataProvider(name = "userEntrySearchData")
  protected Object[][] userEntrySearchData()
  {
    return new Object[][] {
      // @formatter:off
      { true,  SearchScope.BASE_OBJECT, 1 },
      { false, SearchScope.BASE_OBJECT, 1 },
      { true,  SearchScope.SINGLE_LEVEL, 0 },
      { false, SearchScope.SINGLE_LEVEL, 0 },
      { true,  SearchScope.WHOLE_SUBTREE, 1 },
      { false, SearchScope.WHOLE_SUBTREE, 1 },
      // @formatter:on
    };
  }

  @Test(dataProvider = "userEntrySearchData")
  public void testUserEntrySearch(boolean useInternalConnection, SearchScope scope, int expectedEntryCount)
      throws Exception
  {
    SearchRequest request = newSearchRequest(searchDN, scope, "objectclass=*");
    List<SearchResultEntry> result = runSearch(request, useInternalConnection);

    assertEquals(result.size(), expectedEntryCount, "User entry search should return " + expectedEntryCount
        + " child entry");
    if (expectedEntryCount > 0)
    {
      assertEquals(searchDN, result.get(0).getName(), "User entry search should return the expected entry");
    }
  }

  @Test
  public void testGetEntry() throws Exception
  {
    for (Entry expected : entries)
    {
      Entry dbEntry = backend.getEntry(expected.getName());
      Entry actual = new Entry(dbEntry.getName(), dbEntry.getObjectClasses(), dbEntry.getUserAttributes(), null);

      // Remove the userPassword because it will have been encoded.
      expected.removeAttribute(DirectoryServer.getAttributeTypeOrDefault("userpassword"));
      actual.removeAttribute(DirectoryServer.getAttributeTypeOrDefault("userpassword"));

      assertThat(actual).isEqualTo(expected);
    }
  }

  @Test
  public void testExportLDIFAndImportLDIF() throws Exception
  {
    assertTrue(backend.supports(BackendOperation.LDIF_EXPORT), "Export not supported");
    ByteArrayOutputStream ldifOutputContent = new ByteArrayOutputStream();
    try (final LDIFExportConfig exportConfig = new LDIFExportConfig(ldifOutputContent))
    {
      exportConfig.setIncludeOperationalAttributes(true);
      backend.exportLDIF(exportConfig);
    }

    String ldifString = ldifOutputContent.toString();
    assertEquals(ldifString.contains(testBaseDN.toString()), true, "Export without rootDN");
    assertEquals(ldifString.contains(searchDN.toString()), true, "Export without rootDN");


    // Import wants the backend to be configured but not initialized. Finalizing resets the status.
    assertTrue(backend.supports(BackendOperation.LDIF_IMPORT), "Import not supported");
    backend.finalizeBackend();

    ByteArrayInputStream ldifImportContent = new ByteArrayInputStream(ldifOutputContent.toByteArray());
    ByteArrayOutputStream rejectedEntries = new ByteArrayOutputStream();
    try (final LDIFImportConfig importConf = new LDIFImportConfig(ldifImportContent))
    {
      importConf.setInvokeImportPlugins(true);
      importConf.setClearBackend(true);
      importConf.writeRejectedEntries(rejectedEntries);
      importConf.setIncludeBranches(Collections.singleton(testBaseDN));
      importConf.setSkipDNValidation(true);
      importConf.setThreadCount(0);
      backend.importLDIF(importConf, DirectoryServer.getInstance().getServerContext());
    }
    assertEquals(rejectedEntries.size(), 0,
                 "No entries should be rejected. Content was:\n" + rejectedEntries.toString());

    backend.openBackend();
    assertEquals(backend.getEntryCount(), getTotalNumberOfLDIFEntries(), "Not enough entries in DIT.");
    /** +1 for the testBaseDN itself */
    assertEquals(backend.getNumberOfEntriesInBaseDN(testBaseDN), getTotalNumberOfLDIFEntries(),
                 "Not enough entries in DIT.");
    assertEquals(backend.getNumberOfChildren(testBaseDN), 1,
                 "Not enough entries in DIT.");
    /** -2 for baseDn and People entry */
    assertEquals(backend.getNumberOfChildren(testBaseDN.child(DN.valueOf("ou=People"))), getTotalNumberOfLDIFEntries() - 2,
                 "Not enough entries in DIT.");

    VerifyConfig config = new VerifyConfig();
    config.setBaseDN(DN.valueOf("dc=test,dc=com"));
    config.addCompleteIndex("dn2id");
    for (String indexName : backendIndexes.keySet())
    {
      config.addCompleteIndex(indexName);
    }
    assertThat(backend.verifyBackend(config)).isEqualTo(0);

    config = new VerifyConfig();
    config.setBaseDN(DN.valueOf("dc=test,dc=com"));
    config.addCleanIndex("dn2id");
    for (String indexName : backendIndexes.keySet())
    {
      config.addCleanIndex(indexName);
    }
    assertThat(backend.verifyBackend(config)).isEqualTo(0);
  }

  @Test
  public void testRebuildAllIndex() throws Exception
  {
    final EntryContainer entryContainer =  backend.getRootContainer().getEntryContainers().iterator().next();

    // Delete all the indexes
    backend.getRootContainer().getStorage().write(new WriteOperation()
    {
      @Override
      public void run(WriteableTransaction txn) throws Exception
      {
        entryContainer.getDN2ID().delete(txn);
        entryContainer.getDN2URI().delete(txn);
        entryContainer.getID2ChildrenCount().delete(txn);
        for(VLVIndex idx : entryContainer.getVLVIndexes())
        {
          idx.setTrusted(txn, false);
          idx.delete(txn);
        }
        for(AttributeIndex attribute : entryContainer.getAttributeIndexes())
        {
          for(Index idx : attribute.getNameToIndexes().values())
          {
            idx.setTrusted(txn, false);
            idx.delete(txn);
          }
        }
      }
    });

    RebuildConfig rebuildConf = new RebuildConfig();
    rebuildConf.setBaseDN(DN.valueOf("dc=test,dc=com"));
    rebuildConf.setRebuildMode(RebuildMode.ALL);

    backend.closeBackend();
    backend.rebuildBackend(rebuildConf, DirectoryServer.getInstance().getServerContext());
    backend.openBackend();

    VerifyConfig config = new VerifyConfig();
    config.setBaseDN(DN.valueOf("dc=test,dc=com"));
    config.addCompleteIndex("dn2id");
    for (String indexName : backendIndexes.keySet())
    {
      config.addCompleteIndex(indexName);
    }
    assertThat(backend.verifyBackend(config)).isEqualTo(0);


    config = new VerifyConfig();
    config.setBaseDN(DN.valueOf("dc=test,dc=com"));
    config.addCleanIndex("dn2id");
    for (String indexName : backendIndexes.keySet())
    {
      config.addCleanIndex(indexName);
    }
    assertThat(backend.verifyBackend(config)).isEqualTo(0);
  }

  @Test
  public void testRebuildDegradedIndex() throws Exception
  {
    final EntryContainer entryContainer =  backend.getRootContainer().getEntryContainers().iterator().next();

    final Set<String> dirtyIndexes = new HashSet<>(Arrays.asList(new String[] { "sn", "uid", "telephoneNumber" }));
    assertThat(backendIndexes.keySet()).containsAll(dirtyIndexes);

    // Delete all the indexes
    backend.getRootContainer().getStorage().write(new WriteOperation()
    {
      @Override
      public void run(WriteableTransaction txn) throws Exception
      {
        for(AttributeIndex attribute : entryContainer.getAttributeIndexes())
        {
          boolean trusted = !dirtyIndexes.contains(attribute.getAttributeType().getNameOrOID());
          for(Index idx : attribute.getNameToIndexes().values())
          {
            idx.setTrusted(txn, trusted);
          }
        }
      }
    });

    RebuildConfig rebuildConf = new RebuildConfig();
    rebuildConf.setBaseDN(DN.valueOf("dc=test,dc=com"));
    rebuildConf.setRebuildMode(RebuildMode.DEGRADED);

    backend.closeBackend();
    backend.rebuildBackend(rebuildConf, DirectoryServer.getInstance().getServerContext());
    backend.openBackend();

    VerifyConfig config = new VerifyConfig();
    config.setBaseDN(DN.valueOf("dc=test,dc=com"));
    config.addCompleteIndex("dn2id");
    for (String indexName : backendIndexes.keySet())
    {
      config.addCompleteIndex(indexName);
    }
    assertThat(backend.verifyBackend(config)).isEqualTo(0);

    config = new VerifyConfig();
    config.setBaseDN(DN.valueOf("dc=test,dc=com"));
    config.addCleanIndex("dn2id");
    for (String indexName : backendIndexes.keySet())
    {
      config.addCleanIndex(indexName);
    }
    assertThat(backend.verifyBackend(config)).isEqualTo(0);
  }

  @Test
  public void testVerifyID2ChildrenCount() throws Exception
  {
    final Storage storage = backend.getRootContainer().getStorage();
    final DN2ID dn2ID = backend.getRootContainer().getEntryContainer(testBaseDN).getDN2ID();
    final ID2ChildrenCount id2ChildrenCount = backend.getRootContainer().getEntryContainer(testBaseDN).getID2ChildrenCount();

    final VerifyConfig config = new VerifyConfig();
    config.setBaseDN(DN.valueOf("dc=test,dc=com"));
    config.addCleanIndex("dn2id");

    assertThat(backend.verifyBackend(config)).isEqualTo(0);

    // Insert an error
    final EntryID peopleID = storage.read(new ReadOperation<EntryID>()
    {
      @Override
      public EntryID run(ReadableTransaction txn) throws Exception
      {
        return dn2ID.get(txn, testBaseDN.child(DN.valueOf("ou=People")));
      }
    });
    storage.write(new WriteOperation()
    {
      @Override
      public void run(WriteableTransaction txn) throws Exception
      {
        final long delta = id2ChildrenCount.removeCount(txn, peopleID);
        id2ChildrenCount.updateTotalCount(txn, -delta);
        id2ChildrenCount.updateCount(txn, peopleID, 1);
        id2ChildrenCount.updateTotalCount(txn, 1);
      }
    });

    assertThat(backend.verifyBackend(config)).isEqualTo(1);
  }

  @Test
  public void testBackupAndRestore() throws Exception
  {
    assertEquals(backend.supports(BackendOperation.BACKUP), true, "Skip Backup");
    assertNotNull(backupID, "Need to setup a backupID");

    final String backupPath = TestCaseUtils.createTemporaryDirectory("backup").getAbsolutePath();
    backupDirectory = new BackupDirectory(backupPath, testBaseDN);
    BackupConfig backupConf = new BackupConfig(backupDirectory, backupID, false);
    backend.createBackup(backupConf);

    assertTrue(backend.supports(BackendOperation.RESTORE), "Skip Restore");
    backend.restoreBackup(new RestoreConfig(backupDirectory, backupID, true));
  }

  @Test(expectedExceptions=ReadOnlyStorageException.class)
  public void testReadOnly() throws Exception
  {
    C backendCfg = createBackendCfg();
    when(backendCfg.dn()).thenReturn(testBaseDN);
    when(backendCfg.getBaseDN()).thenReturn(newTreeSet(testBaseDN));
    when(backendCfg.listBackendIndexes()).thenReturn(new String[0]);
    when(backendCfg.listBackendVLVIndexes()).thenReturn(new String[0]);

    final Storage storage = backend.configureStorage(backendCfg, DirectoryServer.getInstance().getServerContext());
    final RootContainer readOnlyContainer = new RootContainer(backend.getBackendID(), storage, backendCfg);

    // Put backend offline so that export LDIF open read-only container
    backend.finalizeBackend();
    try
    {
      readOnlyContainer.open(AccessMode.READ_ONLY);
      readOnlyContainer.getStorage().write(new WriteOperation()
      {
        @Override
        public void run(WriteableTransaction txn) throws Exception
        {
          txn.put(new TreeName("dc=test,dc=com", "id2entry"), ByteString.valueOfUtf8("key"), ByteString.valueOfUtf8("value"));
        }
      });
    }
    finally
    {
      readOnlyContainer.close();
      backend.openBackend();
    }
  }
}
