/*
 * 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.mockito.Mockito.when;
import static org.opends.server.ConfigurationMock.legacyMockCfg;
import static org.opends.server.TestCaseUtils.makeEntry;
import static org.opends.server.protocols.internal.InternalClientConnection.getRootConnection;
import static org.opends.server.protocols.internal.Requests.newSearchRequest;
import static org.opends.server.util.CollectionUtils.*;
import static org.opends.server.util.ServerConstants.*;
import static org.testng.Assert.fail;

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

import org.forgerock.opendj.config.server.ConfigException;
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.DirectoryServerTestCase;
import org.opends.server.TestCaseUtils;
import org.opends.server.admin.std.meta.BackendVLVIndexCfgDefn.Scope;
import org.opends.server.admin.std.server.BackendVLVIndexCfg;
import org.opends.server.admin.std.server.PDBBackendCfg;
import org.opends.server.backends.pdb.PDBBackend;
import org.opends.server.controls.ServerSideSortRequestControl;
import org.opends.server.controls.ServerSideSortResponseControl;
import org.opends.server.controls.VLVRequestControl;
import org.opends.server.controls.VLVResponseControl;
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.Control;
import org.opends.server.types.DN;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.Entry;
import org.opends.server.types.SearchResultEntry;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

/**
 * This class contains a number of test cases for the controls which are supported within the
 * backend. In particular, VLV, paged results, and server side sorting controls.
 */
@SuppressWarnings("javadoc")
public class ControlsTestCase extends DirectoryServerTestCase
{

  private static final String BACKEND_BASE_DN = "dc=pluggable-vlv,dc=com";
  private static final String BACKEND_NAME = "pluggable-vlv";
  private static final String VLV_FILTER = "(objectClass=person)";

  // @formatter:off
  private static final User[] USERS = {
    user(givenNames("Alice"),              surname("Zorro"),       employeeNumber(0)),
    user(givenNames("Bob", "Robert"),      surname("deBuilder"),   employeeNumber(1)),
    user(givenNames("Charlie"),            surname("Speed"),       employeeNumber(2)),
    user(givenNames("Mickie"),             surname("Mouse"),       employeeNumber(3)),
    user(givenNames("William", "Bill"),    surname("Beak"),        employeeNumber(4)),
    user(givenNames(),                     surname("Prince"),      employeeNumber(5)),
    user(givenNames("Charlie"),            surname("Chalk"),       employeeNumber(6)),
    user(givenNames("Albert"),             surname("DEinstein"),   employeeNumber(7)),
    user(givenNames("Mini"),               surname("Mouse"),       employeeNumber(8)),
  };
  // @formatter:on

  private static final int CONTENT_COUNT = USERS.length;

  /** Indexed: ordered by ascending givenName then entryID */
  private static final String SORT_ORDER_1 = "givenName";

  /** Indexed: ordered by descending sn and descending employeeNumber then entryID */
  private static final String SORT_ORDER_2 = "-sn -employeeNumber";

  /** Unindexed: ordered by ascending sn and ascending employee number then entryID */
  private static final String SORT_ORDER_3 = "sn employeeNumber";

  /** Indexed: ordered by ascending givenName then entryID */
  private static final String SORT_ORDER_4 = "surname:caseExactOrderingMatch";

