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

import java.util.List;

import javax.naming.OperationNotSupportedException;
import javax.naming.ldap.LdapName;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.ldap.ResultCode;
import org.opends.server.TestCaseUtils;
import org.opends.server.admin.AdminTestCase;
import org.opends.server.admin.TestCfg;
import org.opends.server.admin.TestChildCfg;
import org.opends.server.admin.TestChildCfgDefn;
import org.opends.server.admin.TestParentCfg;
import org.opends.server.admin.client.ldap.JNDIDirContextAdaptor;
import org.opends.server.admin.std.server.RootCfg;
import org.opends.server.core.AddOperation;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.opends.server.types.Entry;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

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

/**
 * Test cases for constraints on the server-side.
 */
public final class ConstraintTest extends AdminTestCase {

  /** Child DN. */
  private static final String TEST_CHILD_1_DN = "cn=test child 1,cn=test children,cn=test parent 1,cn=test parents,cn=config";



  /**
   * A test child add listener.
   */
  private static class AddListener implements
      ConfigurationAddListener<TestChildCfg> {

    /** {@inheritDoc} */
    @Override
    public ConfigChangeResult applyConfigurationAdd(TestChildCfg configuration) {
      return new ConfigChangeResult();
    }



    /** {@inheritDoc} */
    @Override
    public boolean isConfigurationAddAcceptable(TestChildCfg configuration,
        List<LocalizableMessage> unacceptableReasons) {
      return true;
    }

  }



  /**
   * A test child delete listener.
   */
  private static class DeleteListener implements
      ConfigurationDeleteListener<TestChildCfg> {

    /** {@inheritDoc} */
    @Override
    public ConfigChangeResult applyConfigurationDelete(
        TestChildCfg configuration) {
      return new ConfigChangeResult();
    }



    /** {@inheritDoc} */
    @Override
    public boolean isConfigurationDeleteAcceptable(TestChildCfg configuration,
        List<LocalizableMessage> unacceptableReasons) {
      return true;
    }

  }



  /**
   * A test child change listener.
   */
  private static class ChangeListener implements
      ConfigurationChangeListener<TestChildCfg> {

    /** {@inheritDoc} */
    @Override
    public ConfigChangeResult applyConfigurationChange(
        TestChildCfg configuration) {
      return new ConfigChangeResult();
    }



    /** {@inheritDoc} */
    @Override
    public boolean isConfigurationChangeAcceptable(TestChildCfg configuration,
        List<LocalizableMessage> unacceptableReasons) {
      return true;
    }

  }

  /** Test child 1 LDIF. */
  private static final String[] TEST_CHILD_1 = new String[] {
      "dn: cn=test child 1,cn=test children,cn=test parent 1,cn=test parents,cn=config",
      "objectclass: top",
      "objectclass: ds-cfg-test-child-dummy",
      "cn: test child 1",
      "ds-cfg-enabled: true",
      "ds-cfg-java-class: org.opends.server.extensions.UserDefinedVirtualAttributeProvider",
      "ds-cfg-attribute-type: description",
      "ds-cfg-conflict-behavior: virtual-overrides-real"
  };

  /** Test LDIF. */
  private static final String[] TEST_LDIF = new String[] {
      // Base entries.
      "dn: cn=test parents,cn=config",
      "objectclass: top",
      "objectclass: ds-cfg-branch",
      "cn: test parents",
      "",
      // Parent 1 - uses default values for
      // optional-multi-valued-dn-property.
      "dn: cn=test parent 1,cn=test parents,cn=config",
      "objectclass: top",
      "objectclass: ds-cfg-test-parent-dummy",
      "cn: test parent 1",
      "ds-cfg-enabled: true",
      "ds-cfg-java-class: org.opends.server.extensions.UserDefinedVirtualAttributeProvider",
      "ds-cfg-attribute-type: description",
      "ds-cfg-conflict-behavior: virtual-overrides-real",
      "",
      // Child base entries.
      "dn:cn=test children,cn=test parent 1,cn=test parents,cn=config",
      "objectclass: top",
      "objectclass: ds-cfg-branch",
      "cn: test children",
      "",
  };

  /** JNDI LDAP context. */
  private JNDIDirContextAdaptor adaptor;



  /**
   * Sets up tests
   *
   * @throws Exception
   *           If the server could not be initialized.
   */
  @BeforeClass
  public void setUp() throws Exception {
    // This test suite depends on having the schema available, so
    // we'll start the server.
    TestCaseUtils.startServer();
    TestCfg.setUp();

    // Add test managed objects.
    TestCaseUtils.addEntries(TEST_LDIF);
  }



  /**
   * Tears down test environment.
   *
   * @throws Exception
   *           If the test entries could not be removed.
   */
  @AfterClass
  public void tearDown() throws Exception {
    TestCfg.cleanup();

    // Remove test entries.
    deleteSubtree("cn=test parents,cn=config");
  }



