/**
 * Copyright (c) 2015-present, Facebook, Inc.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

import { print } from '../language/printer';
import type {
  ValueNode,
  ListValueNode,
  ObjectValueNode
} from '../language/ast';
import * as Kind from '../language/kinds';
import {
  GraphQLScalarType,
  GraphQLEnumType,
  GraphQLInputObjectType,
  GraphQLList,
  GraphQLNonNull
} from '../type/definition';
import type { GraphQLInputType } from '../type/definition';
import invariant from '../jsutils/invariant';
import keyMap from '../jsutils/keyMap';


/**
 * Utility for validators which determines if a value literal node is valid
 * given an input type.
 *
 * Note that this only validates literal values, variables are assumed to
 * provide values of the correct type.
 */
export function isValidLiteralValue(
  type: GraphQLInputType,
  valueNode: ValueNode
): Array<string> {
  // A value must be provided if the type is non-null.
  if (type instanceof GraphQLNonNull) {
    if (!valueNode || (valueNode.kind === Kind.NULL)) {
      return [ `Expected "${String(type)}", found null.` ];
    }
    return isValidLiteralValue(type.ofType, valueNode);
  }

  if (!valueNode || (valueNode.kind === Kind.NULL)) {
    return [];
  }

  // This function only tests literals, and assumes variables will provide
  // values of the correct type.
  if (valueNode.kind === Kind.VARIABLE) {
    return [];
  }

  // Lists accept a non-list value as a list of one.
  if (type instanceof GraphQLList) {
    const itemType = type.ofType;
    if (valueNode.kind === Kind.LIST) {
      return (valueNode: ListValueNode).values.reduce((acc, item, index) => {
        const errors = isValidLiteralValue(itemType, item);
        return acc.concat(errors.map(error =>
          `In element #${index}: ${error}`
        ));
      }, []);
    }
    return isValidLiteralValue(itemType, valueNode);
  }

  // Input objects check each defined field and look for undefined fields.
  if (type instanceof GraphQLInputObjectType) {
    if (valueNode.kind !== Kind.OBJECT) {
      return [ `Expected "${type.name}", found not an object.` ];
    }
    const fields = type.getFields();

    const errors = [];

    // Ensure every provided field is defined.
    const fieldNodes = (valueNode: ObjectValueNode).fields;
    fieldNodes.forEach(providedFieldNode => {
      if (!fields[providedFieldNode.name.value]) {
        errors.push(
          `In field "${providedFieldNode.name.value}": Unknown field.`
        );
      }
    });

    // Ensure every defined field is valid.
    const fieldNodeMap = keyMap(fieldNodes, fieldNode => fieldNode.name.value);
    Object.keys(fields).forEach(fieldName => {
      const result = isValidLiteralValue(
        fields[fieldName].type,
        fieldNodeMap[fieldName] && fieldNodeMap[fieldName].value
      );
      errors.push(...(result.map(error =>
        `In field "${fieldName}": ${error}`
      )));
    });

    return errors;
  }

  invariant(
    type instanceof GraphQLScalarType || type instanceof GraphQLEnumType,
    'Must be input type'
  );

  // Scalars determine if a literal values is valid.
  if (!type.isValidLiteral(valueNode)) {
    return [ `Expected type "${type.name}", found ${print(valueNode)}.` ];
  }

  return [];
}
