/*
 * 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 2011-2015 ForgeRock AS
 */
package org.opends.server.admin;

import static org.opends.server.util.ServerConstants.*;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.opends.server.DirectoryServerTestCase;
import org.opends.server.TestCaseUtils;
import org.opends.server.core.DirectoryServer;
import org.opends.server.types.AttributeType;
import org.opends.server.types.ObjectClass;
import org.opends.server.types.Schema;
import org.testng.Assert;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;


public class ValidateConfigDefinitionsTest extends DirectoryServerTestCase {

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

  /** Returns all AbstractManagedObjectDefinition objects that are defined in. */
  @DataProvider
  public Object[][] enumrateManageObjectDefns() throws Exception {
    TopCfgDefn topCfgDefn = TopCfgDefn.getInstance();
    List<AbstractManagedObjectDefinition<?,?>> allCfgDefns = new ArrayList<>(topCfgDefn.getAllChildren());

    Object[][] params = new Object[allCfgDefns.size()][];
    for (int i = 0; i < params.length; i++) {
      params[i] = new Object[]{allCfgDefns.get(i)};
    }

    return params;
  }

  /** Exceptions to config objects having a different objectclass. */
  private static final List<String> CLASS_OBJECT_CLASS_EXCEPTIONS =
          Arrays.asList(
                  "org.opends.server.admin.std.meta.RootCfgDefn",
                  "org.opends.server.admin.std.meta.GlobalCfgDefn"
          );


  @Test(dataProvider="enumrateManageObjectDefns")
  public void validateConfigObjectDefinitions(AbstractManagedObjectDefinition<?, ?> objectDef) {
    String objName = objectDef.getName();
    StringBuilder errors = new StringBuilder();
    Collection<PropertyDefinition<?>> allDefinitions =
            objectDef.getAllPropertyDefinitions();

    LDAPProfile ldapProfile = LDAPProfile.getInstance();
    String ldapObjectclassName = ldapProfile.getObjectClass(objectDef);
    if (ldapObjectclassName == null) {
      errors.append("There is no objectclass definition for configuration object " + objName);
    } else {
      String expectedObjectClass = "ds-cfg-" + objName;
      if (!ldapObjectclassName.equals(expectedObjectClass) &&
          !CLASS_OBJECT_CLASS_EXCEPTIONS.contains(objectDef.getClass().getName())) {
        errors.append("For config object " + objName +
                ", the LDAP objectclass must be " + expectedObjectClass +
                " instead of " + ldapObjectclassName).append(EOL + EOL);
      }
    }
    ObjectClass configObjectClass = DirectoryServer.getSchema().getObjectClass(ldapObjectclassName.toLowerCase());;

    for (PropertyDefinition<?> propDef: allDefinitions) {
      validatePropertyDefinition(objectDef, configObjectClass, propDef, errors);
    }

    Assert.assertEquals(errors.length(), 0,
        "The configuration definition for " + objectDef.getName() + " has the following problems: " + EOL + errors);
  }

  /** Exceptions to properties ending in -class being exactly 'java-class'. */
  private static final List<String> CLASS_PROPERTY_EXCEPTIONS =
          Arrays.asList(
                  // e.g. "prop-name-ending-with-class"
          );

  /** Exceptions to properties ending in -enabled being exactly 'enabled'. */
  private static final List<String> ENABLED_PROPERTY_EXCEPTIONS =
          Arrays.asList(
                  "index-filter-analyzer-enabled",
                  "subordinate-indexes-enabled"
                  // e.g. "prop-name-ending-with-enabled"
          );

  /** Exceptions to properties not starting with the name of their config object. */
  private static final List<String> OBJECT_PREFIX_PROPERTY_EXCEPTIONS =
          Arrays.asList(
                  "backend-id",
                  "plugin-type",
                  "replication-server-id",
                  "network-group-id",
                  "workflow-id",
                  "workflow-element-id",
                  "workflow-element"
                  // e.g. "prop-name-starting-with-object-prefix"
          );


