/*
 * 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 2015 ForgeRock AS
 */
package org.forgerock.opendj.config.server;

import static org.fest.assertions.Assertions.assertThat;
import static org.forgerock.opendj.ldap.TestCaseUtils.failWasExpected;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.verify;

import java.io.IOException;
import java.util.List;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.LocalizableMessageBuilder;
import org.forgerock.opendj.config.AdminTestCase;
import org.forgerock.opendj.config.TestCfg;
import org.forgerock.opendj.config.TestChildCfg;
import org.forgerock.opendj.config.TestChildCfgDefn;
import org.forgerock.opendj.config.TestParentCfg;
import org.forgerock.opendj.config.server.spi.ConfigAddListener;
import org.forgerock.opendj.config.server.spi.ConfigChangeListener;
import org.forgerock.opendj.config.server.spi.ConfigDeleteListener;
import org.forgerock.opendj.config.server.spi.ConfigurationRepository;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldif.LDIF;
import org.mockito.ArgumentCaptor;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

/**
 * Test cases for constraints on the server-side.
 */
@SuppressWarnings("javadoc")
public final class ConstraintTest extends AdminTestCase {

    private static class AddListener implements ConfigurationAddListener<TestChildCfg> {

        public ConfigChangeResult applyConfigurationAdd(TestChildCfg configuration) {
            return new ConfigChangeResult();
        }

        public boolean isConfigurationAddAcceptable(TestChildCfg configuration,
            List<LocalizableMessage> unacceptableReasons) {
            return true;
        }
    }

    private static class DeleteListener implements ConfigurationDeleteListener<TestChildCfg> {

        public ConfigChangeResult applyConfigurationDelete(TestChildCfg configuration) {
            return new ConfigChangeResult();
        }

        public boolean isConfigurationDeleteAcceptable(TestChildCfg configuration,
            List<LocalizableMessage> unacceptableReasons) {
            return true;
        }

    }

    private static class ChangeListener implements ConfigurationChangeListener<TestChildCfg> {

        public ConfigChangeResult applyConfigurationChange(TestChildCfg configuration) {
            return new ConfigChangeResult();
        }

        public boolean isConfigurationChangeAcceptable(TestChildCfg configuration,
            List<LocalizableMessage> unacceptableReasons) {
            return true;
        }

    }

    // @Checkstyle:off
    private static final Entry TEST_CHILD_1 = LDIF.makeEntry(
        "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");

    private static final Entry TEST_BASE_CHILD = LDIF.makeEntry(
        "dn:cn=test children,cn=test parent 1,cn=test parents,cn=config",
        "objectclass: top",
        "objectclass: ds-cfg-branch",
        "cn: test children");

    /**
     * Parent 1 - uses default values for
     * optional-multi-valued-dn-property.
     */
    private static final Entry TEST_PARENT_1 = LDIF.makeEntry(
        "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");
    // @Checkstyle:on

    @BeforeClass
    public void setUp() throws Exception {
        TestCfg.setUp();
    }

    @AfterClass
    public void tearDown() throws Exception {
        TestCfg.cleanup();
    }

    /** Success just ensure there is no exception raised. */
    @Test
    public void testGetManagedObjectSuccess() throws Exception {
        // arrange
        ConfigurationRepository configRepository =
            createConfigRepositoryWithEntries(TEST_PARENT_1, TEST_BASE_CHILD, TEST_CHILD_1);
        ServerManagementContext context =
            new ServerManagementContext(configRepository);
        MockConstraint constraint = new MockConstraint(true, false, configRepository);
        try {
            TestCfg.addConstraint(constraint);
            TestParentCfg parentCfg = getParentCfg(TEST_PARENT_1, context);

            // act
            parentCfg.getTestChild(entryName(TEST_CHILD_1));
        } finally {
            TestCfg.removeConstraint(constraint);
        }
    }

    @Test
    public void testGetManagedObjectFail() throws Exception {
        // arrange
        ConfigurationRepository configRepository =
            createConfigRepositoryWithEntries(TEST_PARENT_1, TEST_BASE_CHILD, TEST_CHILD_1);
        ServerManagementContext context =
            new ServerManagementContext(configRepository);
        MockConstraint constraint = new MockConstraint(false, true, configRepository);
        try {
            TestCfg.addConstraint(constraint);
            TestParentCfg parentCfg = getParentCfg(TEST_PARENT_1, context);

            // act
            parentCfg.getTestChild(entryName(TEST_CHILD_1));

            failWasExpected(ConfigException.class);
        } catch (ConfigException e) {
            // assert
            Throwable cause = e.getCause();
            assertThat(e.getCause()).isNotNull().isInstanceOf(ConstraintViolationException.class);
            ConstraintViolationException cve = (ConstraintViolationException) cause;
            assertThat(cve.getMessages().size()).isEqualTo(1);
            assertThat(cve.getManagedObject().getManagedObjectDefinition()).isSameAs(TestChildCfgDefn.getInstance());
        } finally {
            TestCfg.removeConstraint(constraint);
        }
    }

    @DataProvider
    Object[][] constraintValues() {
        return new Object[][] {
            // value of constraint used
            { true }, // success
            { false } // failure
        };
    }

