/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

mod scope;

use std::collections::HashMap;
use std::sync::Arc;

use common::ArgumentName;
use common::Diagnostic;
use common::DiagnosticsResult;
use common::FeatureFlag;
use common::Location;
use common::NamedItem;
use common::SourceLocationKey;
use common::WithLocation;
use graphql_ir::associated_data_impl;
use graphql_ir::transform_list;
use graphql_ir::transform_list_multi;
use graphql_ir::Condition;
use graphql_ir::ConditionValue;
use graphql_ir::ConstantValue;
use graphql_ir::Directive;
use graphql_ir::FragmentDefinition;
use graphql_ir::FragmentDefinitionName;
use graphql_ir::FragmentDefinitionNameMap;
use graphql_ir::FragmentDefinitionNameSet;
use graphql_ir::FragmentSpread;
use graphql_ir::InlineFragment;
use graphql_ir::OperationDefinition;
use graphql_ir::OperationDefinitionName;
use graphql_ir::Program;
use graphql_ir::ProvidedVariableMetadata;
use graphql_ir::Selection;
use graphql_ir::Transformed;
use graphql_ir::TransformedMulti;
use graphql_ir::TransformedValue;
use graphql_ir::Transformer;
use graphql_ir::Value;
use graphql_ir::Variable;
use graphql_ir::VariableDefinition;
use graphql_ir::VariableName;
use graphql_syntax::OperationKind;
use intern::string_key::Intern;
use intern::string_key::StringKeyIndexMap;
use intern::string_key::StringKeyMap;
use itertools::Itertools;
use scope::format_local_variable;
use scope::Scope;
use thiserror::Error;

use super::get_applied_fragment_name;
use crate::match_::SplitOperationMetadata;
use crate::match_::DIRECTIVE_SPLIT_OPERATION;
use crate::no_inline::is_raw_response_type_enabled;
use crate::no_inline::NO_INLINE_DIRECTIVE_NAME;
use crate::no_inline::PARENT_DOCUMENTS_ARG;
use crate::util::get_normalization_operation_name;
use crate::RawResponseGenerationMode;

/// A transform that converts a set of documents containing fragments/fragment
/// spreads *with* arguments to one where all arguments have been inlined. This
/// is effectively static currying of functions. Nodes are changed as follows:
/// - Fragment spreads with arguments are replaced with references to an inlined
///   version of the referenced fragment.
/// - Fragments with argument definitions are cloned once per unique set of
///   arguments, with the name changed to original name + hash and all nested
///   variable references changed to the value of that variable given its
///   arguments.
/// - Field & directive argument variables are replaced with the value of those
///   variables in context.
/// - Definitions of provided variables are added to root operations.
/// - All nodes are cloned with updated children.
///
/// The transform also handles statically passing/failing Condition nodes:
/// - Literal Conditions with a passing value are elided and their selections
///   inlined in their parent.
/// - Literal Conditions with a failing value are removed.
/// - Nodes that would become empty as a result of the above are removed.
///
/// Note that unreferenced fragments are not added to the output.
pub fn apply_fragment_arguments(
    program: &Program,
    is_normalization: bool,
    no_inline_feature: &FeatureFlag,
    base_fragment_names: &FragmentDefinitionNameSet,
) -> DiagnosticsResult<Program> {
    let mut transform = ApplyFragmentArgumentsTransform {
        base_fragment_names,
        errors: Vec::new(),
        fragments: Default::default(),
        is_normalization,
        no_inline_feature,
        program,
        provided_variables: Default::default(),
        scope: Default::default(),
        split_operations: Default::default(),
    };

    let mut next_program = transform
        .transform_program(program)
        .replace_or_else(|| program.clone());

    for (fragment_name, used_fragment) in transform.fragments {
        match used_fragment {
            PendingFragment::Resolved {
                fragment_definition: Some(fragment),
                ..
            } => next_program.insert_fragment(fragment),
            PendingFragment::Resolved {
                fragment_definition: None,
                ..
            } => {
                // The fragment ended up empty, do not add to result Program.
            }
            PendingFragment::Pending => panic!("Unexpected case, {}", fragment_name),
        }
    }

    for (_, (operation, _)) in transform.split_operations {
        if let Some(operation) = operation {
            next_program.insert_operation(Arc::new(operation));
        }
    }

    if transform.errors.is_empty() {
        Ok(next_program)
    } else {
        Err(transform.errors)
    }
}

type ProvidedVariablesMap = StringKeyIndexMap<VariableDefinition>;

#[derive(Debug)]
enum PendingFragment {
    Pending,
    Resolved {
        fragment_definition: Option<Arc<FragmentDefinition>>,
        provided_variables: ProvidedVariablesMap,
    },
}

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct NoInlineFragmentSpreadMetadata {
    pub location: SourceLocationKey,
}

associated_data_impl!(NoInlineFragmentSpreadMetadata);

struct ApplyFragmentArgumentsTransform<'flags, 'program, 'base_fragments> {
    base_fragment_names: &'base_fragments FragmentDefinitionNameSet,
    errors: Vec<Diagnostic>,
    fragments: FragmentDefinitionNameMap<PendingFragment>,
    is_normalization: bool,
    no_inline_feature: &'flags FeatureFlag,
    program: &'program Program,
    // used to keep track of the provided variables used by the current
    //  operation / fragment / no-inline fragment and its transitively
    //  included fragments
    provided_variables: ProvidedVariablesMap,
    scope: Scope,
    split_operations: StringKeyMap<(Option<OperationDefinition>, ProvidedVariablesMap)>,
}

impl Transformer for ApplyFragmentArgumentsTransform<'_, '_, '_> {
    const NAME: &'static str = "ApplyFragmentArgumentsTransform";
    const VISIT_ARGUMENTS: bool = true;
    const VISIT_DIRECTIVES: bool = true;

    fn transform_operation(
        &mut self,
        operation: &OperationDefinition,
    ) -> Transformed<OperationDefinition> {
        self.scope = Scope::root_scope();
        self.provided_variables = Default::default();
        let transform_result = self.default_transform_operation(operation);
        if self.provided_variables.is_empty()
            || operation
                .directives
                .named(*DIRECTIVE_SPLIT_OPERATION)
                .is_some()
        {
            // this transform does not add the SplitOperation directive, so this
            //  should be equal to checking whether the result is a split operation
            self.provided_variables.clear();
            transform_result
        } else {
            let mut add_provided_variables = |new_operation: &mut OperationDefinition| {
                new_operation.variable_definitions.append(
                    &mut self
                        .provided_variables
                        .drain(..)
                        .map(|(_, definition)| definition)
                        .collect_vec(),
                );
            };
            match transform_result {
                Transformed::Keep => {
                    let mut new_operation = operation.clone();
                    add_provided_variables(&mut new_operation);
                    Transformed::Replace(new_operation)
                }
                Transformed::Replace(mut new_operation) => {
                    add_provided_variables(&mut new_operation);
                    Transformed::Replace(new_operation)
                }
                Transformed::Delete => Transformed::Delete,
            }
        }
    }

    fn transform_fragment(
        &mut self,
        fragment: &FragmentDefinition,
    ) -> Transformed<FragmentDefinition> {
        if self.is_normalization {
            let no_inline_directive = fragment.directives.named(*NO_INLINE_DIRECTIVE_NAME);
            if let Some(no_inline_directive) = no_inline_directive {
                self.transform_no_inline_fragment(fragment, no_inline_directive);
            }
        }
        // Non-inlined fragments are promoted to operations; other fragments are deleted
        // unless they are referenced
        Transformed::Delete
    }

    fn transform_fragment_spread(&mut self, spread: &FragmentSpread) -> Transformed<Selection> {
        let fragment = self
            .program
            .fragment(spread.fragment.item)
            .unwrap_or_else(|| {
                panic!(
                    "Tried to spread missing fragment: `{}`.",
                    spread.fragment.item
                );
            });

        // Validate that the fragment spread does not try to pass in provided variables
        for (original_definition_name, definition_location) in
            fragment.used_global_variables.iter().filter_map(|def| {
                Some((
                    ProvidedVariableMetadata::find(&def.directives)?.original_variable_name,
                    def.name.location,
                ))
            })
        {
            if let Some(invalid_argument) = spread
                .arguments
                .named(ArgumentName(original_definition_name.0))
            {
                self.errors.push(
                    Diagnostic::error(
                        ValidationMessage::ProvidedVariableIncompatibleWithArguments {
                            original_definition_name,
                        },
                        invalid_argument.name.location,
                    )
                    .annotate("Provided variable defined here", definition_location),
                );
            }
        }

        if self.is_normalization {
            if let Some(directive) = fragment.directives.named(*NO_INLINE_DIRECTIVE_NAME) {
                self.transform_no_inline_fragment(fragment, directive);
                let transformed_arguments = spread
                    .arguments
                    .iter()
                    .map(|arg| {
                        let mut arg = self.transform_argument(arg).unwrap_or_else(|| arg.clone());
                        arg.name.item.0 =
                            format_local_variable(fragment.name.item, arg.name.item.0);
                        arg
                    })
                    .collect();
                let mut directives = Vec::with_capacity(spread.directives.len() + 1);
                directives.extend(spread.directives.iter().cloned());

                directives.push(
                    NoInlineFragmentSpreadMetadata {
                        location: fragment.name.location.source_location(),
                    }
                    .into(),
                );

                let normalization_name =
                    get_normalization_operation_name(fragment.name.item.0).intern();
                let next_spread = Selection::FragmentSpread(Arc::new(FragmentSpread {
                    arguments: transformed_arguments,
                    directives,
                    fragment: WithLocation::new(
                        fragment.name.location,
                        FragmentDefinitionName(normalization_name),
                    ),
                }));
                // If the fragment type is abstract, we need to ensure that it's only evaluated at runtime if the
                // type of the object matches the fragment's type condition. Rather than reimplement type refinement
                // for fragment spreads, we wrap the fragment spread in an inlinefragment (which may be inlined away)
                // that ensures it will go through type-refinement at runtime.
                return if fragment.type_condition.is_abstract_type() {
                    Transformed::Replace(Selection::InlineFragment(Arc::new(InlineFragment {
                        directives: Default::default(),
                        selections: vec![next_spread],
                        type_condition: Some(fragment.type_condition),
                        spread_location: Location::generated(),
                    })))
                } else {
                    Transformed::Replace(next_spread)
                };
            }
        }

        if let Some(applied_fragment) = self.apply_fragment(spread, fragment) {
            let directives = self
                .transform_directives(&spread.directives)
                .replace_or_else(|| spread.directives.clone());
            Transformed::Replace(Selection::FragmentSpread(Arc::new(FragmentSpread {
                fragment: applied_fragment.name,
                arguments: Vec::new(),
                directives,
            })))
        } else {
            Transformed::Delete
        }
    }

    fn transform_selections(
        &mut self,
        selections: &[Selection],
    ) -> TransformedValue<Vec<Selection>> {
        transform_list_multi(selections, |selection| {
            self.transform_selection_multi(selection)
        })
    }

    fn transform_value(&mut self, value: &Value) -> TransformedValue<Value> {
        match value {
            Value::Variable(prev_variable) => {
                if let Some(scope_value) = self.scope.get(prev_variable.name.item) {
                    match scope_value {
                        Value::Variable(replacement_variable) => {
                            TransformedValue::Replace(Value::Variable(Variable {
                                // Update the name/location to the applied variable name
                                name: replacement_variable.name,
                                // But keep the type of the previous variable, which reflects the type
                                // expected at this location
                                type_: prev_variable.type_.clone(),
                            }))
                        }
                        _ => TransformedValue::Replace(scope_value.clone()),
                    }
                } else {
                    // Assume a global variable if the variable has no local
                    // bindings.
                    TransformedValue::Keep
                }
            }
            Value::Constant(_) => TransformedValue::Keep,
            Value::List(items) => {
                transform_list(items, |value| self.transform_value(value)).map(Value::List)
            }
            Value::Object(arguments) => self.transform_arguments(arguments).map(Value::Object),
        }
    }

    fn transform_condition_value(
        &mut self,
        condition_value: &ConditionValue,
    ) -> TransformedValue<ConditionValue> {
        match condition_value {
            ConditionValue::Variable(prev_variable) => {
                match self.scope.get(prev_variable.name.item) {
                    Some(Value::Variable(replacement_variable)) => {
                        TransformedValue::Replace(ConditionValue::Variable(Variable {
                            // Update the name/location to the applied variable name
                            name: replacement_variable.name,
                            // But keep the type of the previous variable, which reflects the type
                            // expected at this location
                            type_: prev_variable.type_.clone(),
                        }))
                    }
                    Some(Value::Constant(ConstantValue::Boolean(constant_value))) => {
                        TransformedValue::Replace(ConditionValue::Constant(*constant_value))
                    }
                    None => {
                        // Assume a global variable if the variable has no local
                        // bindings.
                        TransformedValue::Keep
                    }
                    Some(other_binding) => {
                        panic!("Invalid variable value for condition: {:?}", other_binding);
                    }
                }
            }
            ConditionValue::Constant(_) => TransformedValue::Keep,
        }
    }
}

