/*
 * 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::Argument;
use graphql_ir::Field;
use graphql_ir::FragmentDefinition;
use graphql_ir::InlineFragment;
use graphql_ir::LinkedField;
use graphql_ir::OperationDefinitionName;
use graphql_ir::ScalarField;
use graphql_ir::Selection;
use graphql_ir::Value;
use graphql_ir::Variable;
use graphql_ir::VariableDefinition;
use graphql_ir::VariableName;
use intern::string_key::StringKey;
use relay_config::SchemaConfig;
use schema::Argument as ArgumentDef;
use schema::FieldID;
use schema::InterfaceID;
use schema::SDLSchema;
use schema::Schema;
use schema::Type;

use super::build_fragment_metadata_as_directive;
use super::build_fragment_spread;
use super::build_operation_variable_definitions;
use super::build_used_global_variables;
use super::validation_message::ValidationMessage;
use super::QueryGenerator;
use super::RefetchRoot;
use super::RefetchableIdentifierInfo;
use super::RefetchableMetadata;
use super::CONSTANTS;
use crate::root_variables::VariableMap;

fn build_refetch_operation(
    schema: &SDLSchema,
    schema_config: &SchemaConfig,
    fragment: &Arc<FragmentDefinition>,
    query_name: OperationDefinitionName,
    variables_map: &VariableMap,
) -> DiagnosticsResult<Option<RefetchRoot>> {
    let id_name = schema_config.node_interface_id_field;

    let node_interface_id = schema.get_type(CONSTANTS.node_type_name).and_then(|type_| {
        if let Type::Interface(id) = type_ {
            Some(id)
        } else {
            None
        }
    });

    match node_interface_id {
        None => Ok(None),
        Some(node_interface_id) => {
            let eligible = match fragment.type_condition {
                Type::Interface(id) => {
                    id == node_interface_id
                        || schema
                            .interface(id)
                            .implementing_objects
                            .iter()
                            .all(|&object_id| {
                                schema
                                    .object(object_id)
                                    .interfaces
                                    .iter()
                                    .any(|interface_id| *interface_id == node_interface_id)
                            })
                }
                Type::Object(id) => schema
                    .object(id)
                    .interfaces
                    .iter()
                    .any(|interface_id| *interface_id == node_interface_id),
                Type::Union(id) => schema.union(id).members.iter().all(|&object_id| {
                    schema
                        .object(object_id)
                        .interfaces
                        .iter()
                        .any(|interface_id| *interface_id == node_interface_id)
                }),
                _ => false,
            };
            if !eligible {
                return Ok(None);
            }

            // Check if the fragment type have an `id` field
            let should_generate_inline_fragment_on_node = schema
                .named_field(fragment.type_condition, id_name)
                .is_none();

            let query_type = schema.query_type().unwrap();
            let (node_field_id, id_arg) =
                get_node_field_id_and_id_arg(schema, query_type, fragment)?;
            let node_interface = schema.interface(node_interface_id);
            let id_field_id = *node_interface
                .fields
                .iter()
                .find(|&&id| schema.field(id).name.item == id_name)
                .unwrap_or_else(|| {
                    panic!("Expected `Node` to contain a field named `{:}`.", id_name)
                });

            let fragment = Arc::new(FragmentDefinition {
                directives: build_fragment_metadata_as_directive(
                    fragment,
                    RefetchableMetadata {
                        operation_name: query_name,
                        path: vec![CONSTANTS.node_field_name],
                        identifier_info: Some(RefetchableIdentifierInfo {
                            identifier_field: id_name,
                            identifier_query_variable_name: schema_config
                                .node_interface_id_variable_name,
                        }),
                    },
                ),
                used_global_variables: build_used_global_variables(
                    variables_map,
                    &fragment.variable_definitions,
                )?,
                variable_definitions: fragment.variable_definitions.clone(),
                selections: enforce_selections_with_id_field(
                    fragment,
                    schema,
                    id_field_id,
                    schema_config.node_interface_id_field,
                    if should_generate_inline_fragment_on_node {
                        Some(node_interface_id)
                    } else {
                        None
                    },
                ),
                ..fragment.as_ref().clone()
            });
            let mut variable_definitions = build_operation_variable_definitions(&fragment);
            if let Some(id_argument) = variable_definitions.named(VariableName(id_name)) {
                return Err(vec![Diagnostic::error(
                    ValidationMessage::RefetchableFragmentOnNodeWithExistingID {
                        fragment_name: fragment.name.item,
                    },
                    id_argument.name.location,
                )]);
            }

            variable_definitions.push(VariableDefinition {
                name: WithLocation::new(fragment.name.location, VariableName(id_name)),
                type_: id_arg.type_.non_null(),
                default_value: None,
                directives: vec![],
            });
            Ok(Some(RefetchRoot {
                variable_definitions,
                selections: vec![Selection::LinkedField(Arc::new(LinkedField {
                    alias: None,
                    definition: WithLocation::new(fragment.name.location, node_field_id),
                    arguments: vec![Argument {
                        name: WithLocation::new(fragment.name.location, id_arg.name),
                        value: WithLocation::new(
                            fragment.name.location,
                            Value::Variable(Variable {
                                name: WithLocation::new(
                                    fragment.name.location,
                                    VariableName(id_name),
                                ),
                                type_: id_arg.type_.non_null(),
                            }),
                        ),
                    }],
                    directives: vec![],
                    selections: vec![build_fragment_spread(&fragment)],
                }))],
                fragment,
            }))
        }
    }
}

fn get_node_field_id_and_id_arg<'s>(
    schema: &'s SDLSchema,
    query_type: Type,
    fragment: &FragmentDefinition,
) -> DiagnosticsResult<(FieldID, &'s ArgumentDef)> {
    let node_field_id = schema.named_field(query_type, CONSTANTS.node_field_name);
    if let Some(node_field_id) = node_field_id {
        let node_field = schema.field(node_field_id);
        let mut arg_iter = node_field.arguments.iter();
        if let Some(id_arg) = arg_iter.next() {
            if arg_iter.len() == 0 {
                return Ok((node_field_id, id_arg));
            }
        }
    }
    Err(vec![Diagnostic::error(
        ValidationMessage::InvalidNodeSchemaForRefetchableFragmentOnNode {
            fragment_name: fragment.name.item,
        },
        fragment.name.location,
    )])
}

fn enforce_selections_with_id_field(
    fragment: &FragmentDefinition,
    schema: &SDLSchema,
    id_field_id: FieldID,
    id_name: StringKey,
    node_interface_id: Option<InterfaceID>,
) -> Vec<Selection> {
    let mut next_selections = fragment.selections.clone();
    let has_id_field = next_selections.iter().any(|sel| {
        if let Selection::ScalarField(field) = sel {
            field.alias_or_name(schema) == id_name
                && schema.field(field.definition.item).type_ == schema.field(id_field_id).type_
        } else {
            false
        }
    });
    if has_id_field {
        next_selections
    } else {
        let id_selection: Selection = if let Some(node_interface_id) = node_interface_id {
            Selection::InlineFragment(Arc::new(InlineFragment {
                type_condition: Some(Type::Interface(node_interface_id)),
                directives: vec![],
                selections: vec![Selection::ScalarField(Arc::new(ScalarField {
                    alias: None,
                    definition: WithLocation::new(fragment.name.location, id_field_id),
                    arguments: vec![],
                    directives: vec![],
                }))],
                spread_location: Location::generated(),
            }))
        } else {
            Selection::ScalarField(Arc::new(ScalarField {
                alias: None,
                definition: WithLocation::new(fragment.name.location, id_field_id),
                arguments: vec![],
                directives: vec![],
            }))
        };

        next_selections.push(id_selection);
        next_selections
    }
}

pub const NODE_QUERY_GENERATOR: QueryGenerator = QueryGenerator {
    description: "the Node interface, object types that implement the Node interface, interfaces whose implementing objects all implement Node, and unions whose members all implement Node",
    build_refetch_operation,
};


/*
 * 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::DiagnosticsResult;
use graphql_ir::FragmentDefinition;
use graphql_ir::OperationDefinitionName;
use relay_config::SchemaConfig;
use schema::SDLSchema;
use schema::Schema;

use super::build_fragment_metadata_as_directive;
use super::build_fragment_spread;
use super::build_operation_variable_definitions;
use super::build_used_global_variables;
use super::QueryGenerator;
use super::RefetchRoot;
use super::RefetchableMetadata;
use crate::root_variables::VariableMap;

fn build_refetch_operation(
    schema: &SDLSchema,
    _schema_config: &SchemaConfig,
    fragment: &Arc<FragmentDefinition>,
    query_name: OperationDefinitionName,
    variables_map: &VariableMap,
) -> DiagnosticsResult<Option<RefetchRoot>> {
    let query_type = schema.query_type().unwrap();
    if fragment.type_condition != query_type {
        return Ok(None);
    }

    let fragment = Arc::new(FragmentDefinition {
        directives: build_fragment_metadata_as_directive(
            fragment,
            RefetchableMetadata {
                operation_name: query_name,
                path: vec![],
                identifier_info: None,
            },
        ),
        used_global_variables: build_used_global_variables(
            variables_map,
            &fragment.variable_definitions,
        )?,
        variable_definitions: fragment.variable_definitions.clone(),
        ..fragment.as_ref().clone()
    });
    Ok(Some(RefetchRoot {
        variable_definitions: build_operation_variable_definitions(&fragment),
        selections: vec![build_fragment_spread(&fragment)],
        fragment,
    }))
}

pub const QUERY_QUERY_GENERATOR: QueryGenerator = QueryGenerator {
    description: "the Query type",
    build_refetch_operation,
};


/*
 * 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::ArgumentName;
use common::Diagnostic;
use common::DiagnosticsResult;
use common::DirectiveName;
use common::Location;
use common::SourceLocationKey;
use common::WithLocation;
use graphql_ir::ConstantValue;
use graphql_ir::Directive;
use graphql_ir::OperationDefinitionName;
use graphql_ir::Value;
use graphql_text_printer::print_value;
use graphql_text_printer::PrinterOptions;
use intern::string_key::Intern;
use intern::Lookup;
use lazy_static::lazy_static;
use schema::SDLSchema;

use super::validation_message::ValidationMessage;

lazy_static! {
    pub static ref REFETCHABLE_NAME: DirectiveName = DirectiveName("refetchable".intern());
    static ref QUERY_NAME_ARG: ArgumentName = ArgumentName("queryName".intern());
    static ref DIRECTIVES_ARG: ArgumentName = ArgumentName("directives".intern());
}

/// Represents the @refetchable Relay directive:
///
/// ```graphql
/// directive @refetchable(
///   queryName: String!
///   directives: [String!]
/// ) on FRAGMENT_DEFINITION
/// ```
pub struct RefetchableDirective {
    pub query_name: WithLocation<OperationDefinitionName>,
    pub directives: Vec<Directive>,
}

impl RefetchableDirective {
    pub fn from_directive(schema: &SDLSchema, directive: &Directive) -> DiagnosticsResult<Self> {
        let mut name = None;
        let mut directives = Vec::new();

        for argument in &directive.arguments {
            if argument.name.item == *QUERY_NAME_ARG {
                if let Some(query_name) = argument.value.item.get_string_literal() {
                    name = Some(WithLocation::new(
                        argument.value.location,
                        OperationDefinitionName(query_name),
                    ));
                } else {
                    return Err(vec![Diagnostic::error(
                        ValidationMessage::ExpectQueryNameToBeString {
                            query_name_value: print_value(
                                schema,
                                &argument.value.item,
                                PrinterOptions::default(),
                            ),
                        },
                        argument.name.location,
                    )]);
                }
            } else if argument.name.item == *DIRECTIVES_ARG {
                directives = if let Value::Constant(ConstantValue::List(items)) =
                    &argument.value.item
                {
                    items
                        .iter()
                        .map(|item| {
                            if let ConstantValue::String(directive_string) = item {
                                let ast_directive = graphql_syntax::parse_directive(
                                    directive_string.lookup(),
                                    SourceLocationKey::generated(),
                                    // We don't currently have span information
                                    // for constant values, so we can't derive a
                                    // reasonable offset here.
                                    0
                                )
                                .map_err(|mut diagnostics| {
                                    for diagnostic in &mut diagnostics {
                                        diagnostic.override_location(argument.value.location);
                                    }
                                    diagnostics
                                })?;
                                graphql_ir::build_directive(
                                    schema,
                                    &ast_directive,
                                    graphql_syntax::DirectiveLocation::Query,
                                    // We don't currently have span information
                                    // for constant values, so we can't derive a
                                    // reasonable offset, which means the spans
                                    // attached to `ast_directive` are invalid.
                                    Location::generated(),
                                )
                                .map_err(|mut diagnostics| {
                                    for diagnostic in &mut diagnostics {
                                        diagnostic.override_location(argument.value.location);
                                    }
                                    diagnostics
                                })
                            } else {
                                Err(vec![Diagnostic::error(
                                    ValidationMessage::RefetchableDirectivesArgRequiresLiteralStringList,
                                    argument.value.location,
                                )])
                            }
                        })
                        .collect::<DiagnosticsResult<_>>()
                } else {
                    Err(vec![Diagnostic::error(
                        ValidationMessage::RefetchableDirectivesArgRequiresLiteralStringList,
                        argument.value.location,
                    )])
                }?
            } else {
                // should be validated by general directive validations
                panic!(
                    "Unexpected name on @refetchable query: `{}`",
                    argument.name.item
                )
            }
        }
        Ok(Self {
            query_name: name.unwrap(),
            directives,
        })
    }
}


/*
 * 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::ArgumentName;
use common::Diagnostic;
use common::DiagnosticsResult;
use common::DirectiveName;
use common::NamedItem;
use common::WithLocation;
use graphql_ir::associated_data_impl;
use graphql_ir::Argument;
use graphql_ir::Directive;
use graphql_ir::FragmentDefinition;
use graphql_ir::FragmentDefinitionName;
use graphql_ir::FragmentSpread;
use graphql_ir::OperationDefinitionName;
use graphql_ir::Selection;
use graphql_ir::Value;
use graphql_ir::Variable;
use graphql_ir::VariableDefinition;
use intern::string_key::Intern;
use intern::string_key::StringKey;
use lazy_static::lazy_static;

use super::validation_message::ValidationMessage;
use crate::root_variables::VariableMap;

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct RefetchableIdentifierInfo {
    pub identifier_field: StringKey,
    pub identifier_query_variable_name: StringKey,
}

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct RefetchableMetadata {
    pub operation_name: OperationDefinitionName,
    pub path: Vec<StringKey>,
    pub identifier_info: Option<RefetchableIdentifierInfo>,
}
associated_data_impl!(RefetchableMetadata);

pub struct Constants {
    pub fetchable: DirectiveName,
    pub field_name: ArgumentName,
    pub node_field_name: StringKey,
    pub node_type_name: StringKey,
    pub viewer_field_name: StringKey,
    pub viewer_type_name: StringKey,
}

lazy_static! {
    pub static ref CONSTANTS: Constants = Constants {
        fetchable: DirectiveName("fetchable".intern()),
        field_name: ArgumentName("field_name".intern()),
        node_field_name: "node".intern(),
        node_type_name: "Node".intern(),
        viewer_field_name: "viewer".intern(),
        viewer_type_name: "Viewer".intern(),
    };
}

pub fn build_fragment_spread(fragment: &FragmentDefinition) -> Selection {
    Selection::FragmentSpread(Arc::new(FragmentSpread {
        fragment: fragment.name,
        directives: vec![],
        arguments: fragment
            .variable_definitions
            .iter()
            .map(|var| Argument {
                name: var.name.map(|x| ArgumentName(x.0)),
                value: WithLocation::new(
                    var.name.location,
                    Value::Variable(Variable {
                        name: var.name,
                        type_: var.type_.clone(),
                    }),
                ),
            })
            .collect(),
    }))
}

pub fn build_operation_variable_definitions(
    fragment: &FragmentDefinition,
) -> Vec<VariableDefinition> {
    let mut result: Vec<_> = fragment
        .used_global_variables
        .iter()
        .chain(fragment.variable_definitions.iter())
        .cloned()
        .collect();
    result.sort_unstable_by(|l, r| l.name.item.cmp(&r.name.item));
    result
}

pub fn build_used_global_variables(
    variable_map: &VariableMap,
    local_variable_definitions: &[VariableDefinition],
) -> DiagnosticsResult<Vec<VariableDefinition>> {
    let mut errors = Vec::new();

    let global_variables = variable_map
        .values()
        .map(|var| {
            if let Some(local_conflicting_var) = local_variable_definitions.named(var.name.item) {
                errors.push(Diagnostic::error(
                    ValidationMessage::LocalGlobalVariableConflict {
                        name: local_conflicting_var.name.item,
                    },
                    local_conflicting_var.name.location,
                ));
            };
            VariableDefinition {
                name: var.name,
                type_: var.type_.clone(),
                default_value: None,
                directives: vec![],
            }
        })
        .collect();

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

/// Attach metadata to the operation and fragment.
pub fn build_fragment_metadata_as_directive(
    fragment: &FragmentDefinition,
    metadata: RefetchableMetadata,
) -> Vec<Directive> {
    let mut next_directives = fragment.directives.clone();
    next_directives.push(metadata.into());
    next_directives
}

/// Metadata attached to generated refetch queries storing the name of the
/// fragment the operation was derived from.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct RefetchableDerivedFromMetadata(pub FragmentDefinitionName);
associated_data_impl!(RefetchableDerivedFromMetadata);


/*
 * 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 graphql_ir::OperationDefinitionName;
use graphql_ir::VariableName;
use intern::string_key::StringKey;
use thiserror::Error;

#[derive(Error, Debug)]
pub(super) enum ValidationMessage {
    #[error(
        "Invalid use of @refetchable on fragment '{fragment_name}', only supported are fragments on:\n{descriptions}"
    )]
    UnsupportedRefetchableFragment {
        fragment_name: FragmentDefinitionName,
        descriptions: String,
    },

    #[error(
        "Invalid use of @refetchable on fragment `{fragment_name}`, fragments cannot be annotated with both @refetchable and @relay(plural: true)."
    )]
    InvalidRefetchableFragmentWithRelayPlural {
        fragment_name: FragmentDefinitionName,
    },

    #[error(
        "Duplicate definition for @refetchable operation '{query_name}' from fragments '{first_fragment_name}' and '{second_fragment_name}'"
    )]
    DuplicateRefetchableOperation {
        query_name: OperationDefinitionName,
        first_fragment_name: FragmentDefinitionName,
        second_fragment_name: FragmentDefinitionName,
    },

    #[error(
        "A unique query name has to be specified in `@refetchable`, an operation `{query_name}` already exists."
    )]
    RefetchableQueryConflictWithQuery { query_name: OperationDefinitionName },

    #[error(
        "Invalid use of @refetchable with @connection in fragment '{fragment_name}', at most once @connection can appear in a refetchable fragment."
    )]
    RefetchableWithMultipleConnections {
        fragment_name: FragmentDefinitionName,
    },

    #[error(
        "Invalid use of @refetchable with @connection in fragment '{fragment_name}', refetchable connections cannot appear inside plural fields."
    )]
    RefetchableWithConnectionInPlural {
        fragment_name: FragmentDefinitionName,
    },

    #[error(
        "Invalid use of @refetchable with @connection in fragment '{fragment_name}', refetchable connections must use variables for the {arguments} arguments."
    )]
    RefetchableWithConstConnectionArguments {
        fragment_name: FragmentDefinitionName,
        arguments: &'static str,
    },

    #[error(
        r#"When provided, the `directives` argument to `@refetchable` needs to be a list of literal strings. Each string should be a server directive valid on queries. Example: `@refetchable(queryName: "ExampleQuery", directives: ["@owner(name: \"an owner\")"])"#
    )]
    RefetchableDirectivesArgRequiresLiteralStringList,

    #[error(
        "Invalid use of @refetchable on fragment `{fragment_name}`, this fragment already has an `$id` variable in scope."
    )]
    RefetchableFragmentOnNodeWithExistingID {
        fragment_name: FragmentDefinitionName,
    },

    #[error(
        "Invalid use of @refetchable on fragment '{fragment_name}', check that your schema defines a `Node {{ id: ID }}` interface and has a `node(id: ID): Node` field on the query type (the id argument may also be non-null)."
    )]
    InvalidNodeSchemaForRefetchableFragmentOnNode {
        fragment_name: FragmentDefinitionName,
    },

    #[error(
        "Invalid use of @refetchable on fragment '{fragment_name}', check that your schema defines a 'Viewer' object type and has a 'viewer: Viewer' field on the query type."
    )]
    InvalidViewerSchemaForRefetchableFragmentOnViewer {
        fragment_name: FragmentDefinitionName,
    },

    // T139416294 this error message doesn't appear to be accurate
    #[error(
        "Invalid use of @refetchable with @connection in fragment '{fragment_name}', check that your schema defines a `directive @fetchable(field_name: String!) on OBJECT` or on `INTERFACE`."
    )]
    InvalidRefetchDirectiveDefinition {
        fragment_name: FragmentDefinitionName,
    },

    #[error(
        "Invalid use of @refetchable on fragment '{fragment_name}', the type '{type_name}' is @fetchable but the identifying field '{identifier_field_name}' does not have type 'ID'."
    )]
    InvalidRefetchIdentifyingField {
        fragment_name: FragmentDefinitionName,
        identifier_field_name: StringKey,
        type_name: StringKey,
    },

    #[error(
        "Invalid use of @refetchable on fragment '{fragment_name}', the type '{type_name}' is @fetchable but there is no corresponding '{fetch_field_name}' field or it is invalid (expected '{fetch_field_name}(id: ID!): ${type_name}')."
    )]
    InvalidRefetchFetchField {
        fetch_field_name: StringKey,
        fragment_name: FragmentDefinitionName,
        type_name: StringKey,
    },

    #[error(
        "Expected the 'queryName' argument of @refetchable to be a string, got '{query_name_value}"
    )]
    ExpectQueryNameToBeString { query_name_value: String },

    #[error(
        "Fragment variable `${name}` conflicts with a global variable generated by the @refetchable generated query"
    )]
    LocalGlobalVariableConflict { 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::sync::Arc;

use common::Diagnostic;
use common::DiagnosticsResult;
use common::WithLocation;
use graphql_ir::FragmentDefinition;
use graphql_ir::LinkedField;
use graphql_ir::OperationDefinitionName;
use graphql_ir::Selection;
use relay_config::SchemaConfig;
use schema::FieldID;
use schema::SDLSchema;
use schema::Schema;
use schema::Type;

use super::build_fragment_metadata_as_directive;
use super::build_fragment_spread;
use super::build_operation_variable_definitions;
use super::build_used_global_variables;
use super::validation_message::ValidationMessage;
use super::QueryGenerator;
use super::RefetchRoot;
use super::RefetchableMetadata;
use super::CONSTANTS;
use crate::root_variables::VariableMap;

fn build_refetch_operation(
    schema: &SDLSchema,
    _schema_config: &SchemaConfig,
    fragment: &Arc<FragmentDefinition>,
    query_name: OperationDefinitionName,
    variables_map: &VariableMap,
) -> DiagnosticsResult<Option<RefetchRoot>> {
    if schema.get_type_name(fragment.type_condition) != CONSTANTS.viewer_type_name {
        return Ok(None);
    }
    let query_type = schema.query_type().unwrap();
    let viewer_field_id = get_viewer_field_id(schema, query_type, fragment)?;

    let fragment = Arc::new(FragmentDefinition {
        directives: build_fragment_metadata_as_directive(
            fragment,
            RefetchableMetadata {
                operation_name: query_name,
                path: vec![CONSTANTS.viewer_field_name],
                identifier_info: None,
            },
        ),
        used_global_variables: build_used_global_variables(
            variables_map,
            &fragment.variable_definitions,
        )?,
        variable_definitions: fragment.variable_definitions.clone(),
        ..fragment.as_ref().clone()
    });
    Ok(Some(RefetchRoot {
        variable_definitions: build_operation_variable_definitions(&fragment),
        selections: vec![Selection::LinkedField(Arc::new(LinkedField {
            alias: None,
            definition: WithLocation::new(fragment.name.location, viewer_field_id),
            arguments: vec![],
            directives: vec![],
            selections: vec![build_fragment_spread(&fragment)],
        }))],
        fragment,
    }))
}

fn get_viewer_field_id(
    schema: &SDLSchema,
    query_type: Type,
    fragment: &FragmentDefinition,
) -> DiagnosticsResult<FieldID> {
    let viewer_type = schema.get_type(CONSTANTS.viewer_type_name);
    let viewer_field_id = schema.named_field(query_type, CONSTANTS.viewer_field_name);
    if let Some(viewer_type) = viewer_type {
        if let Some(viewer_field_id) = viewer_field_id {
            let viewer_field = schema.field(viewer_field_id);
            if viewer_type.is_object()
                && viewer_type == viewer_field.type_.inner()
                && viewer_type == fragment.type_condition
                && viewer_field.arguments.is_empty()
            {
                return Ok(viewer_field_id);
            }
        }
    }
    Err(vec![Diagnostic::error(
        ValidationMessage::InvalidViewerSchemaForRefetchableFragmentOnViewer {
            fragment_name: fragment.name.item,
        },
        fragment.name.location,
    )])
}

pub const VIEWER_QUERY_GENERATOR: QueryGenerator = QueryGenerator {
    description: "the Viewer type",
    build_refetch_operation,
};


/*
 * 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 requireable_field;
mod validation_message;

use std::borrow::Cow;
use std::mem;
use std::sync::Arc;

use common::ArgumentName;
use common::Diagnostic;
use common::DiagnosticsResult;
use common::DirectiveName;
use common::Location;
use common::NamedItem;
use common::WithLocation;
use graphql_ir::associated_data_impl;
use graphql_ir::Directive;
use graphql_ir::Field;
use graphql_ir::FragmentDefinition;
use graphql_ir::FragmentDefinitionNameMap;
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::TransformedValue;
use graphql_ir::Transformer;
use intern::string_key::Intern;
use intern::string_key::StringKey;
use intern::string_key::StringKeyMap;
use intern::Lookup;
use lazy_static::lazy_static;
use requireable_field::RequireableField;
use requireable_field::RequiredMetadata;

use self::validation_message::ValidationMessage;
use crate::DirectiveFinder;
use crate::FragmentAliasMetadata;

lazy_static! {
    pub static ref REQUIRED_DIRECTIVE_NAME: DirectiveName = DirectiveName("required".intern());
    pub static ref ACTION_ARGUMENT: ArgumentName = ArgumentName("action".intern());
    pub static ref CHILDREN_CAN_BUBBLE_METADATA_KEY: DirectiveName =
        DirectiveName("__childrenCanBubbleNull".intern());
    static ref THROW_ACTION: StringKey = "THROW".intern();
    static ref LOG_ACTION: StringKey = "LOG".intern();
    static ref NONE_ACTION: StringKey = "NONE".intern();
    static ref INLINE_DIRECTIVE_NAME: DirectiveName = DirectiveName("inline".intern());
}

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct RequiredMetadataDirective {
    pub action: RequiredAction,
    pub path: StringKey,
}
associated_data_impl!(RequiredMetadataDirective);

pub fn required_directive(program: &Program) -> DiagnosticsResult<Program> {
    let mut transform = RequiredDirective::new(program);

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

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

// #[derive(Clone)]
struct MaybeRequiredField {
    required: Option<RequiredMetadata>,
    field_name: WithLocation<StringKey>,
}

struct RequiredField {
    required: RequiredMetadata,
    field_name: WithLocation<StringKey>,
}

struct RequiredDirective<'s> {
    program: &'s Program,
    errors: Vec<Diagnostic>,
    path: Vec<&'s str>,
    within_abstract_inline_fragment: bool,
    parent_inline_fragment_directive: Option<Location>,
    path_required_map: StringKeyMap<MaybeRequiredField>,
    current_node_required_children: StringKeyMap<RequiredField>,
    required_children_map: StringKeyMap<StringKeyMap<RequiredField>>,
    required_directive_visitor: RequiredDirectiveVisitor<'s>,
}

impl<'program> RequiredDirective<'program> {
    fn new(program: &'program Program) -> Self {
        Self {
            program,
            errors: Default::default(),
            path: vec![],
            within_abstract_inline_fragment: false,
            parent_inline_fragment_directive: None,
            path_required_map: Default::default(),
            current_node_required_children: Default::default(),
            required_children_map: Default::default(),
            required_directive_visitor: RequiredDirectiveVisitor {
                program,
                visited_fragments: Default::default(),
            },
        }
    }

    fn reset_state(&mut self) {
        self.path_required_map = Default::default();
        self.current_node_required_children = Default::default();
        self.parent_inline_fragment_directive = None;
        self.required_children_map = Default::default();
    }

    fn assert_not_within_abstract_inline_fragment(&mut self, directive_location: Location) {
        if self.within_abstract_inline_fragment {
            self.errors.push(Diagnostic::error(
                ValidationMessage::RequiredWithinAbstractInlineFragment,
                // TODO(T70172661): Also referece the location of the inline fragment, once they have a location.
                directive_location,
            ))
        }
    }

    fn assert_not_within_inline_directive(&mut self, directive_location: Location) {
        if let Some(location) = self.parent_inline_fragment_directive {
            self.errors.push(
                Diagnostic::error(
                    ValidationMessage::RequiredWithinInlineDirective,
                    directive_location,
                )
                .annotate("The fragment is annotated as @inline here.", location),
            )
        }
    }

    fn assert_compatible_nullability(&mut self, path: StringKey, current: MaybeRequiredField) {
        if let Some(previous) = self.path_required_map.get(&path) {
            if let Some(previous_metadata) = &previous.required {
                if let Some(current_metadata) = current.required {
                    if previous_metadata.action != current_metadata.action {
                        self.errors.push(
                            Diagnostic::error(
                                ValidationMessage::RequiredActionMismatch {
                                    field_name: current.field_name.item,
                                },
                                previous_metadata.action_location,
                            )
                            .annotate(
                                "should be the same as the `action` declared here",
                                current_metadata.action_location,
                            ),
                        )
                    }
                } else {
                    self.errors.push(
                        Diagnostic::error(
                            ValidationMessage::RequiredFieldMismatch {
                                field_name: current.field_name.item,
                            },
                            previous.field_name.location,
                        )
                        .annotate("but not @required here", current.field_name.location),
                    );
                }
            } else if current.required.is_some() {
                self.errors.push(
                    Diagnostic::error(
                        ValidationMessage::RequiredFieldMismatch {
                            field_name: current.field_name.item,
                        },
                        current.field_name.location,
                    )
                    .annotate("but not @required here", previous.field_name.location),
                )
            }
        } else {
            self.path_required_map.insert(path, current);
        }
    }

    fn get_required_metadata<T: RequireableField>(
        &mut self,
        field: &T,
        path_name: StringKey,
    ) -> Option<RequiredMetadata> {
        let maybe_required = match field.required_metadata() {
            Err(err) => {
                self.errors.push(err);
                return None;
            }
            Ok(required) => required,
        };

        let field_name = field.name_with_location(&self.program.schema);

        if let Some(metadata) = maybe_required {
            self.assert_not_within_abstract_inline_fragment(metadata.directive_location);
            self.assert_not_within_inline_directive(metadata.directive_location);
            self.current_node_required_children.insert(
                path_name,
                RequiredField {
                    field_name,
                    required: metadata,
                },
            );
        }

        self.assert_compatible_nullability(
            path_name,
            MaybeRequiredField {
                required: maybe_required,
                field_name,
            },
        );
        maybe_required
    }

    fn assert_compatible_required_children_severity(
        &mut self,
        required_metadata: RequiredMetadata,
    ) {
        let parent_action = required_metadata.action;
        for required_child in self.current_node_required_children.values() {
            if required_child.required.action < parent_action {
                self.errors.push(
                    Diagnostic::error(
                        ValidationMessage::RequiredFieldInvalidNesting {
                            suggested_action: required_child.required.action.into(),
                        },
                        required_metadata.action_location,
                    )
                    .annotate(
                        "so that it can match its parent",
                        required_child.required.action_location,
                    ),
                );
            }
        }
    }
    fn assert_compatible_required_children<T: RequireableField>(
        &mut self,
        field: &T,
        field_path: StringKey,
    ) {
        let previous_required_children = match self.required_children_map.get(&field_path) {
            Some(it) => it,
            _ => {
                // We haven't seen any other instances of this field, so there's no validation to perform.
                return;
            }
        };

        // Check if this field has a required child field which was omitted in a previously encountered parent.
        for (path, required_child) in self.current_node_required_children.iter() {
            if !previous_required_children.contains_key(path) {
                if let Some(other_parent) = self.path_required_map.get(&field_path) {
                    self.errors.push(
                        Diagnostic::error(
                            ValidationMessage::RequiredFieldMissing {
                                field_name: required_child.field_name.item,
                            },
                            required_child.field_name.location,
                        )
                        .annotate("but is missing from", other_parent.field_name.location),
                    )
                } else {
                    // We want to give a location of the other parent which is
                    // missing this field. We expect that we will be able to
                    // find it in `self.path_required_map` since it should
                    // contain data about every visited field in this program
                    // and the other parent _must_ have already been visited.
                    panic!("Could not find other parent node at path \"{}\".", {
                        field_path
                    });
                }
            }
        }

        // Check if a previous reference to this field had a required child field which we are missing.
        for (path, required_child) in previous_required_children.iter() {
            if !self.current_node_required_children.contains_key(path) {
                self.errors.push(
                    Diagnostic::error(
                        ValidationMessage::RequiredFieldMissing {
                            field_name: required_child.field_name.item,
                        },
                        required_child.field_name.location,
                    )
                    .annotate(
                        "but is missing from",
                        field.name_with_location(&self.program.schema).location,
                    ),
                )
            }
        }
    }
}

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

    fn transform_fragment(
        &mut self,
        fragment: &FragmentDefinition,
    ) -> Transformed<FragmentDefinition> {
        if !self.required_directive_visitor.visit_fragment(fragment) {
            return Transformed::Keep;
        }
        self.reset_state();
        self.parent_inline_fragment_directive = fragment
            .directives
            .named(*INLINE_DIRECTIVE_NAME)
            .map(|inline_directive| inline_directive.name.location);

        let selections = self.transform_selections(&fragment.selections);
        let directives = maybe_add_children_can_bubble_metadata_directive(
            &fragment.directives,
            &self.current_node_required_children,
        );
        if selections.should_keep() && directives.should_keep() {
            return Transformed::Keep;
        }
        Transformed::Replace(FragmentDefinition {
            directives: directives.replace_or_else(|| fragment.directives.clone()),
            selections: selections.replace_or_else(|| fragment.selections.clone()),
            ..fragment.clone()
        })
    }

    fn transform_operation(
        &mut self,
        operation: &OperationDefinition,
    ) -> Transformed<OperationDefinition> {
        if !self
            .required_directive_visitor
            .find(operation.selections.iter().collect())
        {
            return Transformed::Keep;
        }
        self.reset_state();
        let selections = self.transform_selections(&operation.selections);
        let directives = maybe_add_children_can_bubble_metadata_directive(
            &operation.directives,
            &self.current_node_required_children,
        );
        if selections.should_keep() && directives.should_keep() {
            return Transformed::Keep;
        }
        Transformed::Replace(OperationDefinition {
            directives: directives.replace_or_else(|| operation.directives.clone()),
            selections: selections.replace_or_else(|| operation.selections.clone()),
            ..operation.clone()
        })
    }

    fn transform_scalar_field(&mut self, field: &ScalarField) -> Transformed<Selection> {
        let name = field.alias_or_name(&self.program.schema).lookup();
        self.path.push(name);
        let path_name = self.path.join(".").intern();
        self.path.pop();

        match self.get_required_metadata(field, path_name) {
            None => Transformed::Keep,
            Some(required_metadata) => {
                Transformed::Replace(Selection::ScalarField(Arc::new(ScalarField {
                    directives: add_metadata_directive(
                        &field.directives,
                        path_name,
                        required_metadata.action,
                    ),
                    ..field.clone()
                })))
            }
        }
    }

    fn transform_linked_field(&mut self, field: &LinkedField) -> Transformed<Selection> {
        let name = field.alias_or_name(&self.program.schema).lookup();
        self.path.push(name);
        let path_name = self.path.join(".").intern();

        let maybe_required_metadata = self.get_required_metadata(field, path_name);
        let next_directives = match maybe_required_metadata {
            Some(required_metadata) => Cow::from(add_metadata_directive(
                &field.directives,
                path_name,
                required_metadata.action,
            )),
            None => Cow::from(&field.directives),
        };

        // Once we've handled our own directive, take the parent's required
        // children map, leaving behind an empty/default map which our children
        // can populate.
        let parent_node_required_children = mem::take(&mut self.current_node_required_children);

        let previous_abstract_fragment =
            mem::replace(&mut self.within_abstract_inline_fragment, false);

        let selections = self.transform_selections(&field.selections);

        self.assert_compatible_required_children(field, path_name);
        if let Some(required_metadata) = maybe_required_metadata {
            self.assert_compatible_required_children_severity(required_metadata);
        }

        let next_directives_with_metadata = maybe_add_children_can_bubble_metadata_directive(
            &next_directives,
            &self.current_node_required_children,
        );

        self.within_abstract_inline_fragment = previous_abstract_fragment;

        let required_children = mem::replace(
            &mut self.current_node_required_children,
            parent_node_required_children,
        );

        self.required_children_map
            .insert(path_name, required_children);

        self.path.pop();

        if selections.should_keep()
            && next_directives_with_metadata.should_keep()
            && maybe_required_metadata.is_none()
        {
            Transformed::Keep
        } else {
            Transformed::Replace(Selection::LinkedField(Arc::new(LinkedField {
                directives: next_directives_with_metadata
                    .replace_or_else(|| next_directives.into()),
                selections: selections.replace_or_else(|| field.selections.clone()),
                ..field.clone()
            })))
        }
    }

    fn transform_inline_fragment(&mut self, fragment: &InlineFragment) -> Transformed<Selection> {
        let previous = self.within_abstract_inline_fragment;

        let maybe_alias =
            FragmentAliasMetadata::find(&fragment.directives).map(|metadata| metadata.alias.item);

        if let Some(alias) = maybe_alias {
            self.path.push(alias.lookup())
        } else if let Some(type_) = fragment.type_condition {
            if type_.is_abstract_type() {
                self.within_abstract_inline_fragment = true;
            }
        }

        let next_fragment = self.default_transform_inline_fragment(fragment);

        if maybe_alias.is_some() {
            self.path.pop();
        }

        self.within_abstract_inline_fragment = previous;
        next_fragment
    }
}

fn add_metadata_directive(
    directives: &[Directive],
    path_name: StringKey,
    action: RequiredAction,
) -> Vec<Directive> {
    let mut next_directives: Vec<Directive> = Vec::with_capacity(directives.len() + 1);
    next_directives.extend(directives.iter().cloned());
    next_directives.push(
        RequiredMetadataDirective {
            action,
            path: path_name,
        }
        .into(),
    );
    next_directives
}

fn maybe_add_children_can_bubble_metadata_directive(
    directives: &[Directive],
    current_node_required_children: &StringKeyMap<RequiredField>,
) -> TransformedValue<Vec<Directive>> {
    let children_can_bubble = current_node_required_children
        .values()
        .any(|child| child.required.action != RequiredAction::Throw);

    if !children_can_bubble {
        return TransformedValue::Keep;
    }
    let mut next_directives: Vec<Directive> = Vec::with_capacity(directives.len() + 1);
    for directive in directives.iter() {
        next_directives.push(directive.clone());
    }

    next_directives.push(Directive {
        name: WithLocation::generated(*CHILDREN_CAN_BUBBLE_METADATA_KEY),
        arguments: vec![],
        data: None,
    });
    TransformedValue::Replace(next_directives)
}

// Possible @required `action` enum values ordered by severity.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Debug, Hash)]
pub enum RequiredAction {
    None,
    Log,
    Throw,
}

impl Into<StringKey> for RequiredAction {
    fn into(self) -> StringKey {
        match self {
            RequiredAction::None => *NONE_ACTION,
            RequiredAction::Log => *LOG_ACTION,
            RequiredAction::Throw => *THROW_ACTION,
        }
    }
}

impl From<StringKey> for RequiredAction {
    fn from(action: StringKey) -> Self {
        match action {
            _ if action == *THROW_ACTION => Self::Throw,
            _ if action == *LOG_ACTION => Self::Log,
            _ if action == *NONE_ACTION => Self::None,
            // Actions that don't conform to the GraphQL schema should have been filtered out in IR validation.
            _ => unreachable!(),
        }
    }
}

struct RequiredDirectiveVisitor<'s> {
    program: &'s Program,
    visited_fragments: FragmentDefinitionNameMap<bool>,
}

impl<'s> DirectiveFinder for RequiredDirectiveVisitor<'s> {
    fn visit_directive(&self, directive: &Directive) -> bool {
        directive.name.item == *REQUIRED_DIRECTIVE_NAME
    }

    fn visit_fragment_spread(&mut self, fragment_spread: &graphql_ir::FragmentSpread) -> bool {
        let fragment = self
            .program
            .fragment(fragment_spread.fragment.item)
            .unwrap();
        self.visit_fragment(fragment)
    }
}

impl<'s> RequiredDirectiveVisitor<'s> {
    fn visit_fragment(&mut self, fragment: &FragmentDefinition) -> bool {
        if let Some(val) = self.visited_fragments.get(&fragment.name.item) {
            return *val;
        }
        // Avoid dead loop in self-referencing fragments
        self.visited_fragments.insert(fragment.name.item, false);
        let result = self.find(fragment.selections.iter().collect());
        self.visited_fragments.insert(fragment.name.item, result);
        result
    }
}


/*
 * 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::Location;
use common::NamedItem;
use common::WithLocation;
use graphql_ir::Directive;
use graphql_ir::Field;
use graphql_ir::LinkedField;
use graphql_ir::ScalarField;
use intern::string_key::StringKey;
use schema::SDLSchema;

use super::validation_message::ValidationMessage;
use super::ACTION_ARGUMENT;
use super::REQUIRED_DIRECTIVE_NAME;
use crate::RequiredAction;

#[derive(Clone, Copy)]
pub struct RequiredMetadata {
    pub action: RequiredAction,
    pub directive_location: Location,
    pub action_location: Location,
}

pub trait RequireableField {
    fn directives(&self) -> &Vec<Directive>;
    fn name_with_location(&self, schema: &SDLSchema) -> WithLocation<StringKey>;
    fn required_metadata(&self) -> Result<Option<RequiredMetadata>, Diagnostic> {
        if let Some(required_directive) = self.directives().named(*REQUIRED_DIRECTIVE_NAME) {
            let action_arg = get_action_argument(required_directive)?;
            Ok(Some(RequiredMetadata {
                action: action_arg.item,
                action_location: action_arg.location,
                directive_location: required_directive.name.location,
            }))
        } else {
            Ok(None)
        }
    }
}

impl RequireableField for ScalarField {
    fn directives(&self) -> &Vec<Directive> {
        &self.directives
    }
    fn name_with_location(&self, schema: &SDLSchema) -> WithLocation<StringKey> {
        WithLocation::new(self.alias_or_name_location(), self.alias_or_name(schema))
    }
}

impl RequireableField for LinkedField {
    fn directives(&self) -> &Vec<Directive> {
        &self.directives
    }
    fn name_with_location(&self, schema: &SDLSchema) -> WithLocation<StringKey> {
        WithLocation::new(self.alias_or_name_location(), self.alias_or_name(schema))
    }
}

fn get_action_argument(
    required_directive: &Directive,
) -> Result<WithLocation<RequiredAction>, Diagnostic> {
    let maybe_action_arg = required_directive.arguments.named(*ACTION_ARGUMENT);
    let action_arg = maybe_action_arg.ok_or_else(|| {
        Diagnostic::error(
            ValidationMessage::RequiredActionArgumentRequired,
            required_directive.name.location,
        )
    })?;

    let action = action_arg.value.item.expect_constant().unwrap_enum();
    Ok(WithLocation::new(
        action_arg.value.location,
        RequiredAction::from(action),
    ))
}


/*
 * 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 intern::string_key::StringKey;
use thiserror::Error;

#[derive(Error, Debug)]
pub(super) enum ValidationMessage {
    #[error(
        "Unexpected @required within inline fragment on an abstract type. At runtime we cannot know if this field is null, or if it's missing because the inline fragment did not match. Consider using `@alias` to give your inline fragment a name."
    )]
    RequiredWithinAbstractInlineFragment,

    #[error("@required is not supported within @inline fragments.")]
    RequiredWithinInlineDirective,

    #[error("Missing `action` argument. @required expects an `action` argument")]
    RequiredActionArgumentRequired,

    #[error(
        "All references to a @required field must have matching `action` arguments. The `action` used for '{field_name}'"
    )]
    RequiredActionMismatch { field_name: StringKey },

    #[error(
        "All references to a field must have matching @required declarations. The field '{field_name}` is @required here"
    )]
    RequiredFieldMismatch { field_name: StringKey },

    #[error(
        "@required fields must be included in all instances of their parent. The field '{field_name}` is marked as @required here"
    )]
    RequiredFieldMissing { field_name: StringKey },

    #[error(
        "A @required field may not have an `action` less severe than that of its @required parent. This @required directive should probably have `action: {suggested_action}`"
    )]
    RequiredFieldInvalidNesting { suggested_action: StringKey },
}


/*
 * 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::DiagnosticTag;
use common::DiagnosticsResult;
use common::WithLocation;
use graphql_ir::Argument;
use graphql_ir::Directive;
use graphql_ir::ExecutableDefinition;
use graphql_ir::LinkedField;
use graphql_ir::Program;
use graphql_ir::ScalarField;
use graphql_ir::ValidationMessage;
use graphql_ir::Validator;
use graphql_ir::Value;
use schema::FieldID;
use schema::SDLSchema;
use schema::Schema;

pub fn deprecated_fields(
    schema: &Arc<SDLSchema>,
    program: &Program,
) -> DiagnosticsResult<Vec<Diagnostic>> {
    let mut validator = DeprecatedFields::new(schema);
    validator.validate_program(program)?;
    Ok(validator.warnings)
}

pub fn deprecated_fields_for_executable_definition(
    schema: &Arc<SDLSchema>,
    definition: &ExecutableDefinition,
) -> DiagnosticsResult<Vec<Diagnostic>> {
    let mut validator = DeprecatedFields::new(schema);

    match definition {
        ExecutableDefinition::Fragment(fragment) => validator.validate_fragment(fragment),
        ExecutableDefinition::Operation(operation) => validator.validate_operation(operation),
    }?;
    Ok(validator.warnings)
}

struct DeprecatedFields<'a> {
    schema: &'a Arc<SDLSchema>,
    warnings: Vec<Diagnostic>,
}

impl<'a> DeprecatedFields<'a> {
    fn new(schema: &'a Arc<SDLSchema>) -> Self {
        Self {
            schema,
            warnings: vec![],
        }
    }

    fn validate_field(&mut self, field_id: &WithLocation<FieldID>, arguments: &[Argument]) {
        let schema = &self.schema;
        let field_definition = schema.field(field_id.item);
        if let Some(deprecation) = field_definition.deprecated() {
            let parent_type = field_definition.parent_type.unwrap();
            let parent_name = schema.get_type_name(parent_type);

            self.warnings.push(Diagnostic::hint(
                ValidationMessage::DeprecatedField {
                    field_name: field_definition.name.item,
                    parent_name,
                    deprecation_reason: deprecation.reason,
                },
                field_id.location,
                vec![DiagnosticTag::DEPRECATED],
            ));
        }

        for arg in arguments {
            if let Some(arg_definition) = field_definition.arguments.named(arg.name.item) {
                if let Some(directive) = arg_definition.deprecated() {
                    let parent_type = field_definition.parent_type.unwrap();
                    let parent_name = schema.get_type_name(parent_type);

                    self.warnings.push(Diagnostic::hint(
                        ValidationMessage::DeprecatedFieldArgument {
                            argument_name: arg.name.item,
                            field_name: field_definition.name.item,
                            parent_name,
                            deprecation_reason: directive.reason,
                        },
                        arg.name.location,
                        vec![DiagnosticTag::DEPRECATED],
                    ));
                }
            }
        }
    }
}

// While the individual methods return a diagnostic, since using deprecated fields are not errors per-se, we reserve
// returning an `Err` for cases where we are unable to correctly check.
// Deprecation warnings are collected in `self.warnings`.
impl<'a> Validator for DeprecatedFields<'a> {
    const NAME: &'static str = "DeprecatedFields";
    const VALIDATE_ARGUMENTS: bool = false;
    const VALIDATE_DIRECTIVES: bool = true;

    fn validate_linked_field(&mut self, field: &LinkedField) -> DiagnosticsResult<()> {
        self.validate_field(&field.definition, &field.arguments);
        self.default_validate_linked_field(field)
    }

    fn validate_scalar_field(&mut self, field: &ScalarField) -> DiagnosticsResult<()> {
        self.validate_field(&field.definition, &field.arguments);
        self.default_validate_scalar_field(field)
    }

    fn validate_value(&mut self, value: &Value) -> DiagnosticsResult<()> {
        // TODO: `@deprecated` is allowed on Enum values, so technically we
        // should also be validating when someone uses a deprecated enum value
        // as an argument, but that will require some additional methods on our
        // Schema, and potentially some additional traversal in our validation
        // trait to traverse into potentially deep constant objects/arrays.
        self.default_validate_value(value)
    }

    fn validate_directive(&mut self, directive: &Directive) -> DiagnosticsResult<()> {
        if let Some(directive_definition) = self.schema.get_directive(directive.name.item) {
            for arg in &directive.arguments {
                if let Some(arg_definition) = directive_definition.arguments.named(arg.name.item) {
                    if let Some(deprecation) = arg_definition.deprecated() {
                        self.warnings.push(Diagnostic::hint(
                            ValidationMessage::DeprecatedDirectiveArgument {
                                argument_name: arg.name.item,
                                directive_name: directive.name.item,
                                deprecation_reason: deprecation.reason,
                            },
                            arg.name.location,
                            vec![DiagnosticTag::DEPRECATED],
                        ));
                    }
                }
            }
        }
        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 common::Diagnostic;
use common::DiagnosticsResult;
use common::NamedItem;
use graphql_ir::FragmentDefinition;
use graphql_ir::FragmentDefinitionName;
use graphql_ir::FragmentDefinitionNameMap;
use graphql_ir::FragmentSpread;
use graphql_ir::OperationDefinition;
use graphql_ir::Program;
use graphql_ir::Validator;
use thiserror::Error;

use crate::no_inline::NO_INLINE_DIRECTIVE_NAME;

pub fn disallow_circular_no_inline_fragments(program: &Program) -> DiagnosticsResult<()> {
    let mut validator = DisallowCircularNoInlineFragments::new(program);
    validator.validate_program(program)
}
enum FragmentStatus {
    Visiting,
    Visited,
}

struct DisallowCircularNoInlineFragments<'program> {
    program: &'program Program,
    fragments: FragmentDefinitionNameMap<FragmentStatus>,
}

impl<'program> DisallowCircularNoInlineFragments<'program> {
    fn new(program: &'program Program) -> Self {
        Self {
            program,
            fragments: Default::default(),
        }
    }
}

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

    fn validate_operation(&mut self, _: &OperationDefinition) -> DiagnosticsResult<()> {
        Ok(())
    }

    fn validate_fragment(&mut self, fragment: &FragmentDefinition) -> DiagnosticsResult<()> {
        match self.fragments.get(&fragment.name.item) {
            None => {
                self.fragments
                    .insert(fragment.name.item, FragmentStatus::Visiting);
                let result = self.validate_selections(&fragment.selections);
                self.fragments
                    .insert(fragment.name.item, FragmentStatus::Visited);
                result
            }
            Some(FragmentStatus::Visited) => Ok(()),
            Some(FragmentStatus::Visiting) => {
                if fragment
                    .directives
                    .named(*NO_INLINE_DIRECTIVE_NAME)
                    .is_some()
                {
                    Err(vec![Diagnostic::error(
                        ValidationMessage::CircularFragmentReference {
                            fragment_name: fragment.name.item,
                        },
                        fragment.name.location,
                    )])
                } else {
                    Ok(())
                }
            }
        }
    }

    fn validate_fragment_spread(&mut self, spread: &FragmentSpread) -> DiagnosticsResult<()> {
        let fragment = self.program.fragment(spread.fragment.item).unwrap();
        self.validate_fragment(fragment)
    }
}

#[derive(Debug, Error)]
enum ValidationMessage {
    #[error("Found a circular reference from fragment '{fragment_name}'.")]
    CircularFragmentReference {
        fragment_name: FragmentDefinitionName,
    },
}


/*
 * 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::Diagnostic;
use common::DiagnosticsResult;
use common::ScalarName;
use graphql_ir::Program;
use graphql_ir::ValidationMessage;
use intern::string_key::Intern;
use intern::string_key::StringKey;
use lazy_static::lazy_static;
use relay_config::NonNodeIdFieldsConfig;
use relay_config::SchemaConfig;
use schema::Field;
use schema::Interface;
use schema::Object;
use schema::Schema;
use schema::Type;

lazy_static! {
    static ref DEFAULT_CONFIG: NonNodeIdFieldsConfig = NonNodeIdFieldsConfig::default();
    static ref NON_STRING_SCALARS: [ScalarName; 3] = [
        ScalarName("Int".intern()),
        ScalarName("Float".intern()),
        ScalarName("Boolean".intern())
    ];
}

pub fn disallow_non_node_id_fields(
    program: &Program,
    schema_config: &SchemaConfig,
) -> DiagnosticsResult<()> {
    validate_all_id_fields(
        program,
        build_type_allowlist(schema_config),
        schema_config.node_interface_id_field,
    )
}

/// Creates a set of allowed types to bypass validation (empty if none are specified)
fn build_type_allowlist(schema_config: &SchemaConfig) -> &HashMap<StringKey, StringKey> {
    &schema_config
        .non_node_id_fields
        .as_ref()
        .unwrap_or(&DEFAULT_CONFIG)
        .allowed_id_types
}

/// Types (e.g. scalars, enums, objects) that fields can be selected from
#[derive(Debug)]
enum FieldSelectableType<'a> {
    Interface(&'a Interface),
    Object(&'a Object),
}

impl<'a> FieldSelectableType<'a> {
    fn name(&'a self) -> StringKey {
        match *self {
            Self::Interface(i) => i.name.item.0,
            Self::Object(o) => o.name.item.0,
        }
    }
}

/// Creates a FieldSelectableType from some schema Type
fn field_selectable_type_from_type(
    program: &Program,
    type_: Type,
) -> Option<FieldSelectableType<'_>> {
    match type_ {
        Type::Interface(id) => Some(FieldSelectableType::Interface(program.schema.interface(id))),
        Type::Object(id) => Some(FieldSelectableType::Object(program.schema.object(id))),
        _ => None,
    }
}

/// Checks that all `id` fields in the schema have types found in `allowed_types`,
/// or nullable versions of those types.
fn validate_all_id_fields(
    program: &Program,
    allowed_id_types: &HashMap<StringKey, StringKey>,
    node_interface_id_field: StringKey,
) -> DiagnosticsResult<()> {
    // Checks if the field is `Node.id`
    let is_named_id_field = |f: &&Field| -> bool { f.name.item == node_interface_id_field };

    // Checks if the type of `id_field` is `ID`, which follows the definition in the `Node` interface
    let is_allowed_node_id_type =
        |parent_type: &'_ FieldSelectableType<'_>, id_field: &Field| -> bool {
            let field_type = &id_field.type_;
            let field_type_name = &program.schema.get_type_name(field_type.inner());
            let parent_type_name = &parent_type.name();

            allowed_id_types.get_key_value(parent_type_name)
                == Some((parent_type_name, field_type_name))
                || (!field_type.is_list() && program.schema.is_id(field_type.inner()))
        };

    // Checks if the type of `id_field` is legal in Relay's runtime (i.e. can only be String-like types)
    let is_runtime_legal_id_type = |id_field: &Field| -> bool {
        let field_type = id_field.type_.inner();
        let is_custom_scalar = || {
            id_field
                .type_
                .inner()
                .get_scalar_id()
                .map_or(false, |scalar_id| {
                    !NON_STRING_SCALARS.contains(&program.schema.scalar(scalar_id).name.item)
                })
        };

        program.schema.is_id(field_type)
            || program.schema.is_string(field_type)
            || field_type.is_enum()
            || is_custom_scalar()
    };

    let errors: Vec<Diagnostic> = program
        .schema
        .fields()
        .filter(is_named_id_field)
        .filter_map(|field| {
            field.parent_type.and_then(|parent_type| {
                field_selectable_type_from_type(program, parent_type)
                    .map(|allowed_parent_type| (allowed_parent_type, field))
            })
        })
        .filter_map(|(parent_type, id_field)| {
            if !is_runtime_legal_id_type(id_field) {
                Some(Diagnostic::error(
                    ValidationMessage::InvalidIdFieldType {
                        parent_type_name: parent_type.name(),
                        id_field_name: node_interface_id_field,
                        id_type_string: program.schema.get_type_string(&id_field.type_),
                    },
                    id_field.name.location,
                ))
            } else if !is_allowed_node_id_type(&parent_type, id_field) {
                Some(Diagnostic::error(
                    ValidationMessage::DisallowNonNodeIdFieldType {
                        parent_type_name: parent_type.name(),
                        id_field_name: node_interface_id_field,
                        id_type_string: program.schema.get_type_string(&id_field.type_),
                    },
                    id_field.name.location,
                ))
            } else {
                None
            }
        })
        .collect();

    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 common::Diagnostic;
use common::DiagnosticsResult;
use common::WithLocation;
use errors::validate;
use graphql_ir::LinkedField;
use graphql_ir::Program;
use graphql_ir::ScalarField;
use graphql_ir::ValidationMessage;
use graphql_ir::Validator;
use intern::string_key::Intern;
use intern::string_key::StringKey;
use relay_config::SchemaConfig;
use schema::FieldID;
use schema::SDLSchema;
use schema::Schema;

pub fn disallow_reserved_aliases(
    program: &Program,
    schema_config: &SchemaConfig,
) -> DiagnosticsResult<()> {
    let mut validator = DisallowReservedAliases::new(program, schema_config);
    validator.validate_program(program)
}

struct DisallowReservedAliases<'program> {
    program: &'program Program,
    reserved_aliases: Vec<StringKey>,
}

impl<'program> DisallowReservedAliases<'program> {
    fn new(program: &'program Program, schema_config: &SchemaConfig) -> Self {
        Self {
            program,
            reserved_aliases: vec![
                schema_config.node_interface_id_field,
                "__typename".intern(),
                "__id".intern(),
            ],
        }
    }
}

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

    fn validate_linked_field(&mut self, field: &LinkedField) -> DiagnosticsResult<()> {
        validate!(
            if let Some(alias) = field.alias {
                validate_field_alias(
                    &self.program.schema,
                    &self.reserved_aliases,
                    &alias,
                    field.definition.item,
                )
            } else {
                Ok(())
            },
            self.validate_selections(&field.selections)
        )
    }

    fn validate_scalar_field(&mut self, field: &ScalarField) -> DiagnosticsResult<()> {
        if let Some(alias) = field.alias {
            validate_field_alias(
                &self.program.schema,
                &self.reserved_aliases,
                &alias,
                field.definition.item,
            )
        } else {
            Ok(())
        }
    }
}

fn validate_field_alias(
    schema: &SDLSchema,
    reserved_aliases: &[StringKey],
    alias: &WithLocation<StringKey>,
    field: FieldID,
) -> DiagnosticsResult<()> {
    let mut validation_errors = vec![];
    for reserved_alias in reserved_aliases {
        let result = validate_field_alias_once(schema, *reserved_alias, alias, field);
        if let Err(errors) = result {
            for err in errors {
                validation_errors.push(err);
            }
        }
    }
    if validation_errors.is_empty() {
        Ok(())
    } else {
        Err(validation_errors)
    }
}

fn validate_field_alias_once(
    schema: &SDLSchema,
    reserved_alias: StringKey,
    alias: &WithLocation<StringKey>,
    field: FieldID,
) -> DiagnosticsResult<()> {
    if alias.item == reserved_alias && schema.field(field).name.item != reserved_alias {
        Err(vec![Diagnostic::error(
            ValidationMessage::DisallowReservedAliasError(reserved_alias),
            alias.location,
        )])
    } 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 common::Diagnostic;
use common::DiagnosticsResult;
use graphql_ir::FragmentDefinition;
use graphql_ir::OperationDefinition;
use graphql_ir::Program;
use graphql_ir::Selection;
use graphql_ir::ValidationMessage;
use graphql_ir::Validator;
use schema::Schema;

pub fn disallow_typename_on_root(program: &Program) -> DiagnosticsResult<()> {
    let mut validator = DisallowTypenameOnRoot::new(program);
    validator.validate_program(program)
}

struct DisallowTypenameOnRoot<'program> {
    program: &'program Program,
}

impl<'program> DisallowTypenameOnRoot<'program> {
    fn new(program: &'program Program) -> Self {
        Self { program }
    }

    fn validate_query_selections(&mut self, selections: &[Selection]) -> DiagnosticsResult<()> {
        let typename_selection = selections.iter().find_map(|sel| {
            if let Selection::ScalarField(field) = sel {
                if field.definition.item == self.program.schema.typename_field() {
                    Some(field)
                } else {
                    None
                }
            } else {
                None
            }
        });
        if let Some(typename_selection) = typename_selection {
            Err(vec![Diagnostic::error(
                ValidationMessage::DisallowTypenameOnRoot,
                typename_selection.definition.location,
            )])
        } else {
            Ok(())
        }
    }
}

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

    fn validate_operation(&mut self, operation: &OperationDefinition) -> DiagnosticsResult<()> {
        self.validate_query_selections(&operation.selections)
    }

    fn validate_fragment(&mut self, fragment: &FragmentDefinition) -> DiagnosticsResult<()> {
        if self.program.schema.query_type() == Some(fragment.type_condition) {
            self.validate_query_selections(&fragment.selections)?
        }
        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.
 */