  /**
   * Tests that retrieval can succeed.
   *
   * @throws Exception
   *           If the test unexpectedly fails.
   */
  @Test
  public void testGetManagedObjectSuccess() throws Exception {
    MockConstraint constraint = new MockConstraint(true, false);

    try {
      TestCaseUtils.addEntry(TEST_CHILD_1);
      TestCfg.addConstraint(constraint);

      TestParentCfg parent = getParent("test parent 1");
      parent.getTestChild("test child 1");
    } finally {
      TestCfg.removeConstraint(constraint);
      deleteSubtreeNoException(TEST_CHILD_1_DN);
    }
  }



  /**
   * Tests that retrieval can fail.
   *
   * @throws Exception
   *           If the test unexpectedly fails.
   */
  @Test
  public void testGetManagedObjectFail() throws Exception {
    MockConstraint constraint = new MockConstraint(false, true);

    try {
      TestCaseUtils.addEntry(TEST_CHILD_1);
      TestCfg.addConstraint(constraint);

      TestParentCfg parent = getParent("test parent 1");
      parent.getTestChild("test child 1");
    } catch (ConfigException e) {
      Throwable cause = e.getCause();
      if (cause instanceof ConstraintViolationException) {
        ConstraintViolationException cve = (ConstraintViolationException) cause;
        Assert.assertEquals(cve.getMessages().size(), 1);
        Assert.assertSame(cve.getManagedObject().getManagedObjectDefinition(),
            TestChildCfgDefn.getInstance());
      } else {
        // Wrong cause.
        throw e;
      }
    } finally {
      TestCfg.removeConstraint(constraint);
      deleteSubtreeNoException(TEST_CHILD_1_DN);
    }
  }



  /**
   * Tests that an add constraint can succeed.
   *
   * @throws Exception
   *           If the test unexpectedly fails.
   */
  @Test
  public void testAddConstraintSuccess() throws Exception {
    TestParentCfg parent = getParent("test parent 1");
    AddListener listener = new AddListener();
    parent.addTestChildAddListener(listener);

    MockConstraint constraint = new MockConstraint(true, false);
    TestCfg.addConstraint(constraint);

    try {
      try {
        // Add the entry.
        addEntry(ResultCode.SUCCESS, TEST_CHILD_1);
      } finally {
        deleteSubtreeNoException(TEST_CHILD_1_DN);
      }
    } finally {
      TestCfg.removeConstraint(constraint);
      parent.removeTestChildAddListener(listener);
    }
  }



  /**
   * Tests that an add constraint can fail.
   *
   * @throws Exception
   *           If the test unexpectedly fails.
   */
  @Test
  public void testAddConstraintFail() throws Exception {
    TestParentCfg parent = getParent("test parent 1");
    AddListener listener = new AddListener();
    parent.addTestChildAddListener(listener);

    MockConstraint constraint = new MockConstraint(false, true);
    TestCfg.addConstraint(constraint);

    try {
      try {
        // Add the entry.
        addEntry(ResultCode.UNWILLING_TO_PERFORM, TEST_CHILD_1);
      } finally {
        deleteSubtreeNoException(TEST_CHILD_1_DN);
      }
    } finally {
      TestCfg.removeConstraint(constraint);
      parent.removeTestChildAddListener(listener);
    }
  }



  /**
   * Tests that a delete constraint can succeed.
   *
   * @throws Exception
   *           If the test unexpectedly fails.
   */
  @Test
  public void testDeleteConstraintSuccess() throws Exception {
    TestParentCfg parent = getParent("test parent 1");
    DeleteListener listener = new DeleteListener();
    parent.addTestChildDeleteListener(listener);

    MockConstraint constraint = new MockConstraint(false, true);
    TestCfg.addConstraint(constraint);

    try {
      // Add the entry.
      TestCaseUtils.addEntry(TEST_CHILD_1);

      // Now delete it - this should trigger the constraint.
      deleteSubtree(TEST_CHILD_1_DN);
    } finally {
      TestCfg.removeConstraint(constraint);
      parent.removeTestChildDeleteListener(listener);
      deleteSubtreeNoException(TEST_CHILD_1_DN);
    }
  }



  /**
   * Tests that a delete constraint can fail.
   *
   * @throws Exception
   *           If the test unexpectedly fails.
   */
  @Test
  public void testDeleteConstraintFail() throws Exception {
    TestParentCfg parent = getParent("test parent 1");
    DeleteListener listener = new DeleteListener();
    parent.addTestChildDeleteListener(listener);

    MockConstraint constraint = new MockConstraint(true, false);
    TestCfg.addConstraint(constraint);

    try {
      // Add the entry.
      TestCaseUtils.addEntry(TEST_CHILD_1);
      try {
        // Now delete it - this should trigger the constraint.
        deleteSubtree(TEST_CHILD_1_DN);

        // Should not have succeeded.
        Assert.fail("Delete constraint failed to prevent deletion");
      } catch (OperationNotSupportedException e) {
        // Ignore - this is the expected exception.
      }
    } finally {
      TestCfg.removeConstraint(constraint);
      parent.removeTestChildDeleteListener(listener);
      deleteSubtreeNoException(TEST_CHILD_1_DN);
    }
  }