impl ApplyFragmentArgumentsTransform<'_, '_, '_> {
    fn transform_no_inline_fragment(
        &mut self,
        fragment: &FragmentDefinition,
        directive: &Directive,
    ) {
        // If we have already computed, we can return early
        if let Some((_, provided_variables)) = self.split_operations.get(&fragment.name.item.0) {
            for (name, def) in provided_variables {
                self.provided_variables.insert(*name, def.clone());
            }
            return;
        }

        // We do not need to to write normalization files for base fragments
        let is_base = self.base_fragment_names.contains(&fragment.name.item);
        if !is_base && !self.no_inline_feature.is_enabled_for(fragment.name.item.0) {
            self.errors.push(Diagnostic::error(
                format!(
                    "Invalid usage of @no_inline on fragment '{}': this feature is gated and currently set to: {}",
                    fragment.name.item, self.no_inline_feature
                ),
                directive.name.location,
            ));
        }

        // save the context used by the enclosing operation / fragment
        let mut saved_provided_vars = std::mem::take(&mut self.provided_variables);
        let saved_scope = std::mem::replace(&mut self.scope, no_inline_fragment_scope(fragment));

        self.extract_provided_variables(fragment);
        let fragment = self
            .default_transform_fragment(fragment)
            .unwrap_or_else(|| fragment.clone());
        let FragmentDefinition {
            name,
            mut directives,
            mut variable_definitions,
            selections,
            type_condition,
            ..
        } = fragment;

        for variable in &mut variable_definitions {
            variable.name.item = VariableName(format_local_variable(
                fragment.name.item,
                variable.name.item.0,
            ));
        }
        let mut metadata = SplitOperationMetadata {
            derived_from: Some(fragment.name.item),
            location: fragment.name.location,
            parent_documents: Default::default(),
            raw_response_type_generation_mode: is_raw_response_type_enabled(directive)
                .then_some(RawResponseGenerationMode::AllFieldsOptional),
        };
        // - A fragment with user defined @no_inline always produces a $normalization file. The `parent_document` of
        // that file is the fragment itself as it gets deleted iff that fragment is deleted or no longer
        // has the @no_inline directive.
        // - A fragment with @no_inline generated by @module, `parent_documents` also include fragments that
        // spread the current fragment with @module
        metadata.parent_documents.insert(fragment.name.item.into());
        let parent_documents_arg = directive.arguments.named(*PARENT_DOCUMENTS_ARG);
        if let Some(Value::Constant(ConstantValue::List(parent_documents))) =
            parent_documents_arg.map(|arg| &arg.value.item)
        {
            for val in parent_documents {
                if let ConstantValue::String(name) = val {
                    metadata.parent_documents.insert(
                        graphql_ir::ExecutableDefinitionName::FragmentDefinitionName(
                            FragmentDefinitionName(*name),
                        ),
                    );
                } else {
                    panic!("Expected item in the parent_documents to be a StringKey.")
                }
            }
        }
        directives.push(metadata.into());
        let normalization_name = get_normalization_operation_name(name.item.0).intern();
        let operation = if is_base {
            None
        } else {
            Some(OperationDefinition {
                name: WithLocation::new(name.location, OperationDefinitionName(normalization_name)),
                type_: type_condition,
                variable_definitions,
                directives,
                selections,
                kind: OperationKind::Query,
            })
        };

        if self
            .program
            .operation(OperationDefinitionName(normalization_name))
            .is_some()
        {
            self.errors.push(Diagnostic::error(
                format!(
                    "Invalid usage of @no_inline on fragment '{}' - @no_inline is only allowed on allowlisted fragments loaded with @module",
                    fragment.name.item,
                ),
                directive.name.location,
            ));
        }
        self.split_operations.insert(
            fragment.name.item.0,
            (operation, self.provided_variables.clone()),
        );

        // add this fragment's provided variables to that of the enclosing operation / fragment
        saved_provided_vars.extend(self.provided_variables.drain(..).into_iter());
        self.provided_variables = saved_provided_vars;
        self.scope = saved_scope;
    }

    fn extract_provided_variables(&mut self, fragment: &FragmentDefinition) {
        let provided_arguments =
            fragment
                .used_global_variables
                .iter()
                .filter(|variable_definition| {
                    variable_definition
                        .directives
                        .named(ProvidedVariableMetadata::directive_name())
                        .is_some()
                });
        for definition in provided_arguments {
            self.provided_variables
                .entry(definition.name.item.0)
                .or_insert_with(|| definition.clone());
        }
    }

    fn apply_fragment(
        &mut self,
        spread: &FragmentSpread,
        fragment: &FragmentDefinition,
    ) -> Option<Arc<FragmentDefinition>> {
        let transformed_arguments = self
            .transform_arguments(&spread.arguments)
            .replace_or_else(|| spread.arguments.clone());

        let applied_fragment_name =
            get_applied_fragment_name(spread.fragment.item, &transformed_arguments);
        if let Some(applied_fragment) = self.fragments.get(&applied_fragment_name) {
            return match applied_fragment {
                PendingFragment::Resolved {
                    fragment_definition,
                    provided_variables,
                } => {
                    // add this fragment's provided variables to that of the enclosing
                    //  operation / fragment
                    for (name, def) in provided_variables.iter() {
                        self.provided_variables.insert(*name, def.clone());
                    }
                    fragment_definition.clone()
                }
                PendingFragment::Pending => {
                    let mut error = Diagnostic::error(
                        ValidationMessage::CircularFragmentReference {
                            fragment_name: spread.fragment.item,
                        },
                        spread.fragment.location,
                    );
                    for location in self.scope.locations() {
                        error = error.annotate("other member of the cycle", location);
                    }
                    self.errors.push(error);
                    None
                }
            };
        }

        self.fragments
            .insert(applied_fragment_name, PendingFragment::Pending);

        self.scope
            .push(spread.fragment.location, &transformed_arguments, fragment);
        // save the context used by the enclosing operation / fragment
        let mut saved_provided_vars = std::mem::take(&mut self.provided_variables);
        self.extract_provided_variables(fragment);

        let selections = self
            .transform_selections(&fragment.selections)
            .replace_or_else(|| fragment.selections.clone());

        let transformed_fragment = if selections.is_empty() {
            None
        } else {
            Some(Arc::new(FragmentDefinition {
                name: WithLocation::new(fragment.name.location, applied_fragment_name),
                variable_definitions: Vec::new(),
                type_condition: fragment.type_condition,
                // TODO update globals
                used_global_variables: Vec::new(),
                directives: fragment.directives.clone(),
                selections,
            }))
        };

        self.fragments.insert(
            applied_fragment_name,
            PendingFragment::Resolved {
                fragment_definition: transformed_fragment.clone(),
                provided_variables: self.provided_variables.clone(),
            },
        );

        // add this fragment's provided variables to that of the enclosing operation / fragment
        saved_provided_vars.extend(self.provided_variables.drain(..).into_iter());
        self.provided_variables = saved_provided_vars;
        self.scope.pop();

        transformed_fragment
    }

    fn transform_selection_multi(&mut self, selection: &Selection) -> TransformedMulti<Selection> {
        match selection {
            Selection::FragmentSpread(selection) => {
                self.transform_fragment_spread(selection).into()
            }
            Selection::InlineFragment(selection) => {
                self.transform_inline_fragment(selection).into()
            }
            Selection::LinkedField(selection) => self.transform_linked_field(selection).into(),
            Selection::ScalarField(selection) => self.transform_scalar_field(selection).into(),
            Selection::Condition(selection) => self.transform_condition_multi(selection),
        }
    }

    fn transform_condition_multi(&mut self, condition: &Condition) -> TransformedMulti<Selection> {
        let condition_value = self.transform_condition_value(&condition.value);

        // If we replace with a constant condition, remove the condition node.
        if let TransformedValue::Replace(ConditionValue::Constant(const_condition_value)) =
            condition_value
        {
            return if const_condition_value == condition.passing_value {
                let selections = self
                    .transform_selections(&condition.selections)
                    .replace_or_else(|| condition.selections.clone());
                TransformedMulti::ReplaceMultiple(selections)
            } else {
                TransformedMulti::Delete
            };
        }

        // If selections are empty, delete
        let selections = self.transform_selections(&condition.selections);
        if let TransformedValue::Replace(selections) = &selections {
            if selections.is_empty() {
                return TransformedMulti::Delete;
            }
        }

        if selections.should_keep() && condition_value.should_keep() {
            TransformedMulti::Keep
        } else {
            TransformedMulti::Replace(Selection::Condition(Arc::new(Condition {
                value: condition_value.replace_or_else(|| condition.value.clone()),
                selections: selections.replace_or_else(|| condition.selections.clone()),
                ..condition.clone()
            })))
        }
    }
}