  /** Ordered by {@link #SORT_ORDER_1} */
  private static final List<Integer> USERS_BY_ENTRY_ID = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8);

  /** Indexed: ordered by {@link #SORT_ORDER_1} */
  private static final List<Integer> USERS_BY_SORT_ORDER_1 = Arrays.asList(7, 0, 4, 1, 2, 6, 3, 8, 5);

  /** Indexed: ordered by {@link #SORT_ORDER_2} */
  private static final List<Integer> USERS_BY_SORT_ORDER_2 = Arrays.asList(0, 2, 5, 8, 3, 7, 1, 6, 4);

  /** Unindexed: ordered by {@link #SORT_ORDER_3} */
  private static final List<Integer> USERS_BY_SORT_ORDER_3 = Arrays.asList(4, 6, 1, 7, 3, 8, 5, 2, 0);

  /** Unindexed: ordered by {@link #SORT_ORDER_4} */
  private static final List<Integer> USERS_BY_SORT_ORDER_4 = Arrays.asList(4, 6, 7, 3, 8, 5, 2, 0, 1);

  private PDBBackend backend;

  @BeforeClass
  public void beforeClass() throws Exception
  {
    TestCaseUtils.startServer();

    final DN baseDN = DN.valueOf(BACKEND_BASE_DN);

    final PDBBackendCfg backendCfg = legacyMockCfg(PDBBackendCfg.class);
    when(backendCfg.dn()).thenReturn(baseDN);
    when(backendCfg.getBackendId()).thenReturn(BACKEND_NAME);
    when(backendCfg.getBaseDN()).thenReturn(newTreeSet(baseDN));
    when(backendCfg.listBackendIndexes()).thenReturn(new String[0]);
    when(backendCfg.listBackendVLVIndexes()).thenReturn(new String[] { SORT_ORDER_1, SORT_ORDER_2 });

    when(backendCfg.getDBDirectory()).thenReturn(BACKEND_NAME);
    when(backendCfg.getDBDirectoryPermissions()).thenReturn("755");
    when(backendCfg.getDBCacheSize()).thenReturn(0L);
    when(backendCfg.getDBCachePercent()).thenReturn(20);

    createVlvIndex(baseDN, backendCfg, SORT_ORDER_1);
    createVlvIndex(baseDN, backendCfg, SORT_ORDER_2);

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

    backend.addEntry(makeEntry("dn: " + BACKEND_BASE_DN, "objectclass: top", "objectclass: domain"), null);
    for (final User user : USERS)
    {
      backend.addEntry(user.toEntry(), null);
    }
  }

  private void createVlvIndex(final DN baseDN, final PDBBackendCfg backendCfg, final String sortOrder)
      throws ConfigException
  {
    final BackendVLVIndexCfg vlvIndexCfg = legacyMockCfg(BackendVLVIndexCfg.class);
    when(vlvIndexCfg.getName()).thenReturn(sortOrder);
    when(vlvIndexCfg.getBaseDN()).thenReturn(baseDN);
    when(vlvIndexCfg.getFilter()).thenReturn(VLV_FILTER);
    when(vlvIndexCfg.getScope()).thenReturn(Scope.WHOLE_SUBTREE);
    when(vlvIndexCfg.getSortOrder()).thenReturn(sortOrder);
    when(backendCfg.getBackendVLVIndex(sortOrder)).thenReturn(vlvIndexCfg);
  }

  @DataProvider
  private Object[][] encodedKeyDataProvider()
  {
    // @formatter:off
    return new Object[][] {
      // Null keys sort after everything else.
      { null,           null,           0 },
      { "",             null,          -1 },
      { null,           "",             1 },
      { "00",           null,          -1 },
      { null,           "00",           1 },
      { "ff",           null,          -1 },
      { null,           "ff",           1 },

      // Empty keys sort before everything else.
      { "",             "",             0 },
      { "00",           "",             1 },
      { "",             "00",          -1 },
      { "ff",           "",             1 },
      { "",             "ff",          -1 },

      // Bytes comparisons are unsigned.
      { "00",           "00",           0 },
      { "00",           "ff",          -1 },
      { "ff",           "00",           1 },
      { "ff",           "ff",           0 },

      // Short keys sort before long keys.
      { "0000",         "00",           1 },
      { "00",           "0000",        -1 },
      { "ffff",         "ff",           1 },
      { "ff",           "ffff",        -1 },
      { "0000",         "0000",         0 },
      { "ffff",         "ffff",         0 },
      { "0000",         "ffff",        -1 },
      { "ffff",         "0000",         1 },
    };
    // @formatter:on
  }

  @Test(dataProvider = "encodedKeyDataProvider")
  public void vlvKeyEncodingGenerateCorrectAscendingSortOrder(String key1, String key2, int expectedCompareResult)
  {
    ByteString bytes1 = key1 != null ? ByteString.valueOfHex(key1) : null;
    ByteStringBuilder encodedBytes1 = new ByteStringBuilder();
    VLVIndex.encodeVLVKeyValue(bytes1, encodedBytes1, true);

    ByteString bytes2 = key2 != null ? ByteString.valueOfHex(key2) : null;
    ByteStringBuilder encodedBytes2 = new ByteStringBuilder();
    VLVIndex.encodeVLVKeyValue(bytes2, encodedBytes2, true);

    int actualResult = Math.min(Math.max(encodedBytes1.compareTo(encodedBytes2), -1), 1);
    assertThat(actualResult).isEqualTo(expectedCompareResult);
  }

  @Test(dataProvider = "encodedKeyDataProvider")
  public void vlvKeyEncodingGenerateCorrectDescendingSortOrder(String key1, String key2, int expectedCompareResult)
  {
    ByteString bytes1 = key1 != null ? ByteString.valueOfHex(key1) : null;
    ByteStringBuilder encodedBytes1 = new ByteStringBuilder();
    VLVIndex.encodeVLVKeyValue(bytes1, encodedBytes1, false);

    ByteString bytes2 = key2 != null ? ByteString.valueOfHex(key2) : null;
    ByteStringBuilder encodedBytes2 = new ByteStringBuilder();
    VLVIndex.encodeVLVKeyValue(bytes2, encodedBytes2, false);

    int actualResult = Math.min(Math.max(encodedBytes1.compareTo(encodedBytes2), -1), 1);
    assertThat(actualResult).isEqualTo(-expectedCompareResult);
  }

  @DataProvider
  private Object[][] indexedVlvByAssertionDataProvider()
  {
    // @formatter:off
    return new Object[][] {
      {
        SORT_ORDER_2,
        beforeCount(0),
        afterCount(5),
        assertion("ZZ"),                                // before first
        USERS_BY_SORT_ORDER_2.subList(0, 6),
        expectedPosition(1)
      },
      {
        SORT_ORDER_2,
        beforeCount(0),
        afterCount(5),
        assertion("zorro"),                             // matches first
        USERS_BY_SORT_ORDER_2.subList(0, 6),
        expectedPosition(1)
      },
      {
        SORT_ORDER_2,
        beforeCount(0),
        afterCount(3),
        assertion("PRINCE"),                            // matches third
        USERS_BY_SORT_ORDER_2.subList(2, 6),
        expectedPosition(3)
      },
      {
        SORT_ORDER_2,
        beforeCount(1),
        afterCount(3),
        assertion("prince"),                            // matches third
        USERS_BY_SORT_ORDER_2.subList(1, 6),
        expectedPosition(3)
      },
      {
        SORT_ORDER_2,
        beforeCount(10),
        afterCount(10),
        assertion("prince"),                            // matches third
        USERS_BY_SORT_ORDER_2,
        expectedPosition(3)
      },
      {
        SORT_ORDER_2,
        beforeCount(0),
        afterCount(3),
        assertion("a"),                                 // after all
        USERS_BY_SORT_ORDER_2.subList(9, 9),            // nothing
        expectedPosition(10)
      },
    };
    // @formatter:on
  }

  @Test(dataProvider = "indexedVlvByAssertionDataProvider")
  public void indexedVlvByAssertionShouldReturnPageOfResultsInCorrectOrder(final String sortOrder,
      final int beforeCount, final int afterCount, final String assertion, final List<Integer> expectedOrder,
      final int expectedPosition) throws Exception
  {
    vlvByAssertion(sortOrder, beforeCount, afterCount, assertion, expectedOrder, expectedPosition);
  }

  @DataProvider
  private Object[][] indexedVlvByOffsetDataProvider()
  {
    // @formatter:off
    return new Object[][] {
      {
        SORT_ORDER_1,
        beforeCount(0),
        afterCount(3),
        offset(1),
        USERS_BY_SORT_ORDER_1.subList(0, 4)
      },
      {
        SORT_ORDER_1,
        beforeCount(0),
        afterCount(3),
        offset(3),
        USERS_BY_SORT_ORDER_1.subList(2, 6)
      },
      {
        SORT_ORDER_1,
        beforeCount(0),
        afterCount(0),
        offset(5),
        USERS_BY_SORT_ORDER_1.subList(4, 5)
      },
      {
        SORT_ORDER_1,
        beforeCount(3),
        afterCount(3),
        offset(5),
        USERS_BY_SORT_ORDER_1.subList(1, 8)
      },
      {
        SORT_ORDER_1,
        beforeCount(0),
        afterCount(3),
        offset(0),                                      // 0 is equivalent to 1
        USERS_BY_SORT_ORDER_1.subList(0, 4)
      },
      {
        SORT_ORDER_1,
        beforeCount(3),                                 // underflow
        afterCount(3),
        offset(1),
        USERS_BY_SORT_ORDER_1.subList(0, 4)
      },
      {
        SORT_ORDER_1,
        beforeCount(3),
        afterCount(3),
        offset(30),                                     // overflow
        USERS_BY_SORT_ORDER_1.subList(6, 9)
      },
      {
        SORT_ORDER_1,
        beforeCount(3),
        afterCount(3),                                  // overflow
        offset(7),
        USERS_BY_SORT_ORDER_1.subList(3, 9)
      },
      {
        SORT_ORDER_1,
        beforeCount(30),                                // underflow
        afterCount(30),                                 // overflow
        offset(7),
        USERS_BY_SORT_ORDER_1                           // everything
      },
      {
        SORT_ORDER_1,
        beforeCount(0),                                 // underflow
        afterCount(0),                                  // overflow
        offset(30),
        USERS_BY_SORT_ORDER_1.subList(9, 9)             //  nothing
      },

    };
    // @formatter:on
  }

  @Test(dataProvider = "indexedVlvByOffsetDataProvider")
  public void indexedVlvByOffsetShouldReturnPageOfResultsInCorrectOrder(final String sortOrder, final int beforeCount,
      final int afterCount, final int offset, final List<Integer> expectedOrder) throws Exception
  {
    vlvByOffset(sortOrder, beforeCount, afterCount, offset, expectedOrder);
  }

  @Test
  public void indexedVlvByOffsetShouldReturnOffsetRangeErrorForNegativeOffsets() throws Exception
  {
    final List<Control> responseControls = vlvByOffset0(SORT_ORDER_2, 0, 3, -1, USERS_BY_ENTRY_ID);
    assertThat(responseControls).isNotEmpty();
    final VLVResponseControl vlvResponse = getVLVResponseControl(responseControls);
    assertThat(vlvResponse.getVLVResultCode()).isEqualTo(LDAPResultCode.OFFSET_RANGE_ERROR);
  }

  @DataProvider
  private Object[][] unindexedVlvByAssertionDataProvider()
  {
    // @formatter:off
    return new Object[][] {
      {
        SORT_ORDER_3,
        beforeCount(0),
        afterCount(3),
        assertion("a"),                                 // before first
        USERS_BY_SORT_ORDER_3.subList(0, 4),
        expectedPosition(1)
      },
      {
        SORT_ORDER_3,
        beforeCount(0),
        afterCount(3),
        assertion("beak"),                              // matches first
        USERS_BY_SORT_ORDER_3.subList(0, 4),
        expectedPosition(1)
      },
      {
        SORT_ORDER_3,
        beforeCount(0),
        afterCount(3),
        assertion("debuilder"),                         // matches third
        USERS_BY_SORT_ORDER_3.subList(2, 6),
        expectedPosition(3)
      },
      {
        SORT_ORDER_3,
        beforeCount(1),
        afterCount(3),
        assertion("debuilder"),                         // matches third
        USERS_BY_SORT_ORDER_3.subList(1, 6),
        expectedPosition(3)
      },
      {
        SORT_ORDER_3,
        beforeCount(0),
        afterCount(3),
        assertion("zz"),                                // after all
        USERS_BY_SORT_ORDER_3.subList(9, 9),            // nothing
        expectedPosition(10)
      },
      {
        SORT_ORDER_4,
        beforeCount(0),
        afterCount(10),
        assertion("A"),                                 // before all
        USERS_BY_SORT_ORDER_4,                          // everything
        expectedPosition(1)
      },
    };
    // @formatter:on
  }

  @Test(dataProvider = "unindexedVlvByAssertionDataProvider")
  public void unindexedVlvByAssertionShouldReturnPageOfResultsInCorrectOrder(final String sortOrder,
      final int beforeCount, final int afterCount, final String assertion, final List<Integer> expectedOrder,
      final int expectedPosition) throws Exception
  {
    vlvByAssertion(sortOrder, beforeCount, afterCount, assertion, expectedOrder, expectedPosition);
  }

  @DataProvider
  private Object[][] unindexedVlvByOffsetDataProvider()
  {
    // @formatter:off
    return new Object[][] {
      {
        SORT_ORDER_3,
        beforeCount(0),
        afterCount(3),
        offset(1),
        USERS_BY_SORT_ORDER_3.subList(0, 4)
      },
      {
        SORT_ORDER_3,
        beforeCount(0),
        afterCount(3),
        offset(3),
        USERS_BY_SORT_ORDER_3.subList(2, 6)
      },
      {
        SORT_ORDER_3,
        beforeCount(0),
        afterCount(0),
        offset(5),
        USERS_BY_SORT_ORDER_3.subList(4, 5)
      },
      {
        SORT_ORDER_3,
        beforeCount(3),
        afterCount(3),
        offset(5),
        USERS_BY_SORT_ORDER_3.subList(1, 8)
      },
      {
        SORT_ORDER_3,
        beforeCount(0),
        afterCount(3),
        offset(0),                                      // 0 is equivalent to 1
        USERS_BY_SORT_ORDER_3.subList(0, 4)
      },
      {
        SORT_ORDER_3,
        beforeCount(3),                                 // underflow
        afterCount(3),
        offset(1),
        USERS_BY_SORT_ORDER_3.subList(0, 4)
      },
      {
        SORT_ORDER_3,
        beforeCount(3),
        afterCount(3),
        offset(30),                                     // overflow
        USERS_BY_SORT_ORDER_3.subList(6, 9)
      },
      {
        SORT_ORDER_3,
        beforeCount(3),
        afterCount(3),                                  // overflow
        offset(7),
        USERS_BY_SORT_ORDER_3.subList(3, 9)
      },
      {
        SORT_ORDER_3,
        beforeCount(30),                                // underflow
        afterCount(30),                                 // overflow
        offset(7),
        USERS_BY_SORT_ORDER_3                           // everything
      },
      {
        SORT_ORDER_3,
        beforeCount(0),                                 // underflow
        afterCount(0),                                  // overflow
        offset(30),
        USERS_BY_SORT_ORDER_3.subList(9, 9)             //  nothing
      },

    };
    // @formatter:on
  }

  @Test(dataProvider = "unindexedVlvByOffsetDataProvider")
  public void unindexedVlvByOffsetShouldReturnPageOfResultsInCorrectOrder(final String sortOrder,
      final int beforeCount, final int afterCount, final int offset, final List<Integer> expectedOrder)
      throws Exception
  {
    vlvByOffset(sortOrder, beforeCount, afterCount, offset, expectedOrder);
  }

  @AfterClass
  public void afterClass() throws Exception
  {
    backend.finalizeBackend();
    backend = null;
  }

  private static int employeeNumber(final int i)
  {
    return i;
  }

  private static List<String> givenNames(final String... names)
  {
    return Arrays.asList(names);
  }

  private static String surname(final String name)
  {
    return name;
  }

  private static User user(final List<String> givenNames, final String sn, final int employeeNumber)
  {
    return new User(givenNames, sn, employeeNumber);
  }

  private int beforeCount(final int i)
  {
    return i;
  }

  private int afterCount(final int i)
  {
    return i;
  }

  private String assertion(final String s)
  {
    return s;
  }

  private int offset(final int i)
  {
    return i;
  }

  private int expectedPosition(final int i)
  {
    return i;
  }

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

  private List<DN> getDNs(List<Integer> expectedOrder) throws Exception
  {
    List<DN> dns = new ArrayList<>(expectedOrder.size());
    for (int i : expectedOrder)
    {
      dns.add(USERS[i].toDN());
    }
    return dns;
  }

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

  private ServerSideSortResponseControl getServerSideSortResponseControl(final List<Control> responseControls)
      throws DirectoryException
  {
    for (final Control c : responseControls)
    {
      if (c.getOID().equals(OID_SERVER_SIDE_SORT_RESPONSE_CONTROL))
      {
        return getServerSideSortResponseControl(c);
      }
    }
    fail("Expected to find ServerSideSortResponseControl");
    return null;
  }

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

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

  private void vlvByAssertion(final String sortOrder, final int beforeCount, final int afterCount,
      final String assertion, final List<Integer> expectedOrder, final int expectedPosition) throws Exception
  {
    final SearchRequest request =
        newSearchRequest(BACKEND_BASE_DN, SearchScope.WHOLE_SUBTREE, VLV_FILTER).addControl(
            new ServerSideSortRequestControl(mangleSortOrder(sortOrder))).addControl(
            new VLVRequestControl(beforeCount, afterCount, ByteString.valueOfUtf8(assertion)));
    final InternalSearchOperation internalSearch = getRootConnection().processSearch(request);
    assertThat(internalSearch.getResultCode()).isEqualTo(ResultCode.SUCCESS);
    assertThat(getDNs(internalSearch.getSearchEntries())).isEqualTo(getDNs(expectedOrder));

    final List<Control> responseControls = internalSearch.getResponseControls();
    assertThat(responseControls).hasSize(2);

    final ServerSideSortResponseControl sortResponse = getServerSideSortResponseControl(responseControls);
    assertThat(sortResponse.getResultCode()).isEqualTo(LDAPResultCode.SUCCESS);

    final VLVResponseControl vlvResponse = getVLVResponseControl(responseControls);
    assertThat(vlvResponse.getVLVResultCode()).isEqualTo(LDAPResultCode.SUCCESS);
    assertThat(vlvResponse.getTargetPosition()).isEqualTo(expectedPosition);
    assertThat(vlvResponse.getContentCount()).isEqualTo(CONTENT_COUNT);
  }

  private String mangleSortOrder(final String sortOrder)
  {
    return sortOrder.replaceAll(" ", ",");
  }

  private void vlvByOffset(final String sortOrder, final int beforeCount, final int afterCount, final int offset,
      final List<Integer> expectedOrder) throws Exception, DirectoryException
  {
    final List<Control> responseControls = vlvByOffset0(sortOrder, beforeCount, afterCount, offset, expectedOrder);
    assertThat(responseControls).hasSize(2);

    final ServerSideSortResponseControl sortResponse = getServerSideSortResponseControl(responseControls);
    assertThat(sortResponse.getResultCode()).isEqualTo(LDAPResultCode.SUCCESS);

    final VLVResponseControl vlvResponse = getVLVResponseControl(responseControls);
    assertThat(vlvResponse.getVLVResultCode()).isEqualTo(LDAPResultCode.SUCCESS);
    final int offsetInRange = Math.min(Math.max(offset, 1), CONTENT_COUNT + 1);
    assertThat(vlvResponse.getTargetPosition()).isEqualTo(offsetInRange);
    assertThat(vlvResponse.getContentCount()).isEqualTo(CONTENT_COUNT);
  }

  private List<Control> vlvByOffset0(final String sortOrder, final int beforeCount, final int afterCount,
      final int offset, final List<Integer> expectedOrder) throws Exception
  {
    final SearchRequest request =
        newSearchRequest(BACKEND_BASE_DN, SearchScope.WHOLE_SUBTREE, VLV_FILTER).addControl(
            new ServerSideSortRequestControl(mangleSortOrder(sortOrder))).addControl(
            new VLVRequestControl(beforeCount, afterCount, offset, 0));
    final InternalSearchOperation internalSearch = getRootConnection().processSearch(request);

    assertThat(internalSearch.getResultCode()).isEqualTo(ResultCode.SUCCESS);
    assertThat(getDNs(internalSearch.getSearchEntries())).isEqualTo(getDNs(expectedOrder));
    return internalSearch.getResponseControls();
  }

  private static class User
  {
    private final int employeeNumber;
    private final List<String> givenNames;
    private final String sn;

    private User(final List<String> givenNames, final String sn, final int employeeNumber)
    {
      this.givenNames = givenNames;
      this.sn = sn;
      this.employeeNumber = employeeNumber;
    }

    private DN toDN() throws Exception
    {
      return DN.valueOf(String.format("employeeNumber=%d,%s", employeeNumber, BACKEND_BASE_DN));
    }

    private Entry toEntry() throws Exception
    {
      final List<String> ldif = new ArrayList<>();
      ldif.add("dn: " + toDN());
      ldif.add("objectClass: top");
      ldif.add("objectClass: person");
      ldif.add("objectClass: organizationalPerson");
      ldif.add("objectClass: inetOrgPerson");
      ldif.add(String.format("sn: %s", sn));
      ldif.add(String.format("employeeNumber: %d", employeeNumber));
      for (final String givenName : givenNames)
      {
        ldif.add(String.format("givenName: %s", givenName));
      }
      if (givenNames.isEmpty())
      {
        ldif.add(String.format("cn: %s", sn));
      }
      else
      {
        ldif.add(String.format("cn: %s %s", givenNames.get(0), sn));
      }
      return makeEntry(ldif.toArray(new String[0]));
    }
  }
}
