/**
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.aurora.scheduler.storage.testing;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Map;
import java.util.Set;

import com.google.common.base.Defaults;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableSet;
import com.google.gson.internal.Primitives;

import org.apache.thrift.TUnion;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;

/**
 * Utility for validating objects used in storage testing.
 */
public final class StorageEntityUtil {

  private StorageEntityUtil() {
    // Utility class.
  }

  private static void assertFullyPopulated(String name, Object object, Set<Field> ignoredFields) {
    if (object instanceof Collection) {
      Object[] values = ((Collection<?>) object).toArray();
      assertFalse("Collection is empty: " + name, values.length == 0);
      for (int i = 0; i < values.length; i++) {
        assertFullyPopulated(name + "[" + i + "]", values[i], ignoredFields);
      }
    } else if (object instanceof Map) {
      Map<?, ?> map = (Map<?, ?>) object;
      assertFalse("Map is empty: " + name, map.isEmpty());
      for (Map.Entry<?, ?> entry : map.entrySet()) {
        assertFullyPopulated(name + " key", entry.getKey(), ignoredFields);
        assertFullyPopulated(name + "[" + entry.getKey() + "]", entry.getValue(), ignoredFields);
      }
    } else if (object instanceof TUnion) {
      TUnion<?, ?> union = (TUnion<?, ?>) object;
      assertFullyPopulated(
          name + "." + union.getSetField().getFieldName(),
          union.getFieldValue(),
          ignoredFields);
    } else if (!(object instanceof String) && !(object instanceof Enum)) {
      for (Field field : object.getClass().getDeclaredFields()) {
        if (!Modifier.isStatic(field.getModifiers())) {
          validateField(name, object, field, ignoredFields);
        }
      }
    }
  }

  private static void validateField(
      String name,
      Object object,
      Field field,
      Set<Field> ignoredFields) {

    try {
      field.setAccessible(true);
      String fullName = name + "." + field.getName();
      Object fieldValue = field.get(object);
      boolean mustBeSet = !ignoredFields.contains(field);
      if (mustBeSet) {
        assertNotNull(fullName + " is null", fieldValue);
      }
      if (fieldValue != null) {
        if (Primitives.isWrapperType(fieldValue.getClass())) {
          // Special-case the mutable hash code field.
          if (mustBeSet && !fullName.endsWith("cachedHashCode")) {
            assertNotEquals(
                "Primitive value must not be default: " + fullName,
                Defaults.defaultValue(Primitives.unwrap(fieldValue.getClass())),
                fieldValue);
          }
        } else {
          assertFullyPopulated(fullName, fieldValue, ignoredFields);
        }
      }
    } catch (IllegalAccessException e) {
      throw Throwables.propagate(e);
    }
  }

  /**
   * Ensures that an object tree is fully-populated.  This is useful when testing store
   * implementations to validate that all fields are mapped during a round-trip into and out of
   * a store implementation.
   *
   * @param object Object to ensure is fully populated.
   * @param <T> Object type.
   * @return The original {@code object}.
   */
  public static <T> T assertFullyPopulated(T object, Field... ignoredFields) {
    assertFullyPopulated(
        object.getClass().getSimpleName(),
        object,
        ImmutableSet.copyOf(ignoredFields));
    return object;
  }

  /**
   * Convenience method to get a field by name from a class, to pass as an ignored field to
   * {@link #assertFullyPopulated(Object, Field...)}.
   *
   * @param clazz Class to get a field from.
   * @param field Field name.
   * @return Field with the given {@code name}.
   */
  public static Field getField(Class<?> clazz, String field) {
    for (Field f : clazz.getDeclaredFields()) {
      if (f.getName().equals(field)) {
        return f;
      }
    }
    throw new IllegalArgumentException("Field not found: " + field);
  }
}