fn no_inline_fragment_scope(fragment: &FragmentDefinition) -> Scope {
    let mut bindings = HashMap::<VariableName, Value>::with_capacity_and_hasher(
        fragment.variable_definitions.len(),
        Default::default(),
    );
    for variable_definition in &fragment.variable_definitions {
        let variable_name = variable_definition.name.item;
        let scoped_variable_name = format_local_variable(fragment.name.item, variable_name.0);
        bindings.insert(
            variable_name,
            Value::Variable(Variable {
                name: WithLocation::new(
                    variable_definition.name.location,
                    VariableName(scoped_variable_name),
                ),
                type_: variable_definition.type_.clone(),
            }),
        );
    }
    let mut scope = Scope::root_scope();
    scope.push_bindings(fragment.name.location, bindings);
    scope
}

#[derive(Debug, Error)]
enum ValidationMessage {
    #[error("Found a circular reference from fragment '{fragment_name}'.")]
    CircularFragmentReference {
        fragment_name: FragmentDefinitionName,
    },
    #[error(
        "Passing a value to '{original_definition_name}' (a provided variable) through @arguments is not supported."
    )]
    ProvidedVariableIncompatibleWithArguments {
        original_definition_name: VariableName,
    },
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::collections::HashMap;

use common::ArgumentName;
use common::Location;
use common::NamedItem;
use graphql_ir::Argument;
use graphql_ir::ConstantValue;
use graphql_ir::FragmentDefinition;
use graphql_ir::FragmentDefinitionName;
use graphql_ir::Value;
use graphql_ir::VariableName;
use intern::string_key::Intern;
use intern::string_key::StringKey;

#[derive(Default, Debug)]
pub struct Scope {
    bindings: Vec<(Location, HashMap<VariableName, Value>)>,
}

impl Scope {
    pub fn root_scope() -> Self {
        Self {
            bindings: Vec::new(),
        }
    }

    pub fn get(&self, name: VariableName) -> Option<&Value> {
        self.bindings
            .last()
            .and_then(|bindings| bindings.1.get(&name))
    }

    pub fn locations(&self) -> Vec<Location> {
        self.bindings
            .iter()
            .map(|(location, _)| *location)
            .collect()
    }

    pub fn push_bindings(&mut self, location: Location, bindings: HashMap<VariableName, Value>) {
        self.bindings.push((location, bindings));
    }

    pub fn push(
        &mut self,
        location: Location,
        arguments: &[Argument],
        fragment: &FragmentDefinition,
    ) {
        let mut bindings = HashMap::<VariableName, Value>::default();
        for variable_definition in &fragment.variable_definitions {
            let arg_name = variable_definition.name.item;
            let arg_value = match arguments.named(ArgumentName(arg_name.0)) {
                Some(arg_from_spread) => {
                    if arg_from_spread.value.item == Value::Constant(ConstantValue::Null()) {
                        if let Some(default_value) = &variable_definition.default_value {
                            Value::Constant(default_value.item.clone())
                        } else {
                            arg_from_spread.value.item.clone()
                        }
                    } else {
                        arg_from_spread.value.item.clone()
                    }
                }
                None => {
                    if let Some(default_value) = &variable_definition.default_value {
                        Value::Constant(default_value.item.clone())
                    } else {
                        Value::Constant(ConstantValue::Null())
                    }
                }
            };
            bindings.insert(arg_name, arg_value);
        }
        self.bindings.push((location, bindings));
    }

    pub fn pop(&mut self) {
        assert!(
            !self.bindings.is_empty(),
            "cannot pop from empty scope stack",
        );
        self.bindings.pop();
    }
}

pub fn format_local_variable(
    fragment_name: FragmentDefinitionName,
    arg_name: StringKey,
) -> StringKey {
    format!("{}${}", fragment_name, arg_name).intern()
}

#[cfg(test)]
mod tests {
    use common::ArgumentName;
    use common::Location;
    use common::SourceLocationKey;
    use common::Span;
    use common::WithLocation;
    use graphql_ir::Argument;
    use graphql_ir::ConstantValue;
    use graphql_ir::VariableDefinition;
    use graphql_ir::VariableName;
    use intern::string_key::Intern;
    use relay_test_schema::TEST_SCHEMA;
    use schema::Schema;
    use schema::Type;
    use schema::TypeReference;

    use super::*;

    fn default_location() -> Location {
        Location::new(SourceLocationKey::standalone("test-file"), Span::new(0, 0))
    }

    fn with_test_location<T>(item: T) -> WithLocation<T> {
        WithLocation::new(default_location(), item)
    }

    fn optional_int_type_reference() -> TypeReference<Type> {
        TypeReference::Named(TEST_SCHEMA.get_type("Int".intern()).unwrap())
    }

    fn empty_fragment_definition() -> FragmentDefinition {
        FragmentDefinition {
            name: with_test_location(FragmentDefinitionName("TestFragment".intern())),
            type_condition: TEST_SCHEMA.get_type("User".intern()).unwrap(),
            directives: vec![],
            selections: vec![],
            used_global_variables: vec![],
            variable_definitions: vec![],
        }
    }

    // Passing of constant value.
    //
    // @argDefs(size: Int)
    // @args(size: 42)
    // => $size = 42
    #[test]
    fn test_push_sets_literal_argument_values() {
        let mut scope = Scope::root_scope();

        let fragment_definition = FragmentDefinition {
            used_global_variables: vec![],
            variable_definitions: vec![VariableDefinition {
                name: with_test_location(VariableName("size".intern())),
                type_: optional_int_type_reference(),
                default_value: None,
                directives: vec![],
            }],
            ..empty_fragment_definition()
        };

        let spread_arguments = vec![Argument {
            name: with_test_location(ArgumentName("size".intern())),
            value: with_test_location(Value::Constant(ConstantValue::Int(42))),
        }];

        // push the fragment call on the scope stack
        scope.push(default_location(), &spread_arguments, &fragment_definition);
        assert_eq!(
            scope.get(VariableName("size".intern())),
            Some(&Value::Constant(ConstantValue::Int(42)))
        );

        // after pop, the value should no longer be set
        scope.pop();
        assert_eq!(scope.get(VariableName("size".intern())), None);
    }