mod deprecated_fields;
mod disallow_circular_no_inline_fragments;
mod disallow_non_node_id_fields;
mod disallow_reserved_aliases;
mod disallow_typename_on_root;
mod validate_connections;
mod validate_global_variable_names;
mod validate_global_variables;
mod validate_module_names;
mod validate_no_double_underscore_alias;
mod validate_no_inline_with_raw_response_type;
mod validate_no_unselectable_selections;
mod validate_relay_directives;
mod validate_required_arguments;
mod validate_resolver_fragments;
mod validate_server_only_directives;
mod validate_static_args;
mod validate_unused_fragment_variables;
mod validate_unused_variables;

pub use deprecated_fields::deprecated_fields;
pub use deprecated_fields::deprecated_fields_for_executable_definition;
pub use disallow_circular_no_inline_fragments::disallow_circular_no_inline_fragments;
pub use disallow_non_node_id_fields::disallow_non_node_id_fields;
pub use disallow_reserved_aliases::disallow_reserved_aliases;
pub use disallow_typename_on_root::disallow_typename_on_root;
pub use validate_connections::validate_connections;
pub use validate_global_variable_names::validate_global_variable_names;
pub use validate_global_variables::validate_global_variables;
pub use validate_module_names::extract_module_name;
pub use validate_module_names::validate_module_names;
pub use validate_no_double_underscore_alias::validate_no_double_underscore_alias;
pub use validate_no_inline_with_raw_response_type::validate_no_inline_fragments_with_raw_response_type;
pub use validate_no_unselectable_selections::validate_no_unselectable_selections;
pub use validate_relay_directives::validate_relay_directives;
pub use validate_required_arguments::validate_required_arguments;
pub use validate_resolver_fragments::validate_resolver_fragments;
pub use validate_server_only_directives::validate_server_only_directives;
pub use validate_static_args::validate_static_args;
pub use validate_unused_fragment_variables::validate_unused_fragment_variables;
pub use validate_unused_variables::validate_unused_variables;