    @Test(dataProvider = "constraintValues")
    public void testAddConstraint(boolean isUsableConstraint) throws Exception {
        // arrange
        ConfigurationRepository configRepository =
            createConfigRepositoryWithEntries(TEST_PARENT_1, TEST_BASE_CHILD, TEST_CHILD_1);
        ServerManagementContext context =
            new ServerManagementContext(configRepository);
        TestParentCfg parentCfg = getParentCfg(TEST_PARENT_1, context);
        parentCfg.addTestChildAddListener(new AddListener());
        MockConstraint constraint = new MockConstraint(isUsableConstraint, false, configRepository);
        try {
            TestCfg.addConstraint(constraint);

            // act
            boolean isAcceptable = simulateEntryAdd(TEST_CHILD_1, configRepository);

            // assert : success depends on constraint used
            assertThat(isAcceptable).isEqualTo(isUsableConstraint);
        } finally {
            TestCfg.removeConstraint(constraint);
        }
    }

    @Test(dataProvider = "constraintValues")
    public void testDeleteConstraint(boolean isDeleteAllowedConstraint) throws Exception {
        // arrange
        ConfigurationRepository configRepository =
            createConfigRepositoryWithEntries(TEST_PARENT_1, TEST_BASE_CHILD, TEST_CHILD_1);
        ServerManagementContext context =
            new ServerManagementContext(configRepository);
        TestParentCfg parentCfg = getParentCfg(TEST_PARENT_1, context);
        parentCfg.addTestChildDeleteListener(new DeleteListener());
        MockConstraint constraint = new MockConstraint(false, isDeleteAllowedConstraint, configRepository);
        try {
            TestCfg.addConstraint(constraint);

            // act
            boolean isAcceptable = simulateEntryDelete(TEST_CHILD_1, configRepository);

            // assert : success depends on constraint used
            assertThat(isAcceptable).isEqualTo(isDeleteAllowedConstraint);
        } finally {
            TestCfg.removeConstraint(constraint);
        }
    }

    @Test(dataProvider = "constraintValues")
    public void testChangeConstraint(boolean isUsableConstraint) throws Exception {
        // arrange
        ConfigurationRepository configRepository =
            createConfigRepositoryWithEntries(TEST_PARENT_1, TEST_BASE_CHILD, TEST_CHILD_1);
        ServerManagementContext context =
            new ServerManagementContext(configRepository);
        MockConstraint constraint = new MockConstraint(isUsableConstraint, false, configRepository);
        TestParentCfg parentCfg = getParentCfg(TEST_PARENT_1, context);
        TestChildCfg childCfg = parentCfg.getTestChild(entryName(TEST_CHILD_1));

        try {
            TestCfg.addConstraint(constraint);
            childCfg.addChangeListener(new ChangeListener());

            // act
            // It is not an issue to use the same child entry here
            // as we're only interested in constraint checking
            boolean isAcceptable = simulateEntryChange(TEST_CHILD_1, configRepository);

            // assert : success depends on constraint used
            assertThat(isAcceptable).isEqualTo(isUsableConstraint);
        } finally {
            TestCfg.removeConstraint(constraint);
        }
    }

    /**
     * Simulate an entry add by triggering configAddIsAcceptable method of last
     * registered add listener.
     *
     * @return true if add is acceptable, false otherwise.
     */
    private boolean simulateEntryAdd(Entry entry, ConfigurationRepository configRepository) throws IOException {
        // use argument capture to retrieve the actual listener
        ArgumentCaptor<ConfigAddListener> registeredListener = ArgumentCaptor.forClass(ConfigAddListener.class);
        verify(configRepository).registerAddListener(eq(entry.getName().parent()), registeredListener.capture());

        return registeredListener.getValue().configAddIsAcceptable(entry, new LocalizableMessageBuilder());
    }

    /**
     * Simulate an entry delete by triggering configDeleteIsAcceptable method of
     * last registered add listener.
     *
     * @return true if delete is acceptable, false otherwise.
     */
    private boolean simulateEntryDelete(Entry entry, ConfigurationRepository configRepository) throws IOException {
        // use argument capture to retrieve the actual listener
        ArgumentCaptor<ConfigDeleteListener> registeredListener = ArgumentCaptor.forClass(ConfigDeleteListener.class);
        verify(configRepository).registerDeleteListener(eq(entry.getName().parent()), registeredListener.capture());

        return registeredListener.getValue().configDeleteIsAcceptable(entry, new LocalizableMessageBuilder());
    }

    /**
     * Simulate an entry change by triggering configChangeIsAcceptable method on
     * last registered change listener.
     *
     * @return true if change is acceptable, false otherwise.
     */
    private boolean simulateEntryChange(Entry newEntry, ConfigurationRepository configRepository) {
        // use argument capture to retrieve the actual listener
        ArgumentCaptor<ConfigChangeListener> registeredListener = ArgumentCaptor.forClass(ConfigChangeListener.class);
        verify(configRepository).registerChangeListener(eq(newEntry.getName()), registeredListener.capture());

        return registeredListener.getValue().configChangeIsAcceptable(newEntry, new LocalizableMessageBuilder());
    }

}