    // Test of default value behavior.
    //
    // @argDefs(
    //   noDefaultAndNothingPassed: Int
    //   defaultAndValuePassed: Int = 42
    //   defaultAndNothingPassed: Int = 42
    //   defaultAndNullPassed: Int = 42
    // )
    // @args(
    //   defaultAndValuePassed: 3
    //   defaultAndNullPassed: null
    // )
    // => $noDefaultAndNothingPassed = null
    // => $defaultAndValuePassed = 3
    // => $defaultAndNothingPassed = 42
    // => $defaultAndNullPassed = 42
    #[test]
    fn test_default_value() {
        let mut scope = Scope::root_scope();

        let fragment_definition = FragmentDefinition {
            used_global_variables: vec![],
            variable_definitions: vec![
                VariableDefinition {
                    name: with_test_location(VariableName("noDefaultAndNothingPassed".intern())),
                    type_: optional_int_type_reference(),
                    default_value: None,
                    directives: vec![],
                },
                VariableDefinition {
                    name: with_test_location(VariableName("defaultAndValuePassed".intern())),
                    type_: optional_int_type_reference(),
                    default_value: Some(with_test_location(ConstantValue::Int(42))),
                    directives: vec![],
                },
                VariableDefinition {
                    name: with_test_location(VariableName("defaultAndNothingPassed".intern())),
                    type_: optional_int_type_reference(),
                    default_value: Some(with_test_location(ConstantValue::Int(42))),
                    directives: vec![],
                },
                VariableDefinition {
                    name: with_test_location(VariableName("defaultAndNullPassed".intern())),
                    type_: optional_int_type_reference(),
                    default_value: Some(with_test_location(ConstantValue::Int(42))),
                    directives: vec![],
                },
            ],
            ..empty_fragment_definition()
        };

        let spread_arguments = vec![
            Argument {
                name: with_test_location(ArgumentName("defaultAndValuePassed".intern())),
                value: with_test_location(Value::Constant(ConstantValue::Int(3))),
            },
            Argument {
                name: with_test_location(ArgumentName("defaultAndNullPassed".intern())),
                value: with_test_location(Value::Constant(ConstantValue::Null())),
            },
        ];

        scope.push(default_location(), &spread_arguments, &fragment_definition);

        assert_eq!(
            scope.get(VariableName("noDefaultAndNothingPassed".intern())),
            Some(&Value::Constant(ConstantValue::Null()))
        );
        assert_eq!(
            scope.get(VariableName("defaultAndValuePassed".intern())),
            Some(&Value::Constant(ConstantValue::Int(3)))
        );
        assert_eq!(
            scope.get(VariableName("defaultAndNothingPassed".intern())),
            Some(&Value::Constant(ConstantValue::Int(42)))
        );
        assert_eq!(
            scope.get(VariableName("defaultAndNullPassed".intern())),
            // NOTE: This behaves like nothing passed. This is against the
            //       general GraphQL spec for arguments, but unfortunately baked
            //       in legacy behavior.
            Some(&Value::Constant(ConstantValue::Int(42)))
        );
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::sync::Arc;

use common::DirectiveName;
use common::NamedItem;
use common::WithLocation;
use graphql_ir::Directive;
use graphql_ir::FragmentSpread;
use graphql_ir::Program;
use graphql_ir::Selection;
use graphql_ir::Transformed;
use graphql_ir::Transformer;
use intern::string_key::Intern;
use lazy_static::lazy_static;

use crate::UPDATABLE_DIRECTIVE;

lazy_static! {
    pub static ref UPDATABLE_DIRECTIVE_FOR_TYPEGEN: DirectiveName =
        DirectiveName("__updatable".intern());
}

pub fn annotate_updatable_fragment_spreads(program: &Program) -> Program {
    let mut transform = AnnotateUpdatableFragmentSpreads { program };

    transform
        .transform_program(program)
        .replace_or_else(|| program.clone())
}

struct AnnotateUpdatableFragmentSpreads<'s> {
    program: &'s Program,
}

impl<'s> Transformer for AnnotateUpdatableFragmentSpreads<'s> {
    const NAME: &'static str = "AnnotateUpdatableFragmentSpreads";
    const VISIT_ARGUMENTS: bool = false;
    const VISIT_DIRECTIVES: bool = false;

    fn transform_fragment_spread(
        &mut self,
        fragment_spread: &FragmentSpread,
    ) -> Transformed<Selection> {
        let fragment_definition = self.program.fragment(fragment_spread.fragment.item)
            .expect("The existence of this fragment spread's definition should have been validated beforehand");

        if fragment_definition
            .directives
            .named(*UPDATABLE_DIRECTIVE)
            .is_some()
        {
            let mut fragment_spread = fragment_spread.clone();
            fragment_spread.directives.push(Directive {
                name: WithLocation::generated(*UPDATABLE_DIRECTIVE_FOR_TYPEGEN),
                arguments: vec![],
                data: None,
            });
            Transformed::Replace(Selection::FragmentSpread(Arc::new(fragment_spread)))
        } else {
            Transformed::Keep
        }
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::collections::HashSet;

use common::Diagnostic;
use common::DiagnosticsResult;
use common::Location;
use graphql_ir::InlineFragment;
use graphql_ir::LinkedField;
use graphql_ir::Selection;
use schema::SDLSchema;
use schema::Schema;
use schema::Type;

use super::errors::ValidationMessage;

/// Ensure that a discriminated union is created by ensuring that
/// * the linked field has an abstract type
/// * the linked field only contains inline fragments with no directives,
///   which refine the type to a unique concrete type and contain an unaliased
///   __typename field with no directives.
pub(super) fn ensure_discriminated_union_is_created(
    schema: &SDLSchema,
    linked_field: &LinkedField,
    reason_message: &'static str,
) -> DiagnosticsResult<()> {
    let mut errors = vec![];
    let type_reference = &schema.field(linked_field.definition.item).type_;

    if !type_reference.inner().is_abstract_type() {
        errors.push(Diagnostic::error(
            ValidationMessage::EnsureDiscriminatedUnionConcreteOuterLinkedField {
                reason_message,
                linked_field_type: schema.get_type_name(type_reference.inner()),
                linked_field_type_variant: match type_reference.inner() {
                    Type::Enum(_) => "Enum",
                    Type::InputObject(_) => "InputObject",
                    Type::Interface(_) => "Interface",
                    Type::Object(_) => "Object",
                    Type::Scalar(_) => "Scalar",
                    Type::Union(_) => "Union",
                }
                .to_string(),
            },
            linked_field.definition.location,
        ));
    }

    let mut encountered_type_conditions = HashSet::new();
    for selection in linked_field.selections.iter() {
        match selection {
            Selection::InlineFragment(inline_fragment) => {
                if let Err(e) = ensure_inline_fragment_is_valid(
                    schema,
                    inline_fragment,
                    linked_field.definition.location,
                    &mut encountered_type_conditions,
                    reason_message,
                ) {
                    errors.extend(e.into_iter());
                }
            }
            _ => errors.push(
                Diagnostic::error(
                    ValidationMessage::EnsureDiscriminatedUnionNonInlineFragment { reason_message },
                    selection.location(),
                )
                .annotate("enclosing linked field", linked_field.definition.location),
            ),
        }
    }

    if !errors.is_empty() {
        Err(errors)
    } else {
        Ok(())
    }
}

/// Ensure that the given inline fragment has a concrete type condition and contains an unaliased
/// __typename field with no directives, and that the inline fragment has no directives. Ensure
/// that no concrete type conditions are repeated.
fn ensure_inline_fragment_is_valid(
    schema: &SDLSchema,
    inline_fragment: &InlineFragment,
    linked_field_location: Location,
    encountered_type_conditions: &mut HashSet<Type>,
    reason_message: &'static str,
) -> DiagnosticsResult<()> {
    let mut errors = vec![];

    // All inline fragments must have type conditions refining to a distinct concrete type
    if let Some(type_condition) = inline_fragment.type_condition {
        if type_condition.is_abstract_type() {
            errors.push(Diagnostic::error(
                ValidationMessage::EnsureDiscriminatedUnionInlineFragmentNotRefineToConcreteType {
                    reason_message,
                },
                linked_field_location,
            ));
        } else {
            if encountered_type_conditions.contains(&type_condition) {
                errors.push(Diagnostic::error(
                        ValidationMessage::EnsureDiscriminatedUnionInlineFragmentDuplicateConcreteTypeRefinement {
							reason_message,
                            concrete_type: schema.get_type_name(type_condition),
                        },
                        linked_field_location
                    ))
            }
            encountered_type_conditions.insert(type_condition);
        }
    } else {
        errors.push(Diagnostic::error(
            ValidationMessage::EnsureDiscriminatedUnionInlineFragmentNotRefineToConcreteType {
                reason_message,
            },
            linked_field_location,
        ));
    }

    // If we don't find an unaliased __typename field with no directives, emit an error
    if !inline_fragment
        .selections
        .iter()
        .any(|selection| match selection {
            Selection::ScalarField(scalar_field) => {
                scalar_field.alias.is_none()
                    && scalar_field.definition.item == schema.typename_field()
                    && scalar_field.directives.is_empty()
            }
            _ => false,
        })
    {
        errors.push(Diagnostic::error(
            ValidationMessage::EnsureDiscriminatedUnionInlineFragmentNoValidTypename {
                reason_message,
            },
            linked_field_location,
        ));
    }

    if !inline_fragment.directives.is_empty() {
        errors.push(Diagnostic::error(
            ValidationMessage::EnsureDiscriminatedUnionNoInlineFragmentWithDirectives {
                reason_message,
            },
            linked_field_location,
        ));
    }

    if !errors.is_empty() {
        Err(errors)
    } else {
        Ok(())
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use graphql_ir::FragmentDefinitionName;
use intern::string_key::StringKey;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum ValidationMessage {
    #[error(
        "The @{disallowed_directive_name} directive is not allowed on assignable fragment spreads."
    )]
    AssignableFragmentSpreadNoOtherDirectives {
        disallowed_directive_name: StringKey,
    },

    #[error("Assignable fragments can only be nested within at most a single inline fragment.")]
    AssignableFragmentSpreadContainingInlineFragmentSingleNesting,

    #[error("Top-level spreads of assignable fragments are not supported.")]
    AssignableNoTopLevelFragmentSpreads,

    #[error(
        "Assignable fragments should contain only a single, unaliased __typename field with no directives."
    )]
    AssignableOnlyUnaliasedTypenameFieldWithNoDirectives,

    #[error("The @{disallowed_directive_name} directive is not allowed on assignable fragments.")]
    AssignableDisallowOtherDirectives {
        disallowed_directive_name: StringKey,
    },

