/** @category Validation Rules */

import type { ObjMap } from '../../jsutils/ObjMap.ts';

import { GraphQLError } from '../../error/GraphQLError.ts';

import type { FieldNode, OperationDefinitionNode } from '../../language/ast.ts';
import { Kind } from '../../language/kinds.ts';
import type { ASTVisitor } from '../../language/visitor.ts';

import type {
  FieldDetailsList,
  FragmentDetails,
} from '../../execution/collectFields.ts';
import { collectFields } from '../../execution/collectFields.ts';
import type { VariableValues } from '../../execution/values.ts';

import type { ValidationContext } from '../ValidationContext.ts';

function toNodes(fieldDetailsList: FieldDetailsList): ReadonlyArray<FieldNode> {
  return fieldDetailsList.map((fieldDetails) => fieldDetails.node);
}

/**
 * Subscriptions must only include a non-introspection field.
 *
 * A GraphQL subscription is valid only if it contains a single root field and
 * that root field is not an introspection field.
 *
 * See https://spec.graphql.org/draft/#sec-Single-root-field
 * @param context - The validation context used while checking the document.
 * @returns A visitor that reports validation errors for this rule.
 * @example
 * ```ts
 * import { buildSchema, parse, validate } from 'graphql';
 * import { SingleFieldSubscriptionsRule } from 'graphql/validation';
 *
 * const schema = buildSchema(`
 *   type Query {
 *     name: String
 *   }
 *
 *   type Subscription {
 *     a: String
 *     b: String
 *   }
 * `);
 *
 * const invalidDocument = parse(`
 *   subscription { a b }
 * `);
 * const invalidErrors = validate(schema, invalidDocument, [
 *   SingleFieldSubscriptionsRule,
 * ]);
 *
 * invalidErrors.length; // => 1
 *
 * const validDocument = parse(`
 *   subscription { a }
 * `);
 * const validErrors = validate(schema, validDocument, [
 *   SingleFieldSubscriptionsRule,
 * ]);
 *
 * validErrors; // => []
 * ```
 */
export function SingleFieldSubscriptionsRule(
  context: ValidationContext,
): ASTVisitor {
  return {
    OperationDefinition(node: OperationDefinitionNode) {
      if (node.operation === 'subscription') {
        const schema = context.getSchema();
        const subscriptionType = schema.getSubscriptionType();
        if (subscriptionType) {
          const operationName = node.name ? node.name.value : null;
          const variableValues: VariableValues = Object.create(null);
          const document = context.getDocument();
          const fragments: ObjMap<FragmentDetails> = Object.create(null);
          for (const definition of document.definitions) {
            if (definition.kind === Kind.FRAGMENT_DEFINITION) {
              fragments[definition.name.value] = { definition };
            }
          }
          const { groupedFieldSet, forbiddenDirectiveInstances } =
            collectFields(
              schema,
              fragments,
              variableValues,
              subscriptionType,
              node.selectionSet,
              context.hideSuggestions,
              true,
            );
          if (forbiddenDirectiveInstances.length > 0) {
            context.reportError(
              new GraphQLError(
                operationName != null
                  ? `Subscription "${operationName}" must not use \`@skip\` or \`@include\` directives in the top level selection.`
                  : 'Anonymous Subscription must not use `@skip` or `@include` directives in the top level selection.',
                { nodes: forbiddenDirectiveInstances },
              ),
            );
            return;
          }
          if (groupedFieldSet.size > 1) {
            const fieldDetailsLists = [...groupedFieldSet.values()];
            const extraFieldDetailsLists = fieldDetailsLists.slice(1);
            const extraFieldSelections = extraFieldDetailsLists.flatMap(
              (fieldDetailsList) => toNodes(fieldDetailsList),
            );
            context.reportError(
              new GraphQLError(
                operationName != null
                  ? `Subscription "${operationName}" must select only one top level field.`
                  : 'Anonymous Subscription must select only one top level field.',
                { nodes: extraFieldSelections },
              ),
            );
          }
          for (const fieldDetailsList of groupedFieldSet.values()) {
            const fieldName = toNodes(fieldDetailsList)[0].name.value;
            if (fieldName.startsWith('__')) {
              context.reportError(
                new GraphQLError(
                  operationName != null
                    ? `Subscription "${operationName}" must not select an introspection top level field.`
                    : 'Anonymous Subscription must not select an introspection top level field.',
                  { nodes: toNodes(fieldDetailsList) },
                ),
              );
            }
          }
        }
      }
    },
  };
}