/*
 * 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.*;
import static org.mockito.Mockito.*;
import static org.opends.server.ConfigurationMock.*;
import static org.opends.server.util.CollectionUtils.*;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.util.promise.NeverThrowsException;
import org.forgerock.util.promise.PromiseImpl;
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.server.BackendIndexCfg;
import org.opends.server.admin.std.server.PDBBackendCfg;
import org.opends.server.backends.pdb.PDBStorage;
import org.opends.server.backends.pluggable.spi.AccessMode;
import org.opends.server.backends.pluggable.spi.Cursor;
import org.opends.server.backends.pluggable.spi.ReadOperation;
import org.opends.server.backends.pluggable.spi.ReadableTransaction;
import org.opends.server.backends.pluggable.spi.SequentialCursor;
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.core.MemoryQuota;
import org.opends.server.core.ServerContext;
import org.opends.server.extensions.DiskSpaceMonitor;
import org.opends.server.types.DN;
import org.opends.server.types.DirectoryException;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

@Test(groups = { "precommit", "pluggablebackend" }, sequential = true)
public class DN2IDTest extends DirectoryServerTestCase
{
  private final TreeName dn2IDTreeName = new TreeName("base-dn", "index-id");
  private DN baseDN;
  private DN2ID dn2ID;
  private PDBStorage storage;

  // FIXME: This is required since PDBStorage is now using
  // DirectoryServer static method.
  @BeforeClass
  public void startServer() throws Exception
  {
    TestCaseUtils.startServer();
  }

  @BeforeMethod
  public void setUp() throws Exception
  {
    ServerContext serverContext = mock(ServerContext.class);
    when(serverContext.getMemoryQuota()).thenReturn(new MemoryQuota());
    when(serverContext.getDiskSpaceMonitor()).thenReturn(mock(DiskSpaceMonitor.class));

    storage = new PDBStorage(createBackendCfg(), serverContext);
    storage.open(AccessMode.READ_WRITE);
    storage.write(new WriteOperation()
    {
      @Override
      public void run(WriteableTransaction txn) throws Exception
      {
        txn.openTree(dn2IDTreeName, true);
      }
    });

    baseDN = dn("dc=example, dc=com");
    dn2ID = new DN2ID(dn2IDTreeName, baseDN);
  }

  @AfterMethod
  public void tearDown()
  {
    storage.close();
    storage.removeStorageFiles();
  }

  private void populate() throws DirectoryException, Exception
  {
    final String[] dns =
      {
                                 "dc=example,dc=com",
                      "ou=Devices,dc=example,dc=com",
              "cn=dev0,ou=Devices,dc=example,dc=com",
                       "ou=People,dc=example,dc=com",
                "cn=foo,ou=People,dc=example,dc=com",
             "cn=barbar,ou=People,dc=example,dc=com",
             "cn=foofoo,ou=People,dc=example,dc=com",
                "cn=bar,ou=People,dc=example,dc=com",
        "cn=dev0,cn=bar,ou=People,dc=example,dc=com",
        "cn=dev1,cn=bar,ou=People,dc=example,dc=com"
      };

    for (int i = 0; i < dns.length; i++)
    {
      put(dn(dns[i]), i + 1);
    }
  }

  @Test
  public void testCanAddDN() throws Exception
  {
    populate();

    assertThat(get("dc=example,dc=com")).isEqualTo(id(1));
    assertThat(get("ou=People,dc=example,dc=com")).isEqualTo(id(4));
    assertThat(get("cn=dev1,cn=bar,ou=People,dc=example,dc=com")).isEqualTo(id(10));
  }

  @Test
  public void testIsChild() throws DirectoryException, Exception {
    put(dn("dc=example,dc=com"), 1);
    put(dn("ou=People,dc=example,dc=com"), 2);
    put(dn("uid=user.0,ou=People,dc=example,dc=com"), 3);
    put(dn("uid=user.1,ou=People,dc=example,dc=com"), 4);
    put(dn("uid=user.10,ou=People,dc=example,dc=com"), 5);

    storage.read(new ReadOperation<Void>()
    {
      @Override
      public Void run(ReadableTransaction txn) throws Exception
      {
        try (final Cursor<ByteString, ByteString> cursor = txn.openCursor(dn2ID.getName()))
        {
          cursor.next();
          final ByteString rootDN = cursor.getKey();
          cursor.next();
          final ByteString parentDN = cursor.getKey();
          cursor.next();
          assertThat(DN2ID.isChild(rootDN, parentDN)).isTrue();

          final ByteString childDN = cursor.getKey();
          assertThat(DN2ID.isChild(parentDN, childDN)).isTrue();

          cursor.next();
          final ByteString otherChildDN = cursor.getKey();
          assertThat(DN2ID.isChild(parentDN, otherChildDN)).isTrue();
          assertThat(DN2ID.isChild(childDN, otherChildDN)).isFalse();

          final ByteString lastChildDN = cursor.getKey();
          assertThat(DN2ID.isChild(parentDN, lastChildDN)).isTrue();
          assertThat(DN2ID.isChild(otherChildDN, lastChildDN)).isFalse();
          assertThat(DN2ID.isChild(childDN, lastChildDN)).isFalse();
        }
        return null;
      }
    });

  }

  @Test
  public void testGetNonExistingDNReturnNull() throws Exception
  {
    assertThat(get("dc=non,dc=existing")).isNull();
  }

  @Test
  public void testCanRemove() throws Exception
  {
    populate();

    assertThat(get("ou=People,dc=example,dc=com")).isNotNull();
    assertThat(remove("ou=People,dc=example,dc=com")).isTrue();
    assertThat(get("ou=People,dc=example,dc=com")).isNull();
  }

  @Test
  public void testRemoveNonExistingEntry() throws Exception
  {
    assertThat(remove("dc=non,dc=existing")).isFalse();
  }

  @Test
  public void testTraverseChildren() throws Exception
  {
    populate();
    assertThat(traverseChildren("ou=People,dc=example,dc=com"))
      .containsExactly(
                       get("cn=bar,ou=People,dc=example,dc=com"),
                    get("cn=barbar,ou=People,dc=example,dc=com"),
                       get("cn=foo,ou=People,dc=example,dc=com"),
                    get("cn=foofoo,ou=People,dc=example,dc=com"));
  }

  @Test
  public void testTraverseSubordinates() throws Exception
  {
    populate();
    assertThat(traverseSubordinates("ou=People,dc=example,dc=com"))
      .containsExactly(
                      get("cn=bar,ou=People,dc=example,dc=com"),
              get("cn=dev0,cn=bar,ou=People,dc=example,dc=com"),
              get("cn=dev1,cn=bar,ou=People,dc=example,dc=com"),
                   get("cn=barbar,ou=People,dc=example,dc=com"),
                      get("cn=foo,ou=People,dc=example,dc=com"),
                   get("cn=foofoo,ou=People,dc=example,dc=com"));
  }

  private EntryID get(final String dn) throws Exception
  {
    return storage.read(new ReadOperation<EntryID>()
    {
      @Override
      public EntryID run(ReadableTransaction txn) throws Exception
      {
        return dn2ID.get(txn, dn(dn));
      }
    });
  }

  private List<EntryID> traverseChildren(final String dn) throws Exception
  {
    return storage.read(new ReadOperation<List<EntryID>>()
    {
      @Override
      public List<EntryID> run(ReadableTransaction txn) throws Exception
      {
        try (final SequentialCursor<Void, EntryID> cursor = dn2ID.openChildrenCursor(txn, dn(dn)))
        {
          return getAllIDs(cursor);
        }
      }
    });
  }

  private List<EntryID> traverseSubordinates(final String dn) throws Exception
  {
    return storage.read(new ReadOperation<List<EntryID>>()
    {
      @Override
      public List<EntryID> run(ReadableTransaction txn) throws Exception
      {
        try (final SequentialCursor<Void, EntryID> cursor = dn2ID.openSubordinatesCursor(txn, dn(dn)))
        {
          return getAllIDs(cursor);
        }
      }
    });
  }

  private static <K, V> List<V> getAllIDs(SequentialCursor<K, V> cursor) {
    final List<V> values = new ArrayList<>();
    while(cursor.next()) {
      values.add(cursor.getValue());
    }
    return values;
  }

  private void put(final DN dn, final long id) throws Exception
  {
    storage.write(new WriteOperation()
    {
      @Override
      public void run(WriteableTransaction txn) throws Exception
      {
        dn2ID.put(txn, dn, new EntryID(id));
      }
    });
  }

  private boolean remove(final String dn) throws Exception
  {
    final PromiseImpl<Boolean, NeverThrowsException> p = PromiseImpl.create();
    storage.write(new WriteOperation()
    {
      @Override
      public void run(WriteableTransaction txn) throws Exception
      {
        p.handleResult(dn2ID.remove(txn, dn(dn)));
      }
    });
    return p.get(10, TimeUnit.SECONDS);
  }

  private static DN dn(String dn) throws DirectoryException
  {
    return DN.valueOf(dn);
  }

  private static EntryID id(long id)
  {
    return new EntryID(id);
  }

  private static PDBBackendCfg createBackendCfg() throws ConfigException, DirectoryException
  {
    String homeDirName = "pdb_test";
    PDBBackendCfg backendCfg = legacyMockCfg(PDBBackendCfg.class);

    when(backendCfg.getBackendId()).thenReturn("persTest" + homeDirName);
    when(backendCfg.getDBDirectory()).thenReturn(homeDirName);
    when(backendCfg.getDBDirectoryPermissions()).thenReturn("755");
    when(backendCfg.getDBCacheSize()).thenReturn(0L);
    when(backendCfg.getDBCachePercent()).thenReturn(20);
    when(backendCfg.getBaseDN()).thenReturn(newTreeSet(DN.valueOf("dc=test,dc=com")));
    when(backendCfg.dn()).thenReturn(DN.valueOf("dc=test,dc=com"));
    when(backendCfg.listBackendIndexes()).thenReturn(new String[] { "sn" });
    when(backendCfg.listBackendVLVIndexes()).thenReturn(new String[0]);

    BackendIndexCfg indexCfg = legacyMockCfg(BackendIndexCfg.class);
    when(indexCfg.getIndexType()).thenReturn(newTreeSet(IndexType.PRESENCE, IndexType.EQUALITY));
    when(indexCfg.getAttribute()).thenReturn(DirectoryServer.getAttributeTypeOrNull("sn"));
    when(backendCfg.getBackendIndex("sn")).thenReturn(indexCfg);

    return backendCfg;
  }
}
