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

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import org.forgerock.opendj.io.ASN1;
import org.forgerock.opendj.io.ASN1Reader;
import org.forgerock.opendj.io.ASN1Writer;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ByteStringBuilder;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.SearchScope;
import org.opends.server.TestCaseUtils;
import org.opends.server.core.DirectoryServer;
import org.opends.server.protocols.internal.InternalSearchOperation;
import org.opends.server.protocols.internal.SearchRequest;
import org.opends.server.protocols.ldap.LDAPControl;
import org.opends.server.protocols.ldap.LDAPResultCode;
import org.opends.server.types.*;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

import static org.opends.server.protocols.internal.InternalClientConnection.*;
import static org.opends.server.protocols.internal.Requests.*;
import static org.opends.server.util.ServerConstants.*;
import static org.testng.Assert.*;

/**
 * This class contains a number of test cases for the virtual list view request
 * and response controls.
 */
public class VLVControlTestCase
    extends ControlsTestCase
{
  /** The givenName attribute type. */
  private AttributeType givenNameType;
  /** The sn attribute type. */
  private AttributeType snType;

  /** The DN for "Aaccf Johnson". */
  private DN aaccfJohnsonDN;
  /** The DN for "Aaron Zimmerman". */
  private DN aaronZimmermanDN;
  /** The DN for "Albert Smith". */
  private DN albertSmithDN;
  /** The DN for "Albert Zimmerman". */
  private DN albertZimmermanDN;
  /** The DN for "lowercase mcgee". */
  private DN lowercaseMcGeeDN;
  /** The DN for "Mararet Jones". */
  private DN margaretJonesDN;
  /** The DN for "Mary Jones". */
  private DN maryJonesDN;
  /** The DN for "Sam Zweck". */
  private DN samZweckDN;
  /** The DN for "Zorro". */
  private DN zorroDN;



  /**
   * Make sure that the server is running.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @BeforeClass
  public void startServer()
         throws Exception
  {
    TestCaseUtils.startServer();

    givenNameType = DirectoryServer.getAttributeTypeOrNull("givenname");
    assertNotNull(givenNameType);

    snType = DirectoryServer.getAttributeTypeOrNull("sn");
    assertNotNull(snType);

    aaccfJohnsonDN    = DN.valueOf("uid=aaccf.johnson,dc=example,dc=com");
    aaronZimmermanDN  = DN.valueOf("uid=aaron.zimmerman,dc=example,dc=com");
    albertSmithDN     = DN.valueOf("uid=albert.smith,dc=example,dc=com");
    albertZimmermanDN = DN.valueOf("uid=albert.zimmerman,dc=example,dc=com");
    lowercaseMcGeeDN  = DN.valueOf("uid=lowercase.mcgee,dc=example,dc=com");
    margaretJonesDN   = DN.valueOf("uid=margaret.jones,dc=example,dc=com");
    maryJonesDN       = DN.valueOf("uid=mary.jones,dc=example,dc=com");
    samZweckDN        = DN.valueOf("uid=sam.zweck,dc=example,dc=com");
    zorroDN           = DN.valueOf("uid=zorro,dc=example,dc=com");
  }



  /**
   * Populates the JE DB with a set of test data.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  private void populateDB()
          throws Exception
  {
    TestCaseUtils.clearBackend("userRoot", "dc=example,dc=com");

    TestCaseUtils.addEntries(
      "dn: uid=albert.zimmerman,dc=example,dc=com",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: albert.zimmerman",
      "givenName: Albert",
      "sn: Zimmerman",
      "cn: Albert Zimmerman",
      "",
      "dn: uid=albert.smith,dc=example,dc=com",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: albert.smith",
      "givenName: Albert",
      "sn: Smith",
      "cn: Albert Smith",
      "",
      "dn: uid=aaron.zimmerman,dc=example,dc=com",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: albert.zimmerman",
      "givenName: Aaron",
      "givenName: Zeke",
      "sn: Zimmerman",
      "cn: Aaron Zimmerman",
      "",
      "dn: uid=mary.jones,dc=example,dc=com",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: mary.jones",
      "givenName: Mary",
      "sn: Jones",
      "cn: Mary Jones",
      "",
      "dn: uid=margaret.jones,dc=example,dc=com",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: margaret.jones",
      "givenName: Margaret",
      "givenName: Maggie",
      "sn: Jones",
      "sn: Smith",
      "cn: Maggie Jones-Smith",
      "",
      "dn: uid=aaccf.johnson,dc=example,dc=com",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: aaccf.johnson",
      "givenName: Aaccf",
      "sn: Johnson",
      "cn: Aaccf Johnson",
      "",
      "dn: uid=sam.zweck,dc=example,dc=com",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: sam.zweck",
      "givenName: Sam",
      "sn: Zweck",
      "cn: Sam Zweck",
      "",
      "dn: uid=lowercase.mcgee,dc=example,dc=com",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: lowercase.mcgee",
      "givenName: lowercase",
      "sn: mcgee",
      "cn: lowercase mcgee",
      "",
      "dn: uid=zorro,dc=example,dc=com",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "uid: zorro",
      "sn: Zorro",
      "cn: Zorro"
    );
  }



  /**
   * Tests the first constructor for the request control.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testRequestConstructor1()
              throws Exception
  {
    VLVRequestControl vlvRequest = new VLVRequestControl(0, 9, 1, 0);

    assertEquals(vlvRequest.isCritical(), false);
    assertEquals(vlvRequest.getBeforeCount(), 0);
    assertEquals(vlvRequest.getAfterCount(), 9);
    assertEquals(vlvRequest.getOffset(), 1);
    assertEquals(vlvRequest.getContentCount(), 0);
    assertNull(vlvRequest.getContextID());
    assertEquals(vlvRequest.getTargetType(),
                 VLVRequestControl.TYPE_TARGET_BYOFFSET);
    assertNull(vlvRequest.getGreaterThanOrEqualAssertion());
    assertNotNull(vlvRequest.toString());
  }



  /**
   * Tests the second constructor for the request control with a null context
   * ID.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testRequestConstructor2NullContextID()
              throws Exception
  {
    VLVRequestControl vlvRequest = new VLVRequestControl(true, 0, 9, 1, 0, null);

    assertEquals(vlvRequest.isCritical(), true);
    assertEquals(vlvRequest.getBeforeCount(), 0);
    assertEquals(vlvRequest.getAfterCount(), 9);
    assertEquals(vlvRequest.getOffset(), 1);
    assertEquals(vlvRequest.getContentCount(), 0);
    assertNull(vlvRequest.getContextID());
    assertEquals(vlvRequest.getTargetType(),
                 VLVRequestControl.TYPE_TARGET_BYOFFSET);
    assertNull(vlvRequest.getGreaterThanOrEqualAssertion());
    assertNotNull(vlvRequest.toString());
  }



  /**
   * Tests the second constructor for the request control with a non-null
   * context  ID.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testRequestConstructor2NonNullContextID()
              throws Exception
  {
    VLVRequestControl vlvRequest =
         new VLVRequestControl(true, 0, 9, 1, 0, ByteString.valueOfUtf8("foo"));

    assertEquals(vlvRequest.isCritical(), true);
    assertEquals(vlvRequest.getBeforeCount(), 0);
    assertEquals(vlvRequest.getAfterCount(), 9);
    assertEquals(vlvRequest.getOffset(), 1);
    assertEquals(vlvRequest.getContentCount(), 0);
    assertNotNull(vlvRequest.getContextID());
    assertEquals(vlvRequest.getTargetType(),
                 VLVRequestControl.TYPE_TARGET_BYOFFSET);
    assertNull(vlvRequest.getGreaterThanOrEqualAssertion());
    assertNotNull(vlvRequest.toString());
  }



  /**
   * Tests the third constructor for the request control.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testRequestConstructor3()
              throws Exception
  {
    VLVRequestControl vlvRequest =
         new VLVRequestControl(0, 9, ByteString.valueOfUtf8("a"));

    assertEquals(vlvRequest.isCritical(), false);
    assertEquals(vlvRequest.getBeforeCount(), 0);
    assertEquals(vlvRequest.getAfterCount(), 9);
    assertEquals(vlvRequest.getGreaterThanOrEqualAssertion().toString(),
                 "a");
    assertNull(vlvRequest.getContextID());
    assertEquals(vlvRequest.getTargetType(),
                 VLVRequestControl.TYPE_TARGET_GREATERTHANOREQUAL);
    assertNotNull(vlvRequest.toString());
  }



  /**
   * Tests the fourth constructor for the request control with a null context
   * ID.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testRequestConstructor4NullContextID()
              throws Exception
  {
    VLVRequestControl vlvRequest =
         new VLVRequestControl(true, 0, 9, ByteString.valueOfUtf8("a"), null);

    assertEquals(vlvRequest.isCritical(), true);
    assertEquals(vlvRequest.getBeforeCount(), 0);
    assertEquals(vlvRequest.getAfterCount(), 9);
    assertEquals(vlvRequest.getGreaterThanOrEqualAssertion().toString(),
                 "a");
    assertNull(vlvRequest.getContextID());
    assertEquals(vlvRequest.getTargetType(),
                 VLVRequestControl.TYPE_TARGET_GREATERTHANOREQUAL);
    assertNotNull(vlvRequest.toString());
  }



  /**
   * Tests the fourth constructor for the request control with a non-null
   * context ID.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testRequestConstructor4NonNullContextID()
              throws Exception
  {
    VLVRequestControl vlvRequest =
         new VLVRequestControl(true, 0, 9, ByteString.valueOfUtf8("a"),
                               ByteString.valueOfUtf8("foo"));

    assertEquals(vlvRequest.isCritical(), true);
    assertEquals(vlvRequest.getBeforeCount(), 0);
    assertEquals(vlvRequest.getAfterCount(), 9);
    assertEquals(vlvRequest.getGreaterThanOrEqualAssertion().toString(),
                 "a");
    assertNotNull(vlvRequest.getContextID());
    assertEquals(vlvRequest.getTargetType(),
                 VLVRequestControl.TYPE_TARGET_GREATERTHANOREQUAL);
    assertNotNull(vlvRequest.toString());
  }

  /**
   * Tests the ASN.1 encoding for the response control.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testASN1ValueEncoding()
         throws Exception
  {
    ByteStringBuilder builder = new ByteStringBuilder();
    ASN1Writer writer = ASN1.getWriter(builder);
    VLVResponseControl vlvResponse = new VLVResponseControl(true, 0, 15, 0,
        ByteString.valueOfUtf8("foo"));
    vlvResponse.writeValue(writer);

    ASN1Reader reader = ASN1.getReader(builder.toByteString());
    // Should start as an octet string with a nested sequence
    assertEquals(reader.peekType(), ASN1.UNIVERSAL_OCTET_STRING_TYPE);
    reader.readStartSequence();
    // Should be an sequence start
    assertEquals(reader.peekType(), ASN1.UNIVERSAL_SEQUENCE_TYPE);
    reader.readStartSequence();
    // Should be an integer with targetPosition
    assertEquals(reader.peekType(), ASN1.UNIVERSAL_INTEGER_TYPE);
    assertEquals(reader.readInteger(), 0);
    // Should be an integer with contentCount
    assertEquals(reader.peekType(), ASN1.UNIVERSAL_INTEGER_TYPE);
    assertEquals(reader.readInteger(), 15);
    // Should be an enumerated with virtualListViewResult
    assertEquals(reader.peekType(), ASN1.UNIVERSAL_ENUMERATED_TYPE);
    assertEquals(reader.readEnumerated(), 0);
    // Should be an octet string with contextID
    assertEquals(reader.peekType(), ASN1.UNIVERSAL_OCTET_STRING_TYPE);
    assertEquals(reader.readOctetStringAsString(), "foo");
  }

  /**
   * Tests the first constructor for the response control.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testResponseConstructor1()
         throws Exception
  {
    VLVResponseControl vlvResponse = new VLVResponseControl(0, 15, 0);

    assertEquals(vlvResponse.isCritical(), false);
    assertEquals(vlvResponse.getTargetPosition(), 0);
    assertEquals(vlvResponse.getContentCount(), 15);
    assertEquals(vlvResponse.getVLVResultCode(), 0);
    assertNull(vlvResponse.getContextID());
    assertNotNull(vlvResponse.toString());
  }



  /**
   * Tests the second constructor for the response control with a null context
   * ID.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testResponseConstructor2NullContextID()
         throws Exception
  {
    VLVResponseControl vlvResponse = new VLVResponseControl(true, 0, 15, 0, null);

    assertEquals(vlvResponse.isCritical(), true);
    assertEquals(vlvResponse.getTargetPosition(), 0);
    assertEquals(vlvResponse.getContentCount(), 15);
    assertEquals(vlvResponse.getVLVResultCode(), 0);
    assertNull(vlvResponse.getContextID());
    assertNotNull(vlvResponse.toString());
  }



  /**
   * Tests the second constructor for the response control with a non-null
   * context ID.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testResponseConstructor2NonNullContextID()
         throws Exception
  {
    VLVResponseControl vlvResponse =
         new VLVResponseControl(true, 0, 15, 0, ByteString.valueOfUtf8("foo"));

    assertEquals(vlvResponse.isCritical(), true);
    assertEquals(vlvResponse.getTargetPosition(), 0);
    assertEquals(vlvResponse.getContentCount(), 15);
    assertEquals(vlvResponse.getVLVResultCode(), 0);
    assertNotNull(vlvResponse.getContextID());
    assertNotNull(vlvResponse.toString());
  }



  /**
   * Tests performing an internal search using the VLV control to retrieve a
   * subset of the entries using an offset of one.
   *
   * @throws  Exception  If an unexpected problem occurred.
   */
  @Test
  public void testInternalSearchByOffsetOneOffset() throws Exception
  {
    populateDB();

    SearchRequest request = newSearchRequest("dc=example,dc=com", SearchScope.WHOLE_SUBTREE, "(objectClass=person)")
        .addControl(new ServerSideSortRequestControl("givenName"))
        .addControl(new VLVRequestControl(0, 3, 1, 0));
    InternalSearchOperation internalSearch = getRootConnection().processSearch(request);
    assertEquals(internalSearch.getResultCode(), ResultCode.SUCCESS);

    ArrayList<DN> expectedDNOrder = new ArrayList<>();
    expectedDNOrder.add(aaccfJohnsonDN);    // Aaccf
    expectedDNOrder.add(aaronZimmermanDN);  // Aaron
    expectedDNOrder.add(albertZimmermanDN); // Albert, lower entry ID
    expectedDNOrder.add(albertSmithDN);     // Albert, higher entry ID

    assertEquals(getDNs(internalSearch.getSearchEntries()), expectedDNOrder);

    List<Control> responseControls = internalSearch.getResponseControls();
    assertNotNull(responseControls);
    assertEquals(responseControls.size(), 2);

    ServerSideSortResponseControl sortResponse = null;
    VLVResponseControl            vlvResponse  = null;
    for (Control c : responseControls)
    {
      if (c.getOID().equals(OID_SERVER_SIDE_SORT_RESPONSE_CONTROL))
      {
        sortResponse = getServerSideSortResponseControl(c);
      }
      else if (c.getOID().equals(OID_VLV_RESPONSE_CONTROL))
      {
        vlvResponse = getVLVResponseControl(c);
      }
      else
      {
        fail("Response control with unexpected OID " + c.getOID());
      }
    }

    assertNotNull(sortResponse);
    assertEquals(sortResponse.getResultCode(), 0);

    assertNotNull(vlvResponse);
    assertEquals(vlvResponse.getVLVResultCode(), 0);
    assertEquals(vlvResponse.getTargetPosition(), 1);
    assertEquals(vlvResponse.getContentCount(), 9);
  }



  /**
   * Tests performing an internal search using the VLV control to retrieve a
   * subset of the entries using an offset of zero, which should be treated like
   * an offset of one.
   *
   * @throws  Exception  If an unexpected problem occurred.
   */
  @Test
  public void testInternalSearchByOffsetZeroOffset()
         throws Exception
  {
    populateDB();

    SearchRequest request = newSearchRequest("dc=example,dc=com", SearchScope.WHOLE_SUBTREE, "(objectClass=person)")
        .addControl(new ServerSideSortRequestControl("givenName"))
        .addControl(new VLVRequestControl(0, 3, 0, 0));
    InternalSearchOperation internalSearch = getRootConnection().processSearch(request);
    assertEquals(internalSearch.getResultCode(), ResultCode.SUCCESS);

    ArrayList<DN> expectedDNOrder = new ArrayList<>();
    expectedDNOrder.add(aaccfJohnsonDN);    // Aaccf
    expectedDNOrder.add(aaronZimmermanDN);  // Aaron
    expectedDNOrder.add(albertZimmermanDN); // Albert, lower entry ID
    expectedDNOrder.add(albertSmithDN);     // Albert, higher entry ID

    assertEquals(getDNs(internalSearch.getSearchEntries()), expectedDNOrder);

    List<Control> responseControls = internalSearch.getResponseControls();
    assertNotNull(responseControls);
    assertEquals(responseControls.size(), 2);

    ServerSideSortResponseControl sortResponse = null;
    VLVResponseControl            vlvResponse  = null;
    for (Control c : responseControls)
    {
      if (c.getOID().equals(OID_SERVER_SIDE_SORT_RESPONSE_CONTROL))
      {
        sortResponse = getServerSideSortResponseControl(c);
      }
      else if (c.getOID().equals(OID_VLV_RESPONSE_CONTROL))
      {
        vlvResponse = getVLVResponseControl(c);
      }
      else
      {
        fail("Response control with unexpected OID " + c.getOID());
      }
    }

    assertNotNull(sortResponse);
    assertEquals(sortResponse.getResultCode(), 0);

    assertNotNull(vlvResponse);
    assertEquals(vlvResponse.getVLVResultCode(), 0);
    assertEquals(vlvResponse.getTargetPosition(), 1);
    assertEquals(vlvResponse.getContentCount(), 9);
  }



  /**
   * Tests performing an internal search using the VLV control to retrieve a
   * subset of the entries using an offset that isn't at the beginning of the
   * result set but is still completely within the bounds of that set.
   *
   * @throws  Exception  If an unexpected problem occurred.
   */
  @Test
  public void testInternalSearchByOffsetThreeOffset()
         throws Exception
  {
    populateDB();

    SearchRequest request = newSearchRequest("dc=example,dc=com", SearchScope.WHOLE_SUBTREE, "(objectClass=person)")
        .addControl(new ServerSideSortRequestControl("givenName"))
        .addControl(new VLVRequestControl(0, 3, 3, 0));
    InternalSearchOperation internalSearch = getRootConnection().processSearch(request);
    assertEquals(internalSearch.getResultCode(), ResultCode.SUCCESS);

    ArrayList<DN> expectedDNOrder = new ArrayList<>();
    expectedDNOrder.add(albertZimmermanDN); // Albert, lower entry ID
    expectedDNOrder.add(albertSmithDN);     // Albert, higher entry ID
    expectedDNOrder.add(lowercaseMcGeeDN);  // lowercase
    expectedDNOrder.add(margaretJonesDN);   // Maggie

    assertEquals(getDNs(internalSearch.getSearchEntries()), expectedDNOrder);

    List<Control> responseControls = internalSearch.getResponseControls();
    assertNotNull(responseControls);
    assertEquals(responseControls.size(), 2);

    ServerSideSortResponseControl sortResponse = null;
    VLVResponseControl            vlvResponse  = null;
    for (Control c : responseControls)
    {
      if (c.getOID().equals(OID_SERVER_SIDE_SORT_RESPONSE_CONTROL))
      {
        sortResponse = getServerSideSortResponseControl(c);
      }
      else if (c.getOID().equals(OID_VLV_RESPONSE_CONTROL))
      {
        vlvResponse = getVLVResponseControl(c);
      }
      else
      {
        fail("Response control with unexpected OID " + c.getOID());
      }
    }

    assertNotNull(sortResponse);
    assertEquals(sortResponse.getResultCode(), 0);

    assertNotNull(vlvResponse);
    assertEquals(vlvResponse.getVLVResultCode(), 0);
    assertEquals(vlvResponse.getTargetPosition(), 3);
    assertEquals(vlvResponse.getContentCount(), 9);
  }



  /**
   * Tests performing an internal search using the VLV control with a negative
   * target offset.
   *
   * @throws  Exception  If an unexpected problem occurred.
   */
  @Test
  public void testInternalSearchByOffsetNegativeOffset()
         throws Exception
  {
    populateDB();

    SearchRequest request = newSearchRequest("dc=example,dc=com", SearchScope.WHOLE_SUBTREE, "(objectClass=person)")
        .addControl(new ServerSideSortRequestControl("givenName"))
        .addControl(new VLVRequestControl(0, 3, -1, 0));
    InternalSearchOperation internalSearch = getRootConnection().processSearch(request);

    // It will be successful because it's not a critical control.
    assertEquals(internalSearch.getResultCode(), ResultCode.SUCCESS);

    List<Control> responseControls = internalSearch.getResponseControls();
    assertNotNull(responseControls);

    VLVResponseControl vlvResponse = getVLVResponseControl(responseControls);
    assertEquals(vlvResponse.getVLVResultCode(),
                 LDAPResultCode.OFFSET_RANGE_ERROR);
  }



  /**
   * Tests performing an internal search using the VLV control with an offset of
   * one but a beforeCount that puts the start position at a negative value.
   *
   * @throws  Exception  If an unexpected problem occurred.
   */
  @Test
  public void testInternalSearchByOffsetNegativeStartPosition()
         throws Exception
  {
    populateDB();

    SearchRequest request = newSearchRequest("dc=example,dc=com", SearchScope.WHOLE_SUBTREE, "(objectClass=person)")
        .addControl(new ServerSideSortRequestControl("givenName"))
        .addControl(new VLVRequestControl(3, 3, 1, 0));
    InternalSearchOperation internalSearch = getRootConnection().processSearch(request);

    // It will be successful because it's not a critical control.
    assertEquals(internalSearch.getResultCode(), ResultCode.SUCCESS);

    List<Control> responseControls = internalSearch.getResponseControls();
    assertNotNull(responseControls);

    VLVResponseControl vlvResponse = getVLVResponseControl(responseControls);
    assertEquals(vlvResponse.getVLVResultCode(), LDAPResultCode.SUCCESS);
  }



  /**
   * Tests performing an internal search using the VLV control with a start
   * start position beyond the end of the result set.
   *
   * @throws  Exception  If an unexpected problem occurred.
   */
  @Test
  public void testInternalSearchByOffsetStartPositionTooHigh() throws Exception
  {
    populateDB();

    SearchRequest request = newSearchRequest("dc=example,dc=com", SearchScope.WHOLE_SUBTREE, "(objectClass=person)")
        .addControl(new ServerSideSortRequestControl("givenName"))
        .addControl(new VLVRequestControl(3, 3, 30, 0));
    InternalSearchOperation internalSearch = getRootConnection().processSearch(request);
    assertEquals(internalSearch.getResultCode(), ResultCode.SUCCESS);

    ArrayList<DN> expectedDNOrder = new ArrayList<>();
    expectedDNOrder.add(maryJonesDN);       // Mary
    expectedDNOrder.add(samZweckDN);        // Sam
    expectedDNOrder.add(zorroDN);           // No first name

    assertEquals(getDNs(internalSearch.getSearchEntries()), expectedDNOrder);

    List<Control> responseControls = internalSearch.getResponseControls();
    assertNotNull(responseControls);

    VLVResponseControl vlvResponse = getVLVResponseControl(responseControls);
    assertEquals(vlvResponse.getVLVResultCode(), LDAPResultCode.SUCCESS);
    assertEquals(vlvResponse.getTargetPosition(), 10);
    assertEquals(vlvResponse.getContentCount(), 9);
  }



  /**
   * Tests performing an internal search using the VLV control with a start
   * start position within the bounds of the list but not enough remaining
   * entries to meet the afterCount
   *
   * @throws  Exception  If an unexpected problem occurred.
   */
  @Test
  public void testInternalSearchByOffsetIncompleteAfterCount() throws Exception
  {
    populateDB();

    SearchRequest request = newSearchRequest("dc=example,dc=com", SearchScope.WHOLE_SUBTREE, "(objectClass=person)")
        .addControl(new ServerSideSortRequestControl("givenName"))
        .addControl(new VLVRequestControl(0, 3, 7, 0));
    InternalSearchOperation internalSearch = getRootConnection().processSearch(request);
    assertEquals(internalSearch.getResultCode(), ResultCode.SUCCESS);

    ArrayList<DN> expectedDNOrder = new ArrayList<>();
    expectedDNOrder.add(maryJonesDN);       // Mary
    expectedDNOrder.add(samZweckDN);        // Sam
    expectedDNOrder.add(zorroDN);           // No first name

    assertEquals(getDNs(internalSearch.getSearchEntries()), expectedDNOrder);

    List<Control> responseControls = internalSearch.getResponseControls();
    assertNotNull(responseControls);
    assertEquals(responseControls.size(), 2);

    ServerSideSortResponseControl sortResponse = null;
    VLVResponseControl            vlvResponse  = null;
    for (Control c : responseControls)
    {
      if (c.getOID().equals(OID_SERVER_SIDE_SORT_RESPONSE_CONTROL))
      {
        sortResponse = getServerSideSortResponseControl(c);
      }
      else if (c.getOID().equals(OID_VLV_RESPONSE_CONTROL))
      {
        vlvResponse = getVLVResponseControl(c);
      }
      else
      {
        fail("Response control with unexpected OID " + c.getOID());
      }
    }

    assertNotNull(sortResponse);
    assertEquals(sortResponse.getResultCode(), 0);

    assertNotNull(vlvResponse);
    assertEquals(vlvResponse.getVLVResultCode(), 0);
    assertEquals(vlvResponse.getTargetPosition(), 7);
    assertEquals(vlvResponse.getContentCount(), 9);
  }



  /**
   * Tests performing an internal search using the VLV control to retrieve a
   * subset of the entries using an assertion value before any actual value in
   * the list.
   *
   * @throws  Exception  If an unexpected problem occurred.
   */
  @Test
  public void testInternalSearchByValueBeforeAll()
         throws Exception
  {
    populateDB();

    SearchRequest request = newSearchRequest("dc=example,dc=com", SearchScope.WHOLE_SUBTREE, "(objectClass=person)")
        .addControl(new ServerSideSortRequestControl("givenName"))
        .addControl(new VLVRequestControl(0, 3, ByteString.valueOfUtf8("a")));
    InternalSearchOperation internalSearch = getRootConnection().processSearch(request);
    assertEquals(internalSearch.getResultCode(), ResultCode.SUCCESS);

    ArrayList<DN> expectedDNOrder = new ArrayList<>();
    expectedDNOrder.add(aaccfJohnsonDN);    // Aaccf
    expectedDNOrder.add(aaronZimmermanDN);  // Aaron
    expectedDNOrder.add(albertZimmermanDN); // Albert, lower entry ID
    expectedDNOrder.add(albertSmithDN);     // Albert, higher entry ID

    assertEquals(getDNs(internalSearch.getSearchEntries()), expectedDNOrder);

    List<Control> responseControls = internalSearch.getResponseControls();
    assertNotNull(responseControls);
    assertEquals(responseControls.size(), 2);

    ServerSideSortResponseControl sortResponse = null;
    VLVResponseControl            vlvResponse  = null;
    for (Control c : responseControls)
    {
      if (c.getOID().equals(OID_SERVER_SIDE_SORT_RESPONSE_CONTROL))
      {
        sortResponse = getServerSideSortResponseControl(c);
      }
      else if (c.getOID().equals(OID_VLV_RESPONSE_CONTROL))
      {
        vlvResponse = getVLVResponseControl(c);
      }
      else
      {
        fail("Response control with unexpected OID " + c.getOID());
      }
    }

    assertNotNull(sortResponse);
    assertEquals(sortResponse.getResultCode(), 0);

    assertNotNull(vlvResponse);
    assertEquals(vlvResponse.getVLVResultCode(), 0);
    assertEquals(vlvResponse.getTargetPosition(), 1);
    assertEquals(vlvResponse.getContentCount(), 9);
  }



  /**
   * Tests performing an internal search using the VLV control to retrieve a
   * subset of the entries using an assertion value that matches the first value
   * in the list.
   *
   * @throws  Exception  If an unexpected problem occurred.
   */
  @Test
  public void testInternalSearchByValueMatchesFirst()
         throws Exception
  {
    populateDB();

    SearchRequest request = newSearchRequest("dc=example,dc=com", SearchScope.WHOLE_SUBTREE, "(objectClass=person)")
        .addControl(new ServerSideSortRequestControl("givenName"))
        .addControl(new VLVRequestControl(0, 3, ByteString.valueOfUtf8("aaccf")));
    InternalSearchOperation internalSearch = getRootConnection().processSearch(request);
    assertEquals(internalSearch.getResultCode(), ResultCode.SUCCESS);

    ArrayList<DN> expectedDNOrder = new ArrayList<>();
    expectedDNOrder.add(aaccfJohnsonDN);    // Aaccf
    expectedDNOrder.add(aaronZimmermanDN);  // Aaron
    expectedDNOrder.add(albertZimmermanDN); // Albert, lower entry ID
    expectedDNOrder.add(albertSmithDN);     // Albert, higher entry ID

    ArrayList<DN> returnedDNOrder = getDNs(internalSearch.getSearchEntries());

    assertEquals(returnedDNOrder, expectedDNOrder);

    List<Control> responseControls = internalSearch.getResponseControls();
    assertNotNull(responseControls);
    assertEquals(responseControls.size(), 2);

    ServerSideSortResponseControl sortResponse = null;
    VLVResponseControl            vlvResponse  = null;
    for (Control c : responseControls)
    {
      if (c.getOID().equals(OID_SERVER_SIDE_SORT_RESPONSE_CONTROL))
      {
        sortResponse = getServerSideSortResponseControl(c);
      }
      else if (c.getOID().equals(OID_VLV_RESPONSE_CONTROL))
      {
        vlvResponse = getVLVResponseControl(c);
      }
      else
      {
        fail("Response control with unexpected OID " + c.getOID());
      }
    }

    assertNotNull(sortResponse);
    assertEquals(sortResponse.getResultCode(), 0);

    assertNotNull(vlvResponse);
    assertEquals(vlvResponse.getVLVResultCode(), 0);
    assertEquals(vlvResponse.getTargetPosition(), 1);
    assertEquals(vlvResponse.getContentCount(), 9);
  }



  /**
   * Tests performing an internal search using the VLV control to retrieve a
   * subset of the entries using an assertion value that matches the third value
   * in the list.
   *
   * @throws  Exception  If an unexpected problem occurred.
   */
  @Test
  public void testInternalSearchByValueMatchesThird()
         throws Exception
  {
    populateDB();

    SearchRequest request = newSearchRequest("dc=example,dc=com", SearchScope.WHOLE_SUBTREE, "(objectClass=person)")
        .addControl(new ServerSideSortRequestControl("givenName"))
        .addControl(new VLVRequestControl(0, 3, ByteString.valueOfUtf8("albert")));
    InternalSearchOperation internalSearch = getRootConnection().processSearch(request);
    assertEquals(internalSearch.getResultCode(), ResultCode.SUCCESS);

    ArrayList<DN> expectedDNOrder = new ArrayList<>();
    expectedDNOrder.add(albertZimmermanDN); // Albert, lower entry ID
    expectedDNOrder.add(albertSmithDN);     // Albert, higher entry ID
    expectedDNOrder.add(lowercaseMcGeeDN);  // lowercase
    expectedDNOrder.add(margaretJonesDN);   // Maggie

    ArrayList<DN> returnedDNOrder = getDNs(internalSearch.getSearchEntries());

    assertEquals(returnedDNOrder, expectedDNOrder);

    List<Control> responseControls = internalSearch.getResponseControls();
    assertNotNull(responseControls);
    assertEquals(responseControls.size(), 2);

    ServerSideSortResponseControl sortResponse = null;
    VLVResponseControl            vlvResponse  = null;
    for (Control c : responseControls)
    {
      if (c.getOID().equals(OID_SERVER_SIDE_SORT_RESPONSE_CONTROL))
      {
        sortResponse = getServerSideSortResponseControl(c);
      }
      else if (c.getOID().equals(OID_VLV_RESPONSE_CONTROL))
      {
        vlvResponse = getVLVResponseControl(c);
      }
      else
      {
        fail("Response control with unexpected OID " + c.getOID());
      }
    }

    assertNotNull(sortResponse);
    assertEquals(sortResponse.getResultCode(), 0);

    assertNotNull(vlvResponse);
    assertEquals(vlvResponse.getVLVResultCode(), 0);
    assertEquals(vlvResponse.getTargetPosition(), 3);
    assertEquals(vlvResponse.getContentCount(), 9);
  }



  /**
   * Tests performing an internal search using the VLV control to retrieve a
   * subset of the entries using an assertion value that matches the third value
   * in the list and includes a nonzero before count.
   *
   * @throws  Exception  If an unexpected problem occurred.
   */
  @Test
  public void testInternalSearchByValueMatchesThirdWithBeforeCount()
         throws Exception
  {
    populateDB();

    SearchRequest request = newSearchRequest("dc=example,dc=com", SearchScope.WHOLE_SUBTREE, "(objectClass=person)")
        .addControl(new ServerSideSortRequestControl("givenName"))
        .addControl(new VLVRequestControl(1, 3, ByteString.valueOfUtf8("albert")));
    InternalSearchOperation internalSearch = getRootConnection().processSearch(request);
    assertEquals(internalSearch.getResultCode(), ResultCode.SUCCESS);

    ArrayList<DN> expectedDNOrder = new ArrayList<>();
    expectedDNOrder.add(aaronZimmermanDN);  // Aaron
    expectedDNOrder.add(albertZimmermanDN); // Albert, lower entry ID
    expectedDNOrder.add(albertSmithDN);     // Albert, higher entry ID
    expectedDNOrder.add(lowercaseMcGeeDN);  // lowercase
    expectedDNOrder.add(margaretJonesDN);   // Maggie

    assertEquals(getDNs(internalSearch.getSearchEntries()), expectedDNOrder);

    List<Control> responseControls = internalSearch.getResponseControls();
    assertNotNull(responseControls);
    assertEquals(responseControls.size(), 2);

    ServerSideSortResponseControl sortResponse = null;
    VLVResponseControl            vlvResponse  = null;
    for (Control c : responseControls)
    {
      if (c.getOID().equals(OID_SERVER_SIDE_SORT_RESPONSE_CONTROL))
      {
        sortResponse = getServerSideSortResponseControl(c);
      }
      else if (c.getOID().equals(OID_VLV_RESPONSE_CONTROL))
      {
        vlvResponse = getVLVResponseControl(c);
      }
      else
      {
        fail("Response control with unexpected OID " + c.getOID());
      }
    }

    assertNotNull(sortResponse);
    assertEquals(sortResponse.getResultCode(), 0);

    assertNotNull(vlvResponse);
    assertEquals(vlvResponse.getVLVResultCode(), 0);
    assertEquals(vlvResponse.getTargetPosition(), 3);
    assertEquals(vlvResponse.getContentCount(), 9);
  }

  private ArrayList<DN> getDNs(LinkedList<SearchResultEntry> entries)
  {
    ArrayList<DN> results = new ArrayList<>();
    for (Entry e : entries)
    {
      results.add(e.getName());
    }
    return results;
  }

  /**
   * Tests performing an internal search using the VLV control to retrieve a
   * subset of the entries using an assertion value that is after all values in
   * the list.
   *
   * @throws  Exception  If an unexpected problem occurred.
   */
  @Test
  public void testInternalSearchByValueAfterAll() throws Exception
  {
    populateDB();

    SearchRequest request = newSearchRequest("dc=example,dc=com", SearchScope.WHOLE_SUBTREE, "(objectClass=person)")
        .addControl(new ServerSideSortRequestControl("sn"))
        .addControl(new VLVRequestControl(0, 3, ByteString.valueOfUtf8("zz")));
    InternalSearchOperation internalSearch = getRootConnection().processSearch(request);

    // It will be successful because the control isn't critical.
    assertEquals(internalSearch.getResultCode(), ResultCode.SUCCESS);

    List<Control> responseControls = internalSearch.getResponseControls();
    assertNotNull(responseControls);

    VLVResponseControl vlvResponse = getVLVResponseControl(responseControls);
    assertEquals(vlvResponse.getVLVResultCode(),
                 LDAPResultCode.SUCCESS);
    assertEquals(vlvResponse.getTargetPosition(), 10);
    assertEquals(vlvResponse.getContentCount(), 9);
  }

  private ServerSideSortResponseControl getServerSideSortResponseControl(Control c) throws DirectoryException
  {
    if (c instanceof LDAPControl)
    {
      return ServerSideSortResponseControl.DECODER.decode(c.isCritical(), ((LDAPControl) c).getValue());
    }
    return (ServerSideSortResponseControl) c;
  }

  private VLVResponseControl getVLVResponseControl(List<Control> responseControls) throws DirectoryException
  {
    for (Control c : responseControls)
    {
      if (c.getOID().equals(OID_VLV_RESPONSE_CONTROL))
      {
        return getVLVResponseControl(c);
      }
    }
    fail("Expected to find VLVResponseControl");
    return null;
  }

  private VLVResponseControl getVLVResponseControl(Control c) throws DirectoryException
  {
    if (c instanceof LDAPControl)
    {
      return VLVResponseControl.DECODER.decode(c.isCritical(), ((LDAPControl) c).getValue());
    }
    return (VLVResponseControl) c;
  }
}