    #[error(
        "Only fragments decorated with the @assignable directive can be spread within updatable {outer_type_plural}. You can try adding the @assignable directive to the fragment {fragment_name}."
    )]
    UpdatableOnlyAssignableFragmentSpreads {
        outer_type_plural: &'static str,
        fragment_name: FragmentDefinitionName,
    },

    #[error(
        "Within updatable {outer_type_plural}, if an assignable fragment is spread on a linked field, the fragment's type (`{fragment_type}`) must be equal to or a subtype of the field's type (`{field_type}`)."
    )]
    UpdatableSpreadOfAssignableFragmentMustBeEqualToOrSubtypeOfOuterField {
        outer_type_plural: &'static str,
        fragment_type: StringKey,
        field_type: StringKey,
    },

    #[error(
        "The @{disallowed_directive_name} directive is not allowed in updatable {outer_type_plural}."
    )]
    UpdatableDisallowOtherDirectives {
        disallowed_directive_name: StringKey,
        outer_type_plural: &'static str,
    },

    #[error("Fields defined using Relay Resolvers are not allowed within @updatable operations.")]
    UpdatableDisallowRelayResolvers,

    #[error("The directives @include and @skip are not allowed within {outer_type_plural}.")]
    UpdatableNoConditions { outer_type_plural: &'static str },

    #[error(
        "Within updatable {outer_type_plural}, if a linked field contains an inline fragment spread, it must contain only inline fragment spreads."
    )]
    UpdatableOnlyInlineFragments { outer_type_plural: &'static str },

    #[error(
        "Within updatable {outer_type_plural}, inline fragments are only allowed on interfaces or unions, not on concrete types. In updatable queries, each inline fragment must have a type conditions, so no inline fragment would make sense here."
    )]
    UpdatableInlineFragmentsOnlyOnInterfacesOrUnions { outer_type_plural: &'static str },

    #[error(
        "Within updatable {outer_type_plural}, each inline fragment spread must have a type condition. An inline fragment without a type condition was among the selections of {parent_field_type}."
    )]
    UpdatableInlineFragmentsRequireTypeConditions {
        outer_type_plural: &'static str,
        parent_field_type: StringKey,
    },

    #[error(
        "Within updatable {outer_type_plural}, each inline fragment spread must have a type condition narrowing the type to a unique concrete type. `{type_condition}` is not a concrete type."
    )]
    UpdatableInlineFragmentsTypeConditionsMustBeConcrete {
        outer_type_plural: &'static str,
        type_condition: StringKey,
    },

    #[error(
        "Within updatable {outer_type_plural}, a single linked field cannot have multiple inline fragments with the same type condition. However, within {parent_field_alias_or_name}, there were multiple inline fragments narrowing the type to `{type_condition}`."
    )]
    UpdatablePreviouslyEncounteredTypeCondition {
        outer_type_plural: &'static str,
        type_condition: StringKey,
        parent_field_alias_or_name: StringKey,
    },

    #[error(
        "Within updatable {outer_type_plural}, each inline fragment spread must contain an unaliased typename field. However, within {parent_field_alias_or_name}, there are inline fragments without typename fields."
    )]
    UpdatableInlineFragmentsMustHaveTypenameFields {
        outer_type_plural: &'static str,
        parent_field_alias_or_name: StringKey,
    },

    #[error(
        "Within updatable {outer_type_plural}, an inline fragment cannot occur immediately within another inline fragment. Found within {operation_or_fragment_name}. This is because all inline fragments must have type conditions and narrow the type from an abstract type to a concrete type."
    )]
    UpdatableNoNestedInlineFragments {
        outer_type_plural: &'static str,
        operation_or_fragment_name: StringKey,
    },

    #[error("Directives are not allowed on spreads of updatable fragments.")]
    UpdatableFragmentSpreadNoDirectives,

    #[error("Updatable fragments cannot be spread at the top level.")]
    UpdatableFragmentTopLevel,

    #[error("Updatable fragments cannot be contained in @skip or @if.")]
    UpdatableFragmentSpreadNoCondition,

    #[error("Updatable fragments can only be nested within at most a single inline fragment.")]
    UpdatableFragmentSpreadContainingInlineFragmentSingleNesting,

    #[error(
        "This updatable fragment has type `{updatable_fragment_type}`, and is found within a linked field with type `{linked_field_type}`. However, if a record has the type `{linked_field_inner_type}`, it does not necessarily have the type `{updatable_fragment_type}`."
    )]
    UpdatableFragmentSpreadSubtypeOrEqualLinkedField {
        updatable_fragment_type: StringKey,
        linked_field_type: String,
        linked_field_inner_type: StringKey,
    },

    #[error(
        "Because {reason_message}, this linked field must have an abstract type, meaning its type must be an Interface or a Union. However, `{linked_field_type}` is a `{linked_field_type_variant}`."
    )]
    EnsureDiscriminatedUnionConcreteOuterLinkedField {
        reason_message: &'static str,
        linked_field_type: StringKey,
        linked_field_type_variant: String,
    },

    #[error(
        "Because {reason_message}, this linked field can only contain inline fragments, and any inline fragments cannot have @skip or @include."
    )]
    EnsureDiscriminatedUnionNonInlineFragment { reason_message: &'static str },

    #[error(
        "Because {reason_message}, each of this linked field's selections must be an inline fragment with no directives, refining the type to a unique concrete type and containing an unaliased __typename field with no directives. However, an inline fragment in this linked field does not refine to a concrete type."
    )]
    EnsureDiscriminatedUnionInlineFragmentNotRefineToConcreteType { reason_message: &'static str },

    #[error(
        "Because {reason_message}, each of this linked field's selections must be an inline fragment with no directives, refining the type to a unique concrete type and containing an unaliased __typename field with no directives. However, multiple inline fragments in this linked field refine to the concrete type `{concrete_type}`."
    )]
    EnsureDiscriminatedUnionInlineFragmentDuplicateConcreteTypeRefinement {
        concrete_type: StringKey,
        reason_message: &'static str,
    },

    #[error(
        "Because {reason_message}, each of this linked field's selections must be an inline fragment with no directives, refining the type to a unique concrete type and containing an unaliased __typename field with no directives. However, an inline fragment in this linked field does not contain an unaliased __typename selection with no directives."
    )]
    EnsureDiscriminatedUnionInlineFragmentNoValidTypename { reason_message: &'static str },

    #[error(
        "Because {reason_message}, each of this linked field's selections must be an inline fragment with no directives, refining the type to a unique concrete type and containing an unaliased __typename field with no directives. However, an inline fragment in this linked field contains directives."
    )]
    EnsureDiscriminatedUnionNoInlineFragmentWithDirectives { reason_message: &'static str },
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

mod annotate_updatable_fragment_spreads;
mod ensure_discriminated_union_is_created;
mod errors;
mod replace_updatable_fragment_spreads;
mod transform_assignable_fragment_spreads_in_regular_queries;
mod transform_assignable_fragment_spreads_in_updatable_queries;
mod validate_assignable_directive;
mod validate_updatable_directive;
mod validate_updatable_fragment_spread;
use common::DirectiveName;
use intern::string_key::Intern;
use lazy_static::lazy_static;

lazy_static! {
    pub static ref ASSIGNABLE_DIRECTIVE: DirectiveName = DirectiveName("assignable".intern());
    pub static ref UPDATABLE_DIRECTIVE: DirectiveName = DirectiveName("updatable".intern());
}

pub use annotate_updatable_fragment_spreads::*;
use ensure_discriminated_union_is_created::*;
pub use replace_updatable_fragment_spreads::*;
pub use transform_assignable_fragment_spreads_in_regular_queries::*;
pub use transform_assignable_fragment_spreads_in_updatable_queries::*;
pub use validate_assignable_directive::*;
pub use validate_updatable_directive::*;
pub use validate_updatable_fragment_spread::*;

use self::errors::ValidationMessage;


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::sync::Arc;

use common::NamedItem;
use common::WithLocation;
use graphql_ir::FragmentSpread;
use graphql_ir::Program;
use graphql_ir::ScalarField;
use graphql_ir::Selection;
use graphql_ir::Transformed;
use graphql_ir::Transformer;
use schema::Schema;

use crate::UPDATABLE_DIRECTIVE;

pub fn replace_updatable_fragment_spreads(program: &Program) -> Program {
    let mut transform = ReplaceAssignableFragmentSpreads { program };

    transform
        .transform_program(program)
        .replace_or_else(|| program.clone())
}

struct ReplaceAssignableFragmentSpreads<'s> {
    program: &'s Program,
}

impl<'s> Transformer for ReplaceAssignableFragmentSpreads<'s> {
    const NAME: &'static str = "ReplaceAssignableFragmentSpreads";
    const VISIT_ARGUMENTS: bool = false;
    const VISIT_DIRECTIVES: bool = false;