  /**
   * Tests that a modify constraint can succeed.
   *
   * @throws Exception
   *           If the test unexpectedly fails.
   */
  @Test
  public void testChangeConstraintSuccess() throws Exception {
    TestParentCfg parent = getParent("test parent 1");

    MockConstraint constraint = new MockConstraint(true, false);

    try {
      // Add the entry.
      TestCaseUtils.addEntry(TEST_CHILD_1);
      TestChildCfg child = parent.getTestChild("test child 1");

      TestCfg.addConstraint(constraint);
      ChangeListener listener = new ChangeListener();
      child.addChangeListener(listener);

      // Now modify it.
      String[] changes = new String[] {
          "dn: cn=test child 1,cn=test children,cn=test parent 1,cn=test parents,cn=config",
          "changetype: modify",
          "replace: ds-cfg-base-dn",
          "ds-cfg-base-dn: dc=new value 1,dc=com",
          "ds-cfg-base-dn: dc=new value 2,dc=com",
          "-",
          "replace: ds-cfg-group-dn",
          "ds-cfg-group-dn: dc=new value 3,dc=com",
          "ds-cfg-group-dn: dc=new value 4,dc=com"
      };

      int result = TestCaseUtils.applyModifications(true, changes);
      Assert.assertEquals(result, ResultCode.SUCCESS.intValue());
    } finally {
      TestCfg.removeConstraint(constraint);
      deleteSubtreeNoException(TEST_CHILD_1_DN);
    }
  }



  /**
   * Tests that a modify constraint can fail.
   *
   * @throws Exception
   *           If the test unexpectedly fails.
   */
  @Test
  public void testChangeConstraintFail() throws Exception {
    TestParentCfg parent = getParent("test parent 1");
    MockConstraint constraint = new MockConstraint(false, true);

    try {
      // Add the entry.
      TestCaseUtils.addEntry(TEST_CHILD_1);
      TestChildCfg child = parent.getTestChild("test child 1");

      TestCfg.addConstraint(constraint);
      ChangeListener listener = new ChangeListener();
      child.addChangeListener(listener);

      // Now modify it.
      String[] changes = new String[] {
          "dn: cn=test child 1,cn=test children,cn=test parent 1,cn=test parents,cn=config",
          "changetype: modify",
          "replace: ds-cfg-base-dn",
          "ds-cfg-base-dn: dc=new value 1,dc=com",
          "ds-cfg-base-dn: dc=new value 2,dc=com",
          "-",
          "replace: ds-cfg-group-dn",
          "ds-cfg-group-dn: dc=new value 3,dc=com",
          "ds-cfg-group-dn: dc=new value 4,dc=com"
      };

      int result = TestCaseUtils.applyModifications(true, changes);
      Assert.assertEquals(result, ResultCode.UNWILLING_TO_PERFORM.intValue());
    } finally {
      TestCfg.removeConstraint(constraint);
      deleteSubtreeNoException(TEST_CHILD_1_DN);
    }
  }

  /** Add an entry and check its result. */
  private void addEntry(ResultCode expected, String... lines) throws Exception {
    Entry entry = TestCaseUtils.makeEntry(lines);
    AddOperation add = getRootConnection().processAdd(entry);
    assertEquals(add.getResultCode(), expected, add.getErrorMessage().toString());
  }

  private void deleteSubtreeNoException(String dn)
  {
    try
    {
      deleteSubtree(dn);
    }
    catch (Exception e)
    {
      // Ignore.
    }
  }

  /** Deletes the named sub-tree. */
  private void deleteSubtree(String dn) throws Exception {
    getAdaptor().deleteSubtree(new LdapName(dn));
  }

  /** Gets the JNDI connection for the test server instance. */
  private synchronized JNDIDirContextAdaptor getAdaptor() throws Exception {
    if (adaptor == null) {
      adaptor = JNDIDirContextAdaptor.simpleSSLBind("127.0.0.1", TestCaseUtils
          .getServerAdminPort(), "cn=directory manager", "password");
    }
    return adaptor;
  }

  /** Gets the named parent configuration. */
  private TestParentCfg getParent(String name) throws Exception {
    ServerManagementContext ctx = ServerManagementContext.getInstance();
    ServerManagedObject<RootCfg> root = ctx.getRootConfigurationManagedObject();
    return root.getChild(
        TestCfg.getTestOneToManyParentRelationDefinition(), name)
        .getConfiguration();
  }
}