  private void validatePropertyDefinition(AbstractManagedObjectDefinition<?, ?> objectDef,
                                          ObjectClass configObjectClass,
                                          PropertyDefinition<?> propDef,
                                          StringBuilder errors) {
    String objName = objectDef.getName();
    String propName = propDef.getName();

    // We want class properties to be exactly java-class
    if (propName.endsWith("-class") &&
        !propName.equals("java-class") &&
        !CLASS_PROPERTY_EXCEPTIONS.contains(propName))
    {
      errors.append("The " + propName + " property on config object " + objName +
              " should probably be java-class.  If not, then add " +
              propName + " to the CLASS_PROPERTY_EXCEPTIONS array in " +
              ValidateConfigDefinitionsTest.class.getName() + " to suppress" +
              " this warning.");
    }

    // We want enabled properties to be exactly enabled
    if (propName.endsWith("-enabled") && !ENABLED_PROPERTY_EXCEPTIONS.contains(propName))
    {
      errors.append("The " + propName + " property on config object " + objName +
              " should probably be just 'enabled'.  If not, then add " +
              propName + " to the ENABLED_PROPERTY_EXCEPTIONS array in " +
              ValidateConfigDefinitionsTest.class.getName() + " to suppress" +
              " this warning.");
    }

    // It's redundant for properties to be prefixed with the name of their objecty
    if (propName.startsWith(objName) && !propName.equals(objName) &&
            !OBJECT_PREFIX_PROPERTY_EXCEPTIONS.contains(propName))
    {
      errors.append("The " + propName + " property on config object " + objName +
              " should not be prefixed with the name of the config object because" +
              " this is redundant.  If you disagree, then add " +
              propName + " to the OBJECT_PREFIX_PROPERTY_EXCEPTIONS array in " +
              ValidateConfigDefinitionsTest.class.getName() + " to suppress" +
              " this warning.");
    }


    LDAPProfile ldapProfile = LDAPProfile.getInstance();
    String ldapAttrName = ldapProfile.getAttributeName(objectDef, propDef);

    // LDAP attribute name is consistent with the property name
    String expectedLdapAttr = "ds-cfg-" + propName;
    if (!ldapAttrName.equals(expectedLdapAttr)) {
      errors.append("For the " + propName + " property on config object " + objName +
              ", the LDAP attribute must be " + expectedLdapAttr + " instead of " + ldapAttrName).append(EOL + EOL);
    }


    Schema schema = DirectoryServer.getSchema();
    AttributeType attrType = schema.getAttributeType(ldapAttrName.toLowerCase());

    // LDAP attribute exists
    if (attrType == null) {
      errors.append(propName + " property on config object " + objName + " is declared" +
               " to use ldap attribute " + ldapAttrName + ", but this attribute is not in the schema ").append(EOL + EOL);
    } else {

      // LDAP attribute is multivalued if the property is multivalued
      if (propDef.hasOption(PropertyOption.MULTI_VALUED) && attrType.isSingleValue()) {
        errors.append(propName + " property on config object " + objName + " is declared" +
                 " as multi-valued, but the corresponding ldap attribute " + ldapAttrName +
                 " is declared as single-valued.").append(EOL + EOL);
      }

      if (configObjectClass != null) {
        // If it's mandatory in the schema, it must be mandatory on the config property
        Set<AttributeType> mandatoryAttributes = configObjectClass.getRequiredAttributeChain();
        if (mandatoryAttributes.contains(attrType) && !propDef.hasOption(PropertyOption.MANDATORY)) {
          errors.append(propName + " property on config object " + objName + " is not declared" +
                   " as mandatory even though the corresponding ldap attribute " + ldapAttrName +
                   " is declared as mandatory in the schema.").append(EOL + EOL);
        }

        Set<AttributeType> allowedAttributes = new HashSet<>(mandatoryAttributes);
        allowedAttributes.addAll(configObjectClass.getOptionalAttributeChain());
        if (!allowedAttributes.contains(attrType)) {
          errors.append(propName + " property on config object " + objName + " has" +
                   " the corresponding ldap attribute " + ldapAttrName +
                   ", but this attribute is not an allowed attribute on the configuration " +
                   " object's objectclass " + configObjectClass.getNameOrOID()).append(EOL + EOL);
        }
      }
    }
  }
}