    fn transform_fragment_spread(
        &mut self,
        fragment_spread: &FragmentSpread,
    ) -> Transformed<Selection> {
        // This transform runs **after** apply_fragment_arguments, which removes certain fragments.
        // If we don't find a fragment, assume it was correctly removed!
        if let Some(fragment_definition) = self.program.fragment(fragment_spread.fragment.item) {
            if fragment_definition
                .directives
                .named(*UPDATABLE_DIRECTIVE)
                .is_some()
            {
                Transformed::Replace(Selection::ScalarField(Arc::new(ScalarField {
                    alias: None,
                    definition: WithLocation {
                        location: fragment_spread.fragment.location,
                        item: self.program.schema.typename_field(),
                    },
                    arguments: vec![],
                    directives: vec![],
                })))
            } else {
                Transformed::Keep
            }
        } else {
            Transformed::Keep
        }
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::sync::Arc;

use common::Diagnostic;
use common::DiagnosticsResult;
use common::Location;
use common::NamedItem;
use common::WithLocation;
use graphql_ir::Condition;
use graphql_ir::FragmentDefinition;
use graphql_ir::FragmentSpread;
use graphql_ir::InlineFragment;
use graphql_ir::LinkedField;
use graphql_ir::OperationDefinition;
use graphql_ir::Program;
use graphql_ir::ScalarField;
use graphql_ir::Selection;
use graphql_ir::Transformed;
use graphql_ir::Transformer;
use intern::string_key::Intern;
use intern::Lookup;
use schema::Schema;

use super::ensure_discriminated_union_is_created;
use super::errors::ValidationMessage;
use super::ASSIGNABLE_DIRECTIVE;
use super::UPDATABLE_DIRECTIVE;

pub fn transform_assignable_fragment_spreads_in_regular_queries(
    program: &Program,
) -> DiagnosticsResult<Program> {
    let mut transform = AssignableFragmentSpread {
        program,
        errors: Default::default(),
        path: vec![],
    };

    let next_program = transform
        .transform_program(program)
        .replace_or_else(|| program.clone());

    if transform.errors.is_empty() {
        Ok(next_program)
    } else {
        Err(transform.errors)
    }
}

struct AssignableFragmentSpread<'s> {
    program: &'s Program,
    errors: Vec<Diagnostic>,
    path: Vec<PathSegment>,
}

impl<'s> AssignableFragmentSpread<'s> {
    /// 1. Validate that the assignable fragment does not have @skip/@defer, and
    /// is not within an inline fragment with directives, and is nested in a linked field
    /// 2. Mark the enclosing linked field as containing an assignable fragment spread.
    /// This later results in an __id (clientid_field) selection being added to the linked
    /// field.
    fn validate_nesting_and_mark_enclosing_linked_field(
        &mut self,
        fragment_spread: &FragmentSpread,
    ) {
        let mut directly_in_condition = None;
        let mut in_linked_field = false;
        let mut in_inline_fragment = false;

        for (index, item) in self.path.iter_mut().rev().enumerate() {
            match item {
                PathSegment::Condition(c) => {
                    // We can encounter a condition (before a linked field) in two ways:
                    // Directly on the assignable fragment spread: ... Assignable_foo @skip
                    // or
                    // As a directive on an inline fragment ... @skip { ...Assignable_foo }
                    if index == 0 {
                        directly_in_condition = Some(*c);
                    }
                }
                PathSegment::LinkedField {
                    valid_generated_flow_type,
                } => {
                    in_linked_field = true;
                    if in_inline_fragment {
                        *valid_generated_flow_type = ValidGeneratedFlowType::OnlyDiscriminatedUnion;
                    }
                    break;
                }
                PathSegment::InlineFragment => {
                    if in_inline_fragment {
                        self.errors.push(Diagnostic::error(
                            ValidationMessage::AssignableFragmentSpreadContainingInlineFragmentSingleNesting,
                            fragment_spread.fragment.location
                        ));
                    }
                    in_inline_fragment = true;
                }
            }
        }

        if let Some(condition_directive_name) = directly_in_condition {
            self.errors.push(Diagnostic::error(
                ValidationMessage::AssignableFragmentSpreadNoOtherDirectives {
                    disallowed_directive_name: condition_directive_name.intern(),
                },
                fragment_spread.fragment.location,
            ));
        }

        if !in_linked_field {
            self.errors.push(Diagnostic::error(
                ValidationMessage::AssignableNoTopLevelFragmentSpreads,
                fragment_spread.fragment.location,
            ));
        }
    }
}

#[derive(Debug)]
enum PathSegment {
    Condition(&'static str),
    LinkedField {
        valid_generated_flow_type: ValidGeneratedFlowType,
    },
    InlineFragment,
}

#[derive(Debug)]
enum ValidGeneratedFlowType {
    OnlyDiscriminatedUnion,
    Any,
}

impl<'s> Transformer for AssignableFragmentSpread<'s> {
    const NAME: &'static str = "AssignableFragmentTransform";
    const VISIT_ARGUMENTS: bool = false;
    const VISIT_DIRECTIVES: bool = false;

    fn transform_operation(
        &mut self,
        operation: &OperationDefinition,
    ) -> Transformed<OperationDefinition> {
        if operation.directives.named(*UPDATABLE_DIRECTIVE).is_some() {
            Transformed::Keep
        } else {
            self.default_transform_operation(operation)
        }
    }

    fn transform_fragment(
        &mut self,
        fragment_definition: &FragmentDefinition,
    ) -> Transformed<FragmentDefinition> {
        if fragment_definition
            .directives
            .named(*UPDATABLE_DIRECTIVE)
            .is_some()
        {
            Transformed::Keep
        } else {
            self.default_transform_fragment(fragment_definition)
        }
    }

    /// When we encountered a spread of an assignable fragment, we want to return
    /// some additional selections. However, we must return a Transformed<Selection>,
    /// i.e. at most a single selection from this function. So, instead, we return
    /// an inline fragment with no directives and no type condition to house our
    /// additional peer selections and the original fragment spread.
    ///
    /// Thus we return:
    /// - The original fragment spread.
    /// - A fragment spread marker:
    ///   - If the fragment spread's type is abstract, we want to return an additional
    ///     `... on FragmentType { __isFragmentName: __typename }`.
    ///   - If the fragment spread's type is concrete, we either want to return an additional
    ///     `__typename`.
    /// - An unconditional `__id` selection
    ///
    /// So, e.g. we might transform the fragment spread into:
    /// ```graphql
    /// ... {
    ///   ...Original_assignable_node
    ///   ... on Node { __isNode: __typename }
    ///   __id
    /// }
    /// ```
    fn transform_fragment_spread(
        &mut self,
        fragment_spread: &FragmentSpread,
    ) -> Transformed<Selection> {
        let fragment_definition = self
            .program
            .fragment(fragment_spread.fragment.item)
            .expect("Expected fragment to exist.");

        if fragment_definition
            .directives
            .named(*ASSIGNABLE_DIRECTIVE)
            .is_none()
        {
            return Transformed::Keep;
        }

        // Assignable fragments cannot have directives, but we error only on the first one
        if let Some(directive) = fragment_spread.directives.first() {
            self.errors.push(Diagnostic::error(
                ValidationMessage::AssignableFragmentSpreadNoOtherDirectives {
                    disallowed_directive_name: directive.name.item.0,
                },
                directive.name.location,
            ));
        }

        self.validate_nesting_and_mark_enclosing_linked_field(fragment_spread);

        let clientid_selection = Selection::ScalarField(Arc::new(ScalarField {
            alias: None,
            definition: WithLocation::generated(self.program.schema.clientid_field()),
            arguments: vec![],
            directives: vec![],
        }));

        let fragment_spread_marker = if fragment_definition.type_condition.is_abstract_type() {
            Selection::InlineFragment(Arc::new(InlineFragment {
                type_condition: Some(fragment_definition.type_condition),
                directives: vec![],
                selections: vec![Selection::ScalarField(Arc::new(ScalarField {
                    alias: Some(WithLocation::generated(
                        format!("__is{}", fragment_spread.fragment.item.0.lookup()).intern(),
                    )),
                    definition: WithLocation::generated(self.program.schema.typename_field()),
                    arguments: vec![],
                    directives: vec![],
                }))],
                spread_location: Location::generated(),
            }))
        } else {
            Selection::ScalarField(Arc::new(ScalarField {
                alias: None,
                definition: WithLocation::generated(self.program.schema.typename_field()),
                arguments: vec![],
                directives: vec![],
            }))
        };

        Transformed::Replace(Selection::InlineFragment(Arc::new(InlineFragment {
            type_condition: None,
            directives: vec![],
            selections: vec![
                Selection::FragmentSpread(Arc::new(fragment_spread.clone())),
                fragment_spread_marker,
                clientid_selection,
            ],
            spread_location: Location::generated(),
        })))
    }

    fn transform_linked_field(&mut self, linked_field: &LinkedField) -> Transformed<Selection> {
        self.path.push(PathSegment::LinkedField {
            valid_generated_flow_type: ValidGeneratedFlowType::Any,
        });
        let response = self.default_transform_linked_field(linked_field);

        // If we encountered an assignable fragment in an inline fragment, the linked field
        // must result in a discriminated union being created
        let valid_generated_flow_type = if let PathSegment::LinkedField {
            valid_generated_flow_type,
        } = self.path.pop().expect("path should not be empty")
        {
            valid_generated_flow_type
        } else {
            panic!("Unexpected non-linked field");
        };
        if matches!(
            valid_generated_flow_type,
            ValidGeneratedFlowType::OnlyDiscriminatedUnion
        ) {
            if let Err(e) = ensure_discriminated_union_is_created(
                &self.program.schema,
                linked_field,
                "an assignable fragment was spread in this linked field",
            ) {
                self.errors.extend(e.into_iter());
            }
        }

        response
    }

    fn transform_condition(&mut self, condition: &Condition) -> Transformed<Selection> {
        self.path
            .push(PathSegment::Condition(condition.directive_name()));
        let response = self.default_transform_condition(condition);
        self.path.pop().expect("path should not be empty");
        response
    }

    fn transform_inline_fragment(&mut self, fragment: &InlineFragment) -> Transformed<Selection> {
        self.path.push(PathSegment::InlineFragment);
        let response = self.default_transform_inline_fragment(fragment);
        self.path.pop().expect("path should not be empty");
        response
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::sync::Arc;

use common::DirectiveName;
use common::NamedItem;
use common::WithLocation;
use graphql_ir::associated_data_impl;
use graphql_ir::Directive;
use graphql_ir::FragmentDefinition;
use graphql_ir::FragmentSpread;
use graphql_ir::OperationDefinition;
use graphql_ir::Program;
use graphql_ir::Selection;
use graphql_ir::Transformed;
use graphql_ir::Transformer;
use intern::string_key::Intern;
use intern::string_key::StringKey;
use lazy_static::lazy_static;
use schema::SDLSchema;
use schema::Schema;
use schema::Type;

use crate::UPDATABLE_DIRECTIVE;

lazy_static! {
    pub static ref ASSIGNABLE_DIRECTIVE_FOR_TYPEGEN: DirectiveName =
        DirectiveName("__assignable".intern());
}

#[derive(Clone, Debug, PartialEq, Eq, Hash, Copy)]
pub enum TypeConditionInfo {
    Abstract,
    Concrete { concrete_type: StringKey },
}

associated_data_impl!(TypeConditionInfo);

/// Transform all of the updatable queries and fragments in a program by adding
/// an internal-only directive on every fragment spread containing
/// TypeConditionInfo in the associated data.
///
/// This directive is then consumed by the typegen step (which does not have
/// access to the schema) to determine how to create the setter for a given linked
/// field.
///
/// The value that is passed to the setter of a singular linked field is the union of:
/// - null
/// - { __typename: 'ConcreteTypeName', __id: string, $fragmentRefs: spreadRef }
///   for each spread of a concrete type
/// - { __isFragmentName: string, __id: string, $fragmentRefs: spreadRef }
///   for each spread of an abstract type
///
/// For a plural linked field, replace null with [] and the unions with an array of
/// the union of those objects.
pub fn transform_assignable_fragment_spreads_in_updatable_queries(program: &Program) -> Program {
    let mut transform = AssignableFragmentSpreadForUpdatable { program };

    transform
        .transform_program(program)
        .replace_or_else(|| program.clone())
}

struct AssignableFragmentSpreadForUpdatable<'s> {
    program: &'s Program,
}

impl<'s> Transformer for AssignableFragmentSpreadForUpdatable<'s> {
    const NAME: &'static str = "AssignableFragmentTransformForUpdatable";
    const VISIT_ARGUMENTS: bool = false;
    const VISIT_DIRECTIVES: bool = false;

    fn transform_operation(
        &mut self,
        operation: &OperationDefinition,
    ) -> Transformed<OperationDefinition> {
        if operation.directives.named(*UPDATABLE_DIRECTIVE).is_some() {
            self.default_transform_operation(operation)
        } else {
            Transformed::Keep
        }
    }

    fn transform_fragment(
        &mut self,
        operation: &FragmentDefinition,
    ) -> Transformed<FragmentDefinition> {
        if operation.directives.named(*UPDATABLE_DIRECTIVE).is_some() {
            self.default_transform_fragment(operation)
        } else {
            Transformed::Keep
        }
    }

    fn transform_fragment_spread(
        &mut self,
        fragment_spread: &FragmentSpread,
    ) -> Transformed<Selection> {
        let fragment_definition = self
            .program
            .fragment(fragment_spread.fragment.item)
            .expect("Expected fragment to exist.");

        let spread = fragment_spread.clone();
        Transformed::Replace(Selection::FragmentSpread(Arc::new(FragmentSpread {
            directives: vec![get_directive(
                fragment_definition.type_condition,
                &self.program.schema,
            )],
            ..spread
        })))
    }
}

fn get_directive(type_condition: Type, schema: &SDLSchema) -> Directive {
    Directive {
        name: WithLocation::generated(*ASSIGNABLE_DIRECTIVE_FOR_TYPEGEN),
        arguments: vec![],
        data: Some(Box::new(get_associated_data(type_condition, schema))),
    }
}

fn get_associated_data(type_condition: Type, schema: &SDLSchema) -> TypeConditionInfo {
    if type_condition.is_abstract_type() {
        TypeConditionInfo::Abstract
    } else {
        TypeConditionInfo::Concrete {
            concrete_type: schema.get_type_name(type_condition),
        }
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use common::Diagnostic;
use common::DiagnosticsResult;
use common::DirectiveName;
use common::NamedItem;
use graphql_ir::FragmentDefinition;
use graphql_ir::Program;
use graphql_ir::Selection;
use graphql_ir::Validator;
use intern::string_key::Intern;
use lazy_static::lazy_static;
use schema::Schema;

use super::ValidationMessage;
use super::ASSIGNABLE_DIRECTIVE;

lazy_static! {
    static ref ALLOW_LISTED_DIRECTIVES: Vec<DirectiveName> = vec![
        *ASSIGNABLE_DIRECTIVE,
        // TODO have a global list of directives...?
        DirectiveName("fb_owner".intern()),
    ];
}

pub fn validate_assignable_directive(program: &Program) -> DiagnosticsResult<()> {
    AssignableDirective { program }.validate_program(program)
}

struct AssignableDirective<'a> {
    program: &'a Program,
}

impl<'a> Validator for AssignableDirective<'a> {
    const NAME: &'static str = "AssignableDirective";
    const VALIDATE_ARGUMENTS: bool = false;
    const VALIDATE_DIRECTIVES: bool = false;

    fn validate_fragment(&mut self, fragment: &FragmentDefinition) -> DiagnosticsResult<()> {
        if fragment.directives.named(*ASSIGNABLE_DIRECTIVE).is_none() {
            return Ok(());
        }

        let mut errors = vec![];

        // Temporarily, require that assignable fragments contain only a single unaliased
        // __typename field with no directives
        if fragment.selections.len() == 1 {
            let first = fragment
                .selections
                .get(0)
                .expect("Just checked selection length");
            if let Selection::ScalarField(scalar_field) = first {
                if scalar_field.definition.item != self.program.schema.typename_field()
                    || !scalar_field.directives.is_empty()
                    || scalar_field.alias.is_some()
                {
                    errors.push(Diagnostic::error(
                        ValidationMessage::AssignableOnlyUnaliasedTypenameFieldWithNoDirectives,
                        scalar_field.definition.location,
                    ));
                }
            } else {
                errors.push(Diagnostic::error(
                    ValidationMessage::AssignableOnlyUnaliasedTypenameFieldWithNoDirectives,
                    fragment.name.location,
                ));
            }
        } else {
            errors.push(Diagnostic::error(
                ValidationMessage::AssignableOnlyUnaliasedTypenameFieldWithNoDirectives,
                fragment.name.location,
            ));
        }

        for directive in fragment.directives.iter() {
            if !ALLOW_LISTED_DIRECTIVES.contains(&directive.name.item) {
                errors.push(Diagnostic::error(
                    ValidationMessage::AssignableDisallowOtherDirectives {
                        disallowed_directive_name: directive.name.item.0,
                    },
                    directive.name.location,
                ))
            }
        }

        if errors.is_empty() {
            Ok(())
        } else {
            Err(errors)
        }
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::collections::HashSet;

use common::Diagnostic;
use common::DiagnosticsResult;
use common::DirectiveName;
use common::Location;
use common::NamedItem;
use docblock_shared::RELAY_RESOLVER_DIRECTIVE_NAME;
use errors::validate;
use errors::validate_map;
use graphql_ir::Condition;
use graphql_ir::Directive;
use graphql_ir::Field;
use graphql_ir::FragmentDefinition;
use graphql_ir::FragmentSpread;
use graphql_ir::InlineFragment;
use graphql_ir::LinkedField;
use graphql_ir::OperationDefinition;
use graphql_ir::Program;
use graphql_ir::ScalarField;
use graphql_ir::Selection;
use graphql_ir::Validator;
use intern::string_key::Intern;
use intern::string_key::StringKey;
use lazy_static::lazy_static;
use schema::Schema;

use super::ValidationMessage;
use super::ASSIGNABLE_DIRECTIVE;
use super::UPDATABLE_DIRECTIVE;

lazy_static! {
    static ref ALLOW_LISTED_DIRECTIVES: Vec<DirectiveName> = vec![
        *UPDATABLE_DIRECTIVE,
        // TODO have a global list of directives...?
        DirectiveName("fb_owner".intern()),
    ];
}

pub fn validate_updatable_directive(program: &Program) -> DiagnosticsResult<()> {
    UpdatableDirective::new(program).validate_program(program)
}

#[derive(Copy, Clone)]
struct ExecutableDefinitionInfo {
    name: StringKey,
    location: Location,
    type_plural: &'static str,
}

struct UpdatableDirective<'a> {
    executable_definition_info: Option<ExecutableDefinitionInfo>,
    program: &'a Program,
}

impl<'a> UpdatableDirective<'a> {
    fn new(program: &'a Program) -> Self {
        Self {
            program,
            executable_definition_info: None,
        }
    }
}

fn filter_fragment_spreads<'a>(
    selections: impl Iterator<Item = &'a Selection>,
) -> impl Iterator<Item = &'a FragmentSpread> {
    selections.filter_map(|selection| {
        if let Selection::FragmentSpread(fragment_spread) = selection {
            Some(fragment_spread.as_ref())
        } else {
            None
        }
    })
}

fn filter_inline_fragments<'a>(
    selections: impl Iterator<Item = &'a Selection>,
) -> impl Iterator<Item = &'a InlineFragment> {
    selections.filter_map(|selection| {
        if let Selection::InlineFragment(inline_fragment) = selection {
            Some(inline_fragment.as_ref())
        } else {
            None
        }
    })
}

impl<'a> UpdatableDirective<'a> {
    fn validate_fragment_spreads_with_parent(
        &self,
        parent_field: &LinkedField,
        fragment_spreads: impl Iterator<Item = &'a FragmentSpread>,
    ) -> DiagnosticsResult<()> {
        let parent_field_id = parent_field.definition.item;
        let parent_named_type = self.program.schema.field(parent_field_id).type_.inner();
        validate_map(fragment_spreads, |fragment_spread| {
            let mut errors = vec![];

            if let Some(fragment) = self.program.fragments.get(&fragment_spread.fragment.item) {
                // Only fragments which are assignable can be spread.
                if fragment.directives.named(*ASSIGNABLE_DIRECTIVE).is_none() {
                    errors.push(Diagnostic::error(
                        ValidationMessage::UpdatableOnlyAssignableFragmentSpreads {
                            outer_type_plural: self.executable_definition_info.unwrap().type_plural,
                            fragment_name: fragment_spread.fragment.item,
                        },
                        fragment_spread.fragment.location,
                    ));
                }

                // Only fragment spreads whose type is equal to or a subtype of the parent field's type
                // are allowed. In other words, you can spread a fragment on User on a Node field,
                // because a User is always assignable to a Node, but you cannot spread a fragment
                // on Node on a User field, because a Node may not be assignable to a User.
                // Or, you can spread a Node on a Node.
                let fragment_type = fragment.type_condition;
                if !self
                    .program
                    .schema
                    .is_named_type_subtype_of(fragment_type, parent_named_type)
                {
                    errors.push(Diagnostic::error(
                        ValidationMessage::UpdatableSpreadOfAssignableFragmentMustBeEqualToOrSubtypeOfOuterField {
                            outer_type_plural: self.executable_definition_info.unwrap().type_plural,
                            fragment_type: self.program.schema.get_type_name(fragment_type),
                            field_type: self.program.schema.get_type_name(parent_named_type)
                        },
                        fragment_spread.fragment.location
                    ));
                }
            }
            // Note: if the fragment is not found, another validator will surface the error.

            // Assignable fragments cannot have arguments. This is checked in the assignable
            // validator.

            if errors.is_empty() {
                Ok(())
            } else {
                Err(errors)
            }
        })
    }

    fn validate_inline_fragments_with_parent(
        &self,
        parent_field: &LinkedField,
        // Cannot accept an iterator here because we need the length of the vec!
        fragment_spreads: Vec<&'a InlineFragment>,
    ) -> DiagnosticsResult<()> {
        // If we have no fragment spreads, return early
        if fragment_spreads.is_empty() {
            return Ok(());
        }

        // If a linked field contains inline fragments, it must *only* contain inline fragments.
        // This is because there exists a limitation of our typegen: if there are fields within
        // inline fragments with type refinements, and at the top-level, the parent field is not
        // emitted as a disjoint union where the key is the __typename field. Instead, the union
        // is flattened and every field is optional. This breaks type safety for updatable fragments,
        // as users would be able to assign to scalar fields that are not present on a given type.
        let mut errors = vec![];
        if parent_field.selections.len() != fragment_spreads.len() {
            errors.push(Diagnostic::error(
                ValidationMessage::UpdatableOnlyInlineFragments {
                    outer_type_plural: self.executable_definition_info.unwrap().type_plural,
                },
                parent_field.definition.location,
            ));
        }

        // Furthermore, inline fragments are only allowed if the parent type is an interface or union
        let parent_field_id = parent_field.definition.item;
        let parent_named_type = self.program.schema.field(parent_field_id).type_.inner();
        if !parent_named_type.is_abstract_type() {
            errors.push(Diagnostic::error(
                ValidationMessage::UpdatableInlineFragmentsOnlyOnInterfacesOrUnions {
                    outer_type_plural: self.executable_definition_info.unwrap().type_plural,
                },
                parent_field.definition.location,
            ));
        }

        let mut previously_encountered_concrete_types = HashSet::new();
        for fragment_spread in fragment_spreads.into_iter() {
            // A fragment spread on a linked field is valid iff:
            // - it contains a type condition
            // - that type condition is for a concrete type
            // - that concrete type has not occurred before
            // - it contains a typename field with no alias

            match fragment_spread.type_condition {
                None => errors.push(Diagnostic::error(
                    ValidationMessage::UpdatableInlineFragmentsRequireTypeConditions {
                        outer_type_plural: self.executable_definition_info.unwrap().type_plural,
                        parent_field_type: self.program.schema.get_type_name(parent_named_type),
                    },
                    parent_field.definition.location,
                )),
                Some(type_condition) => {
                    let type_condition_name = self.program.schema.get_type_name(type_condition);
                    if type_condition.is_abstract_type() {
                        errors.push(Diagnostic::error(
                            ValidationMessage::UpdatableInlineFragmentsTypeConditionsMustBeConcrete {
                                outer_type_plural: self.executable_definition_info.unwrap().type_plural,
                                type_condition: type_condition_name,
                            },
                            parent_field.definition.location,
                        ))
                    }

                    if previously_encountered_concrete_types.contains(&type_condition_name) {
                        errors.push(Diagnostic::error(
                            ValidationMessage::UpdatablePreviouslyEncounteredTypeCondition {
                                outer_type_plural: self
                                    .executable_definition_info
                                    .unwrap()
                                    .type_plural,
                                type_condition: type_condition_name,
                                parent_field_alias_or_name: parent_field
                                    .alias_or_name(&self.program.schema),
                            },
                            parent_field.definition.location,
                        ));
                    } else {
                        previously_encountered_concrete_types.insert(type_condition_name);
                    }
                }
            }

            // Attempt to find a typename field with no alias, in order to guarantee that
            // the linked field (parent_field) with fragment spreads is written by
            // relay-typegen as a disjoint union.
            if !fragment_spread.selections.iter().any(|selection| {
                if let Selection::ScalarField(scalar_field) = selection {
                    scalar_field.definition.item == self.program.schema.typename_field()
                        && scalar_field.alias.is_none()
                } else {
                    false
                }
            }) {
                errors.push(Diagnostic::error(
                    ValidationMessage::UpdatableInlineFragmentsMustHaveTypenameFields {
                        outer_type_plural: self.executable_definition_info.unwrap().type_plural,
                        parent_field_alias_or_name: parent_field
                            .alias_or_name(&self.program.schema),
                    },
                    parent_field.definition.location,
                ))
            }
        }

        if errors.is_empty() {
            Ok(())
        } else {
            Err(errors)
        }
    }
}

impl<'a> Validator for UpdatableDirective<'a> {
    const NAME: &'static str = "UpdatableDirective";
    const VALIDATE_ARGUMENTS: bool = false;
    const VALIDATE_DIRECTIVES: bool = true;

    fn validate_operation(&mut self, operation: &OperationDefinition) -> DiagnosticsResult<()> {
        if operation.directives.named(*UPDATABLE_DIRECTIVE).is_some() {
            self.executable_definition_info = Some(ExecutableDefinitionInfo {
                name: operation.name.item.0,
                location: operation.name.location,
                type_plural: "operations",
            });
            self.default_validate_operation(operation)
        } else {
            Ok(())
        }
    }

    fn validate_fragment(&mut self, fragment: &FragmentDefinition) -> DiagnosticsResult<()> {
        if fragment.directives.named(*UPDATABLE_DIRECTIVE).is_some() {
            self.executable_definition_info = Some(ExecutableDefinitionInfo {
                name: fragment.name.item.0,
                location: fragment.name.location,
                type_plural: "fragments",
            });
            self.default_validate_fragment(fragment)
        } else {
            Ok(())
        }
    }

    fn validate_directive(&mut self, directive: &Directive) -> DiagnosticsResult<()> {
        if !ALLOW_LISTED_DIRECTIVES.contains(&directive.name.item) {
            Err(vec![Diagnostic::error(
                ValidationMessage::UpdatableDisallowOtherDirectives {
                    disallowed_directive_name: directive.name.item.0,
                    outer_type_plural: self.executable_definition_info.unwrap().type_plural,
                },
                directive.name.location,
            )])
        } else {
            Ok(())
        }
    }

    fn validate_scalar_field(&mut self, field: &ScalarField) -> DiagnosticsResult<()> {
        let field_def = self.program.schema.field(field.definition.item);
        if field_def
            .directives
            .named(*RELAY_RESOLVER_DIRECTIVE_NAME)
            .is_some()
        {
            return Err(vec![
                Diagnostic::error(
                    ValidationMessage::UpdatableDisallowRelayResolvers,
                    field.definition.location,
                )
                .annotate("The field is defined here:", field_def.name.location),
            ]);
        }
        self.default_validate_scalar_field(field)
    }

    fn validate_linked_field(&mut self, linked_field: &LinkedField) -> DiagnosticsResult<()> {
        let fragment_spreads = filter_fragment_spreads(linked_field.selections.iter());
        let inline_fragments = filter_inline_fragments(linked_field.selections.iter());

        let field_def = self.program.schema.field(linked_field.definition.item);
        if field_def
            .directives
            .named(*RELAY_RESOLVER_DIRECTIVE_NAME)
            .is_some()
        {
            return Err(vec![
                Diagnostic::error(
                    ValidationMessage::UpdatableDisallowRelayResolvers,
                    linked_field.definition.location,
                )
                .annotate("The field is defined here:", field_def.name.location),
            ]);
        }

        validate!(
            self.validate_fragment_spreads_with_parent(linked_field, fragment_spreads),
            self.validate_inline_fragments_with_parent(linked_field, inline_fragments.collect()),
            self.validate_selections(&linked_field.selections),
            self.validate_directives(&linked_field.directives)
        )
    }

    fn validate_inline_fragment(
        &mut self,
        inline_fragment: &InlineFragment,
    ) -> DiagnosticsResult<()> {
        let no_immediately_nested_fragments = {
            let mut errors = vec![];

            for selection in &inline_fragment.selections {
                if matches!(selection, Selection::InlineFragment(_)) {
                    errors.push(Diagnostic::error(
                        ValidationMessage::UpdatableNoNestedInlineFragments {
                            outer_type_plural: self.executable_definition_info.unwrap().type_plural,
                            operation_or_fragment_name: self
                                .executable_definition_info
                                .unwrap()
                                .name,
                        },
                        self.executable_definition_info.unwrap().location,
                    ));
                }
            }

            if errors.is_empty() {
                Ok(())
            } else {
                Err(errors)
            }
        };
        validate!(
            self.validate_selections(&inline_fragment.selections),
            self.validate_directives(&inline_fragment.directives),
            no_immediately_nested_fragments
        )
    }

    fn validate_condition(&mut self, condition: &Condition) -> DiagnosticsResult<()> {
        Err(vec![Diagnostic::error(
            ValidationMessage::UpdatableNoConditions {
                outer_type_plural: self.executable_definition_info.unwrap().type_plural,
            },
            condition.location,
        )])
    }
}