/*
 * 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::sync::try_join;
use common::DiagnosticsResult;
use common::PerfLogEvent;
use common::PerfLogger;
use graphql_ir::FragmentDefinitionNameSet;
use graphql_ir::Program;
use relay_config::ProjectConfig;

use super::*;
use crate::apply_custom_transforms::apply_after_custom_transforms;
use crate::apply_custom_transforms::apply_before_custom_transforms;
use crate::apply_custom_transforms::CustomTransformsConfig;
use crate::assignable_fragment_spread::annotate_updatable_fragment_spreads;
use crate::assignable_fragment_spread::replace_updatable_fragment_spreads;
use crate::client_extensions_abstract_types::client_extensions_abstract_types;
use crate::disallow_non_node_id_fields;
use crate::generate_relay_resolvers_model_fragments::generate_relay_resolvers_model_fragments;
use crate::generate_relay_resolvers_operations_for_nested_objects::generate_relay_resolvers_operations_for_nested_objects;
use crate::match_::hash_supported_argument;
use crate::skip_updatable_queries::skip_updatable_queries;

#[derive(Debug)]
pub struct Programs {
    pub source: Arc<Program>,
    pub reader: Arc<Program>,
    pub normalization: Arc<Program>,
    pub operation_text: Arc<Program>,
    pub typegen: Arc<Program>,
}

pub fn apply_transforms<TPerfLogger>(
    project_config: &ProjectConfig,
    program: Arc<Program>,
    base_fragment_names: Arc<FragmentDefinitionNameSet>,
    perf_logger: Arc<TPerfLogger>,
    print_stats: Option<fn(extra_info: &str, program: &Program) -> ()>,
    custom_transforms_config: Option<&CustomTransformsConfig>,
) -> DiagnosticsResult<Programs>
where
    TPerfLogger: PerfLogger + 'static,
{
    // The execution pipeline is as follows, where items at the same indentation
    // can be computed independently and therefore in parallel:
    // |- common
    //    |- reader
    //    |- operation
    //       |- normalization
    //       |- operation_text
    // |- typegen
    //
    // NOTE: try_join(f1, f2) prefers the errors from f1 over f2, so process the normalization
    // program first since it is likely to include more errors (both per-fragment and
    // whole-operation errors) than the reader_program which just includes per-fragment errors.
    let (((normalization_program, text_program), reader_program), typegen_program) = try_join(
        || {
            let common_program = apply_common_transforms(
                project_config,
                Arc::clone(&program),
                Arc::clone(&base_fragment_names),
                Arc::clone(&perf_logger),
                custom_transforms_config,
            )?;

            try_join(
                || {
                    let operation_program = apply_operation_transforms(
                        project_config,
                        Arc::clone(&common_program),
                        Arc::clone(&base_fragment_names),
                        Arc::clone(&perf_logger),
                        custom_transforms_config,
                    )?;

                    try_join(
                        || {
                            apply_normalization_transforms(
                                project_config,
                                Arc::clone(&operation_program),
                                Arc::clone(&base_fragment_names),
                                Arc::clone(&perf_logger),
                                print_stats,
                                custom_transforms_config,
                            )
                        },
                        || {
                            apply_operation_text_transforms(
                                project_config,
                                Arc::clone(&operation_program),
                                Arc::clone(&base_fragment_names),
                                Arc::clone(&perf_logger),
                                custom_transforms_config,
                            )
                        },
                    )
                },
                || {
                    apply_reader_transforms(
                        project_config,
                        Arc::clone(&common_program),
                        Arc::clone(&base_fragment_names),
                        Arc::clone(&perf_logger),
                        custom_transforms_config,
                    )
                },
            )
        },
        || {
            apply_typegen_transforms(
                project_config,
                Arc::clone(&program),
                Arc::clone(&base_fragment_names),
                Arc::clone(&perf_logger),
                custom_transforms_config,
            )
        },
    )?;

    Ok(Programs {
        source: program,
        reader: reader_program,
        normalization: normalization_program,
        operation_text: text_program,
        typegen: typegen_program,
    })
}

/// Applies transforms that apply to every output.
fn apply_common_transforms(
    project_config: &ProjectConfig,
    program: Arc<Program>,
    base_fragment_names: Arc<FragmentDefinitionNameSet>,
    perf_logger: Arc<impl PerfLogger>,
    custom_transforms_config: Option<&CustomTransformsConfig>,
) -> DiagnosticsResult<Arc<Program>> {
    let log_event = perf_logger.create_event("apply_common_transforms");
    log_event.string("project", project_config.name.to_string());

    let custom_transforms = &custom_transforms_config.and_then(|c| c.common_transforms.as_ref());
    let mut program = apply_before_custom_transforms(
        &program,
        custom_transforms,
        project_config,
        &base_fragment_names,
        &log_event,
        None,
    )?;

    program = log_event.time("transform_connections", || {
        transform_connections(&program, &project_config.schema_config.connection_interface)
    });
    program = log_event.time("mask", || mask(&program));
    program = log_event.time("transform_defer_stream", || {
        transform_defer_stream(&program)
    })?;
    program = log_event.time("transform_match", || {
        transform_match(
            &program,
            &project_config.feature_flags,
            project_config.module_import_config,
        )
    })?;
    program = log_event.time("transform_subscriptions", || {
        transform_subscriptions(&program)
    })?;
    program = log_event.time("transform_refetchable_fragment", || {
        transform_refetchable_fragment(
            &program,
            &project_config.schema_config,
            &base_fragment_names,
            false,
        )
    })?;

    program = log_event.time("relay_actor_change_transform", || {
        relay_actor_change_transform(&program, &project_config.feature_flags.actor_change_support)
    })?;

    program = log_event.time("provided_variable_fragment_transform", || {
        provided_variable_fragment_transform(&program)
    })?;

    program = log_event.time("generate_relay_resolvers_model_fragments", || {
        generate_relay_resolvers_model_fragments(&program, &project_config.schema_config)
    });

    program = log_event.time(
        "generate_relay_resolvers_operations_for_nested_objects",
        || {
            generate_relay_resolvers_operations_for_nested_objects(
                &program,
                &project_config.schema_config,
            )
        },
    )?;

    program = apply_after_custom_transforms(
        &program,
        custom_transforms,
        project_config,
        &base_fragment_names,
        &log_event,
        None,
    )?;

    log_event.complete();

    Ok(Arc::new(program))
}

/// Applies transforms only for generated reader code.
/// Corresponds to the "fragment transforms" in the JS compiler.
fn apply_reader_transforms(
    project_config: &ProjectConfig,
    program: Arc<Program>,
    base_fragment_names: Arc<FragmentDefinitionNameSet>,
    perf_logger: Arc<impl PerfLogger>,
    custom_transforms_config: Option<&CustomTransformsConfig>,
) -> DiagnosticsResult<Arc<Program>> {
    let log_event = perf_logger.create_event("apply_reader_transforms");
    log_event.string("project", project_config.name.to_string());

    let custom_transforms = &custom_transforms_config.and_then(|c| c.reader_transforms.as_ref());
    let mut program = apply_before_custom_transforms(
        &program,
        custom_transforms,
        project_config,
        &base_fragment_names,
        &log_event,
        None,
    )?;

    program = log_event.time("fragment_alias_directive", || {
        fragment_alias_directive(
            &program,
            &project_config.feature_flags.enable_fragment_aliases,
        )
    })?;

    program = log_event.time("required_directive", || required_directive(&program))?;
    program = log_event.time("client_edges", || {
        client_edges(&program, &project_config.schema_config)
    })?;

    program = log_event.time("relay_resolvers", || {
        relay_resolvers(
            &program,
            project_config.feature_flags.enable_relay_resolver_transform,
        )
    })?;

    program = log_event.time("client_extensions", || client_extensions(&program));
    program = log_event.time("handle_field_transform", || {
        handle_field_transform(&program)
    });

    program = log_event.time(
        "transform_assignable_fragment_spreads_in_regular_queries",
        || transform_assignable_fragment_spreads_in_regular_queries(&program),
    )?;

    program = log_event.time("inline_data_fragment", || inline_data_fragment(&program))?;
    program = log_event.time("skip_unreachable_node", || {
        skip_unreachable_node_strict(&program)
    })?;
    program = log_event.time("remove_base_fragments", || {
        remove_base_fragments(&program, &base_fragment_names)
    });

    log_event.time("flatten", || flatten(&mut program, true, false))?;
    program = log_event.time("skip_redundant_nodes", || skip_redundant_nodes(&program));
    program = log_event.time("generate_data_driven_dependency_metadata", || {
        generate_data_driven_dependency_metadata(&program)
    });
    program = log_event.time("hash_supported_argument", || {
        hash_supported_argument(&program)
    })?;

    program = apply_after_custom_transforms(
        &program,
        custom_transforms,
        project_config,
        &base_fragment_names,
        &log_event,
        None,
    )?;

    log_event.complete();

    Ok(Arc::new(program))
}

/// Applies transforms that apply to all operation artifacts.
/// Corresponds to the "query transforms" in the JS compiler.
fn apply_operation_transforms(
    project_config: &ProjectConfig,
    program: Arc<Program>,
    base_fragment_names: Arc<FragmentDefinitionNameSet>,
    perf_logger: Arc<impl PerfLogger>,
    custom_transforms_config: Option<&CustomTransformsConfig>,
) -> DiagnosticsResult<Arc<Program>> {
    let log_event = perf_logger.create_event("apply_operation_transforms");
    log_event.string("project", project_config.name.to_string());

    let custom_transforms = &custom_transforms_config.and_then(|c| c.operation_transforms.as_ref());
    let mut program = apply_before_custom_transforms(
        &program,
        custom_transforms,
        project_config,
        &base_fragment_names,
        &log_event,
        None,
    )?;

    program = log_event.time("skip_updatable_queries", || {
        skip_updatable_queries(&program)
    });

    program = log_event.time("client_edges", || {
        client_edges(&program, &project_config.schema_config)
    })?;
    program = log_event.time("relay_resolvers", || {
        relay_resolvers(
            &program,
            project_config.feature_flags.enable_relay_resolver_transform,
        )
    })?;

    program = log_event.time("split_module_import", || {
        split_module_import(&program, &base_fragment_names)
    });
    program = log_event.time("generate_id_field", || {
        generate_id_field(&program, &project_config.schema_config)
    });
    program = log_event.time("declarative_connection", || {
        transform_declarative_connection(
            &program,
            &project_config.schema_config.connection_interface,
        )
    })?;

    program = log_event.time("generate_live_query_metadata", || {
        generate_live_query_metadata(&program)
    })?;

    if project_config.schema_config.non_node_id_fields.is_some() {
        log_event.time("disallow_non_node_id_fields", || {
            disallow_non_node_id_fields(&program, &project_config.schema_config)
        })?;
    }

    program = apply_after_custom_transforms(
        &program,
        custom_transforms,
        project_config,
        &base_fragment_names,
        &log_event,
        None,
    )?;

    log_event.complete();

    Ok(Arc::new(program))
}

/// After the operation transforms, this applies further transforms that only
/// apply to the generated normalization code.
///
/// Corresponds to the "codegen transforms" in the JS compiler
fn apply_normalization_transforms(
    project_config: &ProjectConfig,
    program: Arc<Program>,
    base_fragment_names: Arc<FragmentDefinitionNameSet>,
    perf_logger: Arc<impl PerfLogger>,
    maybe_print_stats: Option<fn(extra_info: &str, program: &Program) -> ()>,
    custom_transforms_config: Option<&CustomTransformsConfig>,
) -> DiagnosticsResult<Arc<Program>> {
    let log_event = perf_logger.create_event("apply_normalization_transforms");
    log_event.string("project", project_config.name.to_string());
    if let Some(print_stats) = maybe_print_stats {
        print_stats("normalization start", &program);
    }

    let custom_transforms =
        &custom_transforms_config.and_then(|c| c.normalization_transforms.as_ref());
    let mut program = apply_before_custom_transforms(
        &program,
        custom_transforms,
        project_config,
        &base_fragment_names,
        &log_event,
        maybe_print_stats,
    )?;

    program = log_event.time("apply_fragment_arguments", || {
        apply_fragment_arguments(
            &program,
            true,
            &project_config.feature_flags.no_inline,
            &base_fragment_names,
        )
    })?;
    if let Some(print_stats) = maybe_print_stats {
        print_stats("apply_fragment_arguments", &program);
    }

    program = log_event.time("client_extensions_abstract_types", || {
        client_extensions_abstract_types(&program)
    });

    if let Some(print_stats) = maybe_print_stats {
        print_stats("client_extensions_abstract_types", &program);
    }

    program = log_event.time("replace_updatable_fragment_spreads", || {
        replace_updatable_fragment_spreads(&program)
    });

    program = log_event.time("hash_supported_argument", || {
        hash_supported_argument(&program)
    })?;
    if let Some(print_stats) = maybe_print_stats {
        print_stats("hash_supported_argument", &program);
    }

    program = log_event.time("skip_unreachable_node", || {
        skip_unreachable_node_strict(&program)
    })?;
    if let Some(print_stats) = maybe_print_stats {
        print_stats("skip_unreachable_node", &program);
    }

    program = log_event.time("inline_fragments", || inline_fragments(&program));
    if let Some(print_stats) = maybe_print_stats {
        print_stats("inline_fragments", &program);
    }

    program = log_event.time("client_extensions", || client_extensions(&program));
    if let Some(print_stats) = maybe_print_stats {
        print_stats("client_extensions", &program);
    }

    program = log_event.time("generate_typename", || generate_typename(&program, true));
    if let Some(print_stats) = maybe_print_stats {
        print_stats("generate_typename", &program);
    }

    log_event.time("flatten", || flatten(&mut program, true, false))?;
    if let Some(print_stats) = maybe_print_stats {
        print_stats("flatten", &program);
    }

    program = log_event.time("skip_redundant_nodes", || skip_redundant_nodes(&program));
    if let Some(print_stats) = maybe_print_stats {
        print_stats("skip_redundant_nodes", &program);
    }

    program = log_event.time("generate_test_operation_metadata", || {
        generate_test_operation_metadata(&program, &project_config.test_path_regex)
    })?;
    if let Some(print_stats) = maybe_print_stats {
        print_stats("generate_test_operation_metadata", &program);
    }

    program = apply_after_custom_transforms(
        &program,
        custom_transforms,
        project_config,
        &base_fragment_names,
        &log_event,
        maybe_print_stats,
    )?;

    log_event.complete();

    Ok(Arc::new(program))
}

/// After the operation transforms, this applies further transforms that only
/// apply to the printed operation text.
///
/// Corresponds to the "print transforms" in the JS compiler
fn apply_operation_text_transforms(
    project_config: &ProjectConfig,
    program: Arc<Program>,
    base_fragment_names: Arc<FragmentDefinitionNameSet>,
    perf_logger: Arc<impl PerfLogger>,
    custom_transforms_config: Option<&CustomTransformsConfig>,
) -> DiagnosticsResult<Arc<Program>> {
    let log_event = perf_logger.create_event("apply_operation_text_transforms");
    log_event.string("project", project_config.name.to_string());

    let custom_transforms =
        &custom_transforms_config.and_then(|c| c.operation_text_transforms.as_ref());
    let mut program = apply_before_custom_transforms(
        &program,
        custom_transforms,
        project_config,
        &base_fragment_names,
        &log_event,
        None,
    )?;

    program = log_event.time("apply_fragment_arguments", || {
        apply_fragment_arguments(
            &program,
            false,
            &project_config.feature_flags.no_inline,
            &base_fragment_names,
        )
    })?;

    program = log_event.time("remove_client_edge_selections", || {
        remove_client_edge_selections(&program)
    })?;

    log_event.time("validate_global_variables", || {
        validate_global_variables(&program)
    })?;

    program = log_event.time("replace_updatable_fragment_spreads", || {
        replace_updatable_fragment_spreads(&program)
    });

    program = log_event.time("skip_split_operation", || skip_split_operation(&program));
    program = log_event.time("skip_unreachable_node_strict", || {
        skip_unreachable_node_strict(&program)
    })?;
    program = log_event.time("skip_null_arguments_transform", || {
        skip_null_arguments_transform(&program)
    });
    log_event.time("validate_selection_conflict", || {
        graphql_ir_validations::validate_selection_conflict::<RelayLocationAgnosticBehavior>(
            &program, true,
        )
    })?;
    program = log_event.time("skip_client_extensions", || {
        skip_client_extensions(&program)
    });
    program = log_event.time("skip_unreachable_node_loose", || {
        skip_unreachable_node_loose(&program)
    });

    program = log_event.time("generate_typename", || generate_typename(&program, false));
    log_event.time("flatten", || flatten(&mut program, false, true))?;
    program = log_event.time("validate_operation_variables", || {
        validate_operation_variables(&program)
    })?;
    program = log_event.time("skip_client_directives", || {
        skip_client_directives(&program)
    });
    log_event.time("validate_required_arguments", || {
        validate_required_arguments(&program)
    })?;
    program = log_event.time("unwrap_custom_directive_selection", || {
        unwrap_custom_directive_selection(&program)
    });

    program = apply_after_custom_transforms(
        &program,
        custom_transforms,
        project_config,
        &base_fragment_names,
        &log_event,
        None,
    )?;

    log_event.complete();

    Ok(Arc::new(program))
}

fn apply_typegen_transforms(
    project_config: &ProjectConfig,
    program: Arc<Program>,
    base_fragment_names: Arc<FragmentDefinitionNameSet>,
    perf_logger: Arc<impl PerfLogger>,
    custom_transforms_config: Option<&CustomTransformsConfig>,
) -> DiagnosticsResult<Arc<Program>> {
    let log_event = perf_logger.create_event("apply_typegen_transforms");
    log_event.string("project", project_config.name.to_string());

    let custom_transforms = &custom_transforms_config.and_then(|c| c.typegen_transforms.as_ref());
    let mut program = apply_before_custom_transforms(
        &program,
        custom_transforms,
        project_config,
        &base_fragment_names,
        &log_event,
        None,
    )?;

    program = log_event.time("fragment_alias_directive", || {
        fragment_alias_directive(
            &program,
            &project_config.feature_flags.enable_fragment_aliases,
        )
    })?;

    program = log_event.time("mask", || mask(&program));
    program = log_event.time("transform_match", || {
        transform_match(
            &program,
            &project_config.feature_flags,
            project_config.module_import_config,
        )
    })?;
    program = log_event.time("transform_subscriptions", || {
        transform_subscriptions(&program)
    })?;
    program = log_event.time("required_directive", || required_directive(&program))?;
    program = log_event.time("generate_relay_resolvers_model_fragments", || {
        generate_relay_resolvers_model_fragments(&program, &project_config.schema_config)
    });
    program = log_event.time(
        "generate_relay_resolvers_operations_for_nested_objects",
        || {
            generate_relay_resolvers_operations_for_nested_objects(
                &program,
                &project_config.schema_config,
            )
        },
    )?;

    program = log_event.time("client_edges", || {
        client_edges(&program, &project_config.schema_config)
    })?;

    program = log_event.time(
        "transform_assignable_fragment_spreads_in_regular_queries",
        || transform_assignable_fragment_spreads_in_regular_queries(&program),
    )?;
    program = log_event.time(
        "transform_assignable_fragment_spreads_in_updatable_queries",
        || transform_assignable_fragment_spreads_in_updatable_queries(&program),
    );
    program = log_event.time("annotate_updatable_fragment_spreads", || {
        annotate_updatable_fragment_spreads(&program)
    });

    program = log_event.time("relay_resolvers", || {
        relay_resolvers(
            &program,
            project_config.feature_flags.enable_relay_resolver_transform,
        )
    })?;
    log_event.time("flatten", || flatten(&mut program, false, false))?;
    program = log_event.time("transform_refetchable_fragment", || {
        transform_refetchable_fragment(
            &program,
            &project_config.schema_config,
            &base_fragment_names,
            true,
        )
    })?;
    program = log_event.time("remove_base_fragments", || {
        remove_base_fragments(&program, &base_fragment_names)
    });

    program = log_event.time("relay_actor_change_transform", || {
        relay_actor_change_transform(&program, &project_config.feature_flags.actor_change_support)
    })?;

    program = apply_after_custom_transforms(
        &program,
        custom_transforms,
        project_config,
        &base_fragment_names,
        &log_event,
        None,
    )?;

    log_event.complete();

    Ok(Arc::new(program))
}


/*
 * 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::Location;
use common::NamedItem;
use common::ObjectName;
use common::WithLocation;
use docblock_shared::HAS_OUTPUT_TYPE_ARGUMENT_NAME;
use docblock_shared::RELAY_RESOLVER_DIRECTIVE_NAME;
use graphql_ir::associated_data_impl;
use graphql_ir::Argument;
use graphql_ir::ConstantValue;
use graphql_ir::Directive;
use graphql_ir::ExecutableDefinitionName;
use graphql_ir::Field;
use graphql_ir::FragmentDefinition;
use graphql_ir::FragmentDefinitionName;
use graphql_ir::InlineFragment;
use graphql_ir::LinkedField;
use graphql_ir::OperationDefinition;
use graphql_ir::OperationDefinitionName;
use graphql_ir::Program;
use graphql_ir::Selection;
use graphql_ir::Transformed;
use graphql_ir::Transformer;
use graphql_ir::Value;
use graphql_syntax::OperationKind;
use intern::string_key::Intern;
use intern::string_key::StringKey;
use intern::string_key::StringKeyMap;
use intern::Lookup;
use lazy_static::lazy_static;
use relay_config::SchemaConfig;
use schema::DirectiveValue;
use schema::Schema;
use schema::Type;

use super::ValidationMessageWithData;
use crate::refetchable_fragment::RefetchableFragment;
use crate::refetchable_fragment::REFETCHABLE_NAME;
use crate::relay_resolvers::get_bool_argument_is_true;
use crate::RequiredMetadataDirective;
use crate::ValidationMessage;
use crate::REQUIRED_DIRECTIVE_NAME;

lazy_static! {
    // This gets attached to the generated query
    pub static ref QUERY_NAME_ARG: ArgumentName = ArgumentName("queryName".intern());
    pub static ref TYPE_NAME_ARG: StringKey = "typeName".intern();
    pub static ref CLIENT_EDGE_SOURCE_NAME: ArgumentName = ArgumentName("clientEdgeSourceDocument".intern());
    pub static ref CLIENT_EDGE_WATERFALL_DIRECTIVE_NAME: DirectiveName = DirectiveName("waterfall".intern());
}

/// Directive added to inline fragments created by the transform. The inline
/// fragment groups together the client edge's backing field as well as a linked
/// field containing the selections being read off of the link.
///
/// Each instance of the directive within a traversal is assigned a unique id.
/// This is added to prevent future transforms from merging multiple of these inline
/// fragments.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum ClientEdgeMetadataDirective {
    ServerObject {
        query_name: OperationDefinitionName,
        unique_id: u32,
    },
    ClientObject {
        type_name: Option<ObjectName>,
        unique_id: u32,
    },
}
associated_data_impl!(ClientEdgeMetadataDirective);

/// Metadata directive attached to generated queries
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct ClientEdgeGeneratedQueryMetadataDirective {
    pub source_name: WithLocation<ExecutableDefinitionName>,
}
associated_data_impl!(ClientEdgeGeneratedQueryMetadataDirective);

pub struct ClientEdgeMetadata<'a> {
    /// The field which defines the graph relationship (currently always a Resolver)
    pub backing_field: Selection,
    /// Models the client edge field and its selections
    pub linked_field: &'a LinkedField,
    /// Additional metadata about the client edge
    pub metadata_directive: ClientEdgeMetadataDirective,
}

// Client edges consists of two parts:
//
// 1. A backing field which contains the ID defining the graph relationship
// 2. A linked field containing the selections that the user has asked for off
// of that relationship.
//
// In order to ensure both of these elements are present in our IR, and also get
// traversed by subsequent transform steps, we model Client Edges in our IR as
// an inline fragment containing these two children in an implicit order.
//
// This utility method is intended to reduce the number of places that need to
// know about this implicit contract by reading an inline fragment and returning
// structured metadata, if present.
impl<'a> ClientEdgeMetadata<'a> {
    pub fn find(fragment: &'a InlineFragment) -> Option<Self> {
        ClientEdgeMetadataDirective::find(&fragment.directives).map(|metadata_directive| {

            // Double check that some flatten/inline transform is not trying to combine/merge our inline directives together.
            assert!(
                fragment.selections.len() == 2,
                "Expected Client Edge inline fragment to have exactly two selections. This is a bug in the Relay compiler."
            );
            let mut backing_field = fragment
                .selections
                .get(0)
                .expect("Client Edge inline fragments have exactly two selections").clone();

            let backing_field_directives = backing_field.directives().iter().filter(|directive|
                directive.name.item != RequiredMetadataDirective::directive_name()
            ).cloned().collect();
            backing_field.set_directives(backing_field_directives);

            let linked_field = match fragment.selections.get(1) {
                Some(Selection::LinkedField(linked_field)) => linked_field,
                _ => panic!("Client Edge inline fragments have exactly two selections, with the second selection being a linked field.")
            };

            ClientEdgeMetadata {
                metadata_directive: metadata_directive.clone(),
                backing_field,
                linked_field,
            }
        })
    }
}
pub fn client_edges(program: &Program, schema_config: &SchemaConfig) -> DiagnosticsResult<Program> {
    let mut transform = ClientEdgesTransform::new(program, schema_config);
    let mut next_program = transform
        .transform_program(program)
        .replace_or_else(|| program.clone());

    if transform.errors.is_empty() {
        for fragment in transform.new_fragments {
            next_program.insert_fragment(fragment);
        }
        for operation in transform.new_operations {
            next_program.insert_operation(Arc::new(operation));
        }
        Ok(next_program)
    } else {
        Err(transform.errors)
    }
}

struct ClientEdgesTransform<'program, 'sc> {
    path: Vec<&'program str>,
    document_name: Option<WithLocation<ExecutableDefinitionName>>,
    query_names: StringKeyMap<usize>,
    program: &'program Program,
    new_fragments: Vec<Arc<FragmentDefinition>>,
    new_operations: Vec<OperationDefinition>,
    errors: Vec<Diagnostic>,
    schema_config: &'sc SchemaConfig,
    next_key: u32,
}

impl<'program, 'sc> ClientEdgesTransform<'program, 'sc> {
    fn new(program: &'program Program, schema_config: &'sc SchemaConfig) -> Self {
        Self {
            program,
            schema_config,
            path: Default::default(),
            query_names: Default::default(),
            document_name: Default::default(),
            new_fragments: Default::default(),
            new_operations: Default::default(),
            errors: Default::default(),
            next_key: 0,
        }
    }

    fn generate_query_name(&mut self) -> OperationDefinitionName {
        let document_name = self.document_name.expect("We are within a document");
        let name_root = format!(
            "ClientEdgeQuery_{}_{}",
            document_name.item,
            self.path.join("__")
        )
        .intern();

        // Due to duplicate inline fragments, or inline fragments without type
        // conditions, it's possible that multiple fields will have the same
        // path. In this case, we will append incrementing numbers to the end of
        // the query name to ensure uniqueness.
        let num = self
            .query_names
            .entry(name_root)
            .and_modify(|n| *n += 1)
            .or_insert(0);

        match num {
            0 => OperationDefinitionName(name_root),
            n => OperationDefinitionName(format!("{}_{}", name_root, n).intern()),
        }
    }

    fn generate_client_edge_query(
        &mut self,
        generated_query_name: OperationDefinitionName,
        field_type: Type,
        selections: Vec<Selection>,
    ) {
        let document_name = self.document_name.expect("Expect to be within a document");
        let synthetic_fragment_name = WithLocation::new(
            // The artifact for the refetchable fragment and query derived from
            // this fragment will be placed on disk based on this source
            // location. Currently non-Haste environments assume that this
            // fragment and the query derived from it will use the same location
            // source, and thus will be placed in the same `__generated__`
            // directory. Based on this assumption they import the file using `./`.
            document_name.location,
            FragmentDefinitionName(format!("Refetchable{}", generated_query_name).intern()),
        );

        let synthetic_refetchable_fragment = FragmentDefinition {
            name: synthetic_fragment_name,
            variable_definitions: Vec::new(),
            used_global_variables: Vec::new(),
            type_condition: field_type,
            directives: vec![
                // Used to influence where we place this generated file, and
                // the document from which we derive the source hash for the
                // Client Edge generated query's artifact.
                ClientEdgeGeneratedQueryMetadataDirective {
                    source_name: document_name,
                }
                .into(),
            ],
            selections,
        };

        let mut transformer = RefetchableFragment::new(self.program, self.schema_config, false);

        let refetchable_fragment = transformer
            .transform_refetch_fragment_with_refetchable_directive(
                &Arc::new(synthetic_refetchable_fragment),
                &make_refetchable_directive(generated_query_name),
            );

        match refetchable_fragment {
            Err(diagnostics) => {
                self.errors.extend(diagnostics);
            }
            Ok((refetchable_directive, refetchable_root)) => {
                self.new_fragments.push(refetchable_root.fragment);

                let query_type = self.program.schema.query_type().unwrap();

                let mut directives = refetchable_directive.directives;
                directives.push(
                    // Used to influence where we place this generated file, and
                    // the document from which we derive the source hash for the
                    // Client Edge generated query's artifact.
                    ClientEdgeGeneratedQueryMetadataDirective {
                        source_name: document_name,
                    }
                    .into(),
                );
                self.new_operations.push(OperationDefinition {
                    kind: OperationKind::Query,
                    name: WithLocation::new(
                        document_name.location,
                        refetchable_directive.query_name.item,
                    ),
                    type_: query_type,
                    variable_definitions: refetchable_root.variable_definitions,
                    directives,
                    selections: refetchable_root.selections,
                });
            }
        };
    }

    fn transform_linked_field_impl(&mut self, field: &LinkedField) -> Transformed<Selection> {
        let schema = &self.program.schema;
        let field_type = schema.field(field.definition.item);

        // Eventually we will want to enable client edges on non-resolver client
        // schema extensions, but we'll start with limiting them to resolvers.
        let resolver_directive = field_type.directives.named(*RELAY_RESOLVER_DIRECTIVE_NAME);

        let is_client_edge = field_type.is_extension && resolver_directive.is_some();

        let waterfall_directive = field
            .directives()
            .named(*CLIENT_EDGE_WATERFALL_DIRECTIVE_NAME);

        if !is_client_edge {
            // Non-Client-Edge fields do not incur a waterfall, and thus should
            // not be annotated with @waterfall.
            if let Some(directive) = waterfall_directive {
                self.errors.push(Diagnostic::error_with_data(
                    ValidationMessageWithData::RelayResolversUnexpectedWaterfall,
                    directive.name.location,
                ));
            }
            return self.default_transform_linked_field(field);
        }

        let allowed_directive_names = [
            *CLIENT_EDGE_WATERFALL_DIRECTIVE_NAME,
            *REQUIRED_DIRECTIVE_NAME,
            RequiredMetadataDirective::directive_name(),
        ];

        let other_directives = field
            .directives
            .iter()
            .filter(|directive| {
                !allowed_directive_names
                    .iter()
                    .any(|item| directive.name.item == *item)
            })
            .collect::<Vec<_>>();

        for directive in other_directives {
            self.errors.push(Diagnostic::error(
                ValidationMessage::ClientEdgeUnsupportedDirective {
                    directive_name: directive.name.item,
                },
                directive.name.location,
            ));
        }

        let edge_to_type = field_type.type_.inner();

        let is_edge_to_client_object = schema.is_extension_type(edge_to_type);

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

        let metadata_directive = if is_edge_to_client_object {
            // We assume edges to client objects will be resolved on the client
            // and thus not incur a waterfall. This will change in the future
            // for @live Resolvers that can trigger suspense.
            if let Some(directive) = waterfall_directive {
                self.errors.push(Diagnostic::error_with_data(
                    ValidationMessageWithData::RelayResolversUnexpectedWaterfall,
                    directive.name.location,
                ));
            }

            match edge_to_type {
                Type::Interface(_) => {
                    if !has_output_type(resolver_directive) {
                        self.errors.push(Diagnostic::error(
                            ValidationMessage::ClientEdgeToClientInterface,
                            field.alias_or_name_location(),
                        ));
                    }
                    ClientEdgeMetadataDirective::ClientObject {
                        type_name: None,
                        unique_id: self.get_key(),
                    }
                }
                Type::Union(_) => {
                    self.errors.push(Diagnostic::error(
                        ValidationMessage::ClientEdgeToClientUnion,
                        field.alias_or_name_location(),
                    ));
                    return Transformed::Keep;
                }
                Type::Object(object_id) => ClientEdgeMetadataDirective::ClientObject {
                    type_name: Some(schema.object(object_id).name.item),
                    unique_id: self.get_key(),
                },
                _ => {
                    panic!(
                        "Expected a linked field to reference either an Object, Interface, or Union"
                    )
                }
            }
        } else {
            // Client Edges to server objects must be annotated with @waterfall
            if waterfall_directive.is_none() {
                self.errors.push(Diagnostic::error_with_data(
                    ValidationMessageWithData::RelayResolversMissingWaterfall {
                        field_name: field_type.name.item,
                    },
                    field.definition.location,
                ));
            }
            let client_edge_query_name = self.generate_query_name();

            self.generate_client_edge_query(
                client_edge_query_name,
                field_type.type_.inner(),
                new_selections.clone(),
            );
            ClientEdgeMetadataDirective::ServerObject {
                query_name: client_edge_query_name,
                unique_id: self.get_key(),
            }
        };
        let mut inline_fragment_directives: Vec<Directive> = vec![metadata_directive.into()];
        if let Some(required_directive_metadata) = field
            .directives
            .named(RequiredMetadataDirective::directive_name())
            .cloned()
        {
            inline_fragment_directives.push(required_directive_metadata);
        }

        let transformed_field = Arc::new(LinkedField {
            selections: new_selections,
            ..field.clone()
        });

        let inline_fragment = InlineFragment {
            type_condition: None,
            directives: inline_fragment_directives,
            selections: vec![
                Selection::LinkedField(transformed_field.clone()),
                Selection::LinkedField(transformed_field),
            ],
            spread_location: Location::generated(),
        };

        Transformed::Replace(Selection::InlineFragment(Arc::new(inline_fragment)))
    }

    fn get_key(&mut self) -> u32 {
        let key = self.next_key;
        self.next_key += 1;
        key
    }
}

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

    fn transform_fragment(
        &mut self,
        fragment: &FragmentDefinition,
    ) -> Transformed<FragmentDefinition> {
        self.document_name = Some(fragment.name.map(|name| name.into()));
        let new_fragment = self.default_transform_fragment(fragment);
        self.document_name = None;
        new_fragment
    }

    fn transform_operation(
        &mut self,
        operation: &OperationDefinition,
    ) -> Transformed<OperationDefinition> {
        self.document_name = Some(operation.name.map(|name| name.into()));
        let new_operation = self.default_transform_operation(operation);
        self.document_name = None;
        new_operation
    }

    fn transform_inline_fragment(&mut self, fragment: &InlineFragment) -> Transformed<Selection> {
        match fragment
            .type_condition
            .map(|type_| self.program.schema.get_type_name(type_))
        {
            Some(type_name) => {
                self.path.push(type_name.lookup());

                let new_inline_fragment = self.default_transform_inline_fragment(fragment);
                self.path.pop();

                new_inline_fragment
            }
            None => self.default_transform_inline_fragment(fragment),
        }
    }

    fn transform_linked_field(&mut self, field: &LinkedField) -> Transformed<Selection> {
        self.path
            .push(field.alias_or_name(&self.program.schema).lookup());

        let new_linked_field = self.transform_linked_field_impl(field);

        self.path.pop();

        new_linked_field
    }

    fn transform_scalar_field(
        &mut self,
        field: &graphql_ir::ScalarField,
    ) -> Transformed<Selection> {
        if let Some(directive) = field
            .directives()
            .named(*CLIENT_EDGE_WATERFALL_DIRECTIVE_NAME)
        {
            self.errors.push(Diagnostic::error_with_data(
                ValidationMessageWithData::RelayResolversUnexpectedWaterfall,
                directive.name.location,
            ));
        }
        self.default_transform_scalar_field(field)
    }
}

fn make_refetchable_directive(query_name: OperationDefinitionName) -> Directive {
    Directive {
        name: WithLocation::generated(*REFETCHABLE_NAME),
        arguments: vec![Argument {
            name: WithLocation::generated(*QUERY_NAME_ARG),
            value: WithLocation::generated(Value::Constant(ConstantValue::String(query_name.0))),
        }],
        data: None,
    }
}

pub fn remove_client_edge_selections(program: &Program) -> DiagnosticsResult<Program> {
    let mut transform = ClientEdgesCleanupTransform::default();
    let next_program = transform
        .transform_program(program)
        .replace_or_else(|| program.clone());

    Ok(next_program)
}

#[derive(Default)]
struct ClientEdgesCleanupTransform;

impl Transformer for ClientEdgesCleanupTransform {
    const NAME: &'static str = "ClientEdgesCleanupTransform";
    const VISIT_ARGUMENTS: bool = false;
    const VISIT_DIRECTIVES: bool = false;

    fn transform_inline_fragment(&mut self, fragment: &InlineFragment) -> Transformed<Selection> {
        match ClientEdgeMetadata::find(fragment) {
            Some(metadata) => {
                let new_selection = metadata.backing_field;

                Transformed::Replace(
                    self.transform_selection(&new_selection)
                        .unwrap_or_else(|| new_selection.clone()),
                )
            }
            None => self.default_transform_inline_fragment(fragment),
        }
    }
}

// We should restructure the calling code so that this function does not
// accept an option.
fn has_output_type(directive: Option<&DirectiveValue>) -> bool {
    match directive {
        Some(directive) => {
            get_bool_argument_is_true(&directive.arguments, *HAS_OUTPUT_TYPE_ARGUMENT_NAME)
        }
        None => false,
    }
}


/*
 * 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::Location;
use common::PointerAddress;
use common::WithLocation;
use fnv::FnvHashMap;
use graphql_ir::Directive;
use graphql_ir::InlineFragment;
use graphql_ir::LinkedField;
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 lazy_static::lazy_static;
use schema::Schema;

use crate::ClientEdgeMetadata;

/// A transform that group all client selections and generates ... @__clientExtension inline fragments
/// the generated result is used by codegen only to generate `ClientExtension` nodes.
/// We mark client selection as  `Transformed::Delete`, and consume them in `transform_selections`.
pub fn client_extensions(program: &Program) -> Program {
    let mut transform = ClientExtensionsTransform::new(program);
    transform
        .transform_program(program)
        .replace_or_else(|| program.clone())
}

type Seen = FnvHashMap<PointerAddress, Transformed<Selection>>;

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

struct ClientExtensionsTransform<'program> {
    program: &'program Program,
    seen: Seen,
}

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

    fn transform_client_edge(
        &mut self,
        fragment: &InlineFragment,
        metadata: ClientEdgeMetadata<'_>,
    ) -> Transformed<Selection> {
        // Backing field should always be a RelayResolver. We can't wrap a
        // resolver. If its a scalar (no fragment) we don't need to wrap
        // anything because normalizer ignores the field itself.
        // If it's an inline fragment, on a server type, we can traverse in

        // Here we transform each field and then ignore the case that the
        // transform has identified either selection as being a client extension
        // by calling `unwrap_or_else` with the untransformed value. This ensures the
        // selections get deeply transformed, but that neither of the direct
        // fields of the client edge get wrapped with a client extension inline
        // fragment or deleted.
        let backing_field = self
            .transform_selection(&metadata.backing_field)
            .unwrap_or_else(|| metadata.backing_field.clone());

        let selections = self
            .transform_linked_field(metadata.linked_field)
            .unwrap_or_else(|| Selection::LinkedField(Arc::new(metadata.linked_field.clone())));

        Transformed::Replace(Selection::InlineFragment(Arc::new(InlineFragment {
            selections: vec![backing_field, selections],
            ..fragment.clone()
        })))
    }
}

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

    fn transform_selections(
        &mut self,
        selections: &[Selection],
    ) -> TransformedValue<Vec<Selection>> {
        if selections.is_empty() {
            return TransformedValue::Keep;
        }
        let mut client_selections = vec![];
        let mut server_selections = vec![];
        let mut has_changes = false;
        for (index, prev_item) in selections.iter().enumerate() {
            let next_item = self.transform_selection(prev_item);
            match next_item {
                Transformed::Keep => {
                    if has_changes {
                        server_selections.push(prev_item.clone());
                    }
                }
                Transformed::Delete => {
                    if has_changes {
                        client_selections.push(prev_item.clone());
                    } else {
                        debug_assert!(client_selections.capacity() == 0);
                        debug_assert!(server_selections.capacity() == 0);
                        // assume most items won't be skipped and allocate space for all items
                        server_selections.reserve(selections.len());
                        server_selections.extend(selections.iter().take(index).cloned());
                        client_selections.push(prev_item.clone());
                        has_changes = true;
                    }
                }
                Transformed::Replace(next_item) => {
                    if !has_changes {
                        debug_assert!(client_selections.capacity() == 0);
                        debug_assert!(server_selections.capacity() == 0);
                        // assume most items won't be skipped and allocate space for all items
                        server_selections.reserve(selections.len());
                        server_selections.extend(selections.iter().take(index).cloned());
                        has_changes = true;
                    }
                    server_selections.push(next_item);
                }
            }
        }
        if has_changes {
            if !client_selections.is_empty() {
                server_selections.push(Selection::InlineFragment(Arc::new(InlineFragment {
                    type_condition: None,
                    directives: vec![build_client_extension_directive()],
                    selections: client_selections,
                    spread_location: Location::generated(),
                })));
            }
            TransformedValue::Replace(server_selections)
        } else {
            TransformedValue::Keep
        }
    }

    fn transform_selection(&mut self, selection: &Selection) -> Transformed<Selection> {
        let key = PointerAddress::new(selection);
        if let Some(prev) = self.seen.get(&key) {
            prev.clone()
        } else {
            self.seen.insert(key, Transformed::Keep);
            let result = self.default_transform_selection(selection);
            self.seen.insert(key, result.clone());
            result
        }
    }

    fn transform_inline_fragment(&mut self, fragment: &InlineFragment) -> Transformed<Selection> {
        // Client Edges are modeled in the IR as inline fragments. If we
        // traverse into those fragments, and pull its contents out into a
        // separate inline fragment (without this directive) we will have lost
        // the fact that these selections belong to the client edge.
        //
        // Client edges are all explicitly handled for each artifact type, so we
        // don't need to handle them specifically as client schema extensions.
        if let Some(metadata) = ClientEdgeMetadata::find(fragment) {
            return self.transform_client_edge(fragment, metadata);
        }
        if let Some(type_condition) = fragment.type_condition {
            if self.program.schema.is_extension_type(type_condition) {
                return Transformed::Delete;
            }
        }
        self.default_transform_inline_fragment(fragment)
    }

    fn transform_linked_field(&mut self, field: &LinkedField) -> Transformed<Selection> {
        if self
            .program
            .schema
            .field(field.definition.item)
            .is_extension
        {
            Transformed::Delete
        } else {
            self.default_transform_linked_field(field)
        }
    }

    fn transform_scalar_field(&mut self, field: &ScalarField) -> Transformed<Selection> {
        if self
            .program
            .schema
            .field(field.definition.item)
            .is_extension
        {
            Transformed::Delete
        } else {
            Transformed::Keep
        }
    }
}

// TODO(T63388023): Returns a typed directive
fn build_client_extension_directive() -> Directive {
    Directive {
        name: WithLocation::generated(*CLIENT_EXTENSION_DIRECTIVE_NAME),
        arguments: Default::default(),
        data: None,
    }
}


/*
 * 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::hash_map::Entry;
use std::collections::HashMap;
use std::mem;
use std::sync::Arc;

use graphql_ir::associated_data_impl;
use graphql_ir::FragmentDefinition;
use graphql_ir::FragmentDefinitionName;
use graphql_ir::FragmentSpread;
use graphql_ir::InlineFragment;
use graphql_ir::OperationDefinition;
use graphql_ir::Program;
use graphql_ir::Selection;
use graphql_ir::Transformed;
use graphql_ir::Transformer;
use intern::string_key::StringKey;
use intern::string_key::StringKeySet;
use schema::ObjectID;
use schema::Schema;
use schema::Type;

use crate::generate_abstract_type_refinement_key;

/// A transform collects data about which concrete types implement client schema
/// extension abstract types (interfaces and unions).
pub fn client_extensions_abstract_types(program: &Program) -> Program {
    let mut transform = ClientExtensionsAbstactTypesTransform::new(program);
    transform
        .transform_program(program)
        .replace_or_else(|| program.clone())
}

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct AbstractTypeImplements {
    pub name: StringKey,
    pub concrete: Vec<StringKey>,
}

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct ClientExtensionAbstractTypeMetadataDirective {
    pub abstract_types: Vec<AbstractTypeImplements>,
}
associated_data_impl!(ClientExtensionAbstractTypeMetadataDirective);

/// Maps an abstract type (union or interface) to a list of
/// concrete types which implement that interface.
type TypeMap = HashMap<StringKey, StringKeySet>;

#[derive(Clone)]
enum PendingFragment {
    Pending,
    Resolved(TypeMap),
}

struct ClientExtensionsAbstactTypesTransform<'program> {
    program: &'program Program,
    abstract_type_map: TypeMap,
    fragment_type_maps: HashMap<FragmentDefinitionName, PendingFragment>,
}

impl<'program> ClientExtensionsAbstactTypesTransform<'program> {
    fn new(program: &'program Program) -> Self {
        Self {
            program,
            abstract_type_map: Default::default(),
            fragment_type_maps: Default::default(),
        }
    }

    /// Records all nessesary type relationships for a fragment type condition.
    fn handle_type_condition(&mut self, type_condition: Type) {
        match type_condition {
            Type::Interface(interface_id) => {
                let interface = self.program.schema.interface(interface_id);
                if interface.is_extension {
                    self.add_abstract_type(type_condition, &interface.implementing_objects);
                } else {
                    // TODO: We should also record type information about
                    // concrete types defined in the client schema extension
                    // which implement a server interface
                }
            }
            Type::Union(union_id) => {
                let union = self.program.schema.union(union_id);
                if union.is_extension {
                    self.add_abstract_type(type_condition, &union.members)
                } else {
                    // TODO: We should also record type information about
                    // concrete types defined in the client schema extension
                    // which implement a server interface
                }
            }
            Type::Object(_) => {
                // For concerete type conditions, we don't need to record any additional data.
            }
            _ => panic!("Expected type condition to be on an Interface, Object or Union"),
        };
    }

    /// Record that a list of concrete types match an abstract type
    fn add_abstract_type(&mut self, abstract_type: Type, object_ids: &[ObjectID]) {
        let abstract_type_name =
            generate_abstract_type_refinement_key(&self.program.schema, abstract_type);
        let names_iter = object_ids
            .iter()
            .map(|object_id| self.program.schema.object(*object_id).name.item.0);
        match self.abstract_type_map.entry(abstract_type_name) {
            Entry::Occupied(mut occupied) => {
                occupied.get_mut().extend(names_iter);
            }
            Entry::Vacant(vacant) => {
                vacant.insert(names_iter.collect::<StringKeySet>());
            }
        }
    }

    /// Add all type relationships that are referenced within a fragment definition.
    /// Caches on a per-definition basis so each fragment is explored at most once.
    fn traverse_into_fragment_spread(&mut self, fragment_definition: &Arc<FragmentDefinition>) {
        let pending_fragment = match self.fragment_type_maps.get(&fragment_definition.name.item) {
            Some(type_map) => type_map.clone(),
            None => {
                // Set aside the previously discovered types
                let parent_type_map = mem::take(&mut self.abstract_type_map);

                // Mark this fragment as pending so that we can detect cycles and avoid deadlock.
                self.fragment_type_maps
                    .insert(fragment_definition.name.item, PendingFragment::Pending);

                // Collect type information from the fragment definition
                self.default_transform_fragment(fragment_definition);

                // Reset back to the previously seen type information
                let fragment_type_map = mem::replace(&mut self.abstract_type_map, parent_type_map);

                let pending_fragment = PendingFragment::Resolved(fragment_type_map);

                // Cache fragment results
                self.fragment_type_maps
                    .insert(fragment_definition.name.item, pending_fragment.clone());

                // Return the fragment's type map
                pending_fragment
            }
        };

        match pending_fragment {
            PendingFragment::Pending => {
                panic!(
                    "Unexpected fragment cycle. We expect these to be caught earlier in the transform pipeline in apply_fragment_arguments."
                )
            }
            PendingFragment::Resolved(type_map) => {
                // Augment this query's type map with the type information from the fragment.
                for (key, value) in type_map.into_iter() {
                    match self.abstract_type_map.entry(key) {
                        Entry::Occupied(mut occupied) => {
                            occupied.get_mut().extend(value);
                        }
                        Entry::Vacant(vacant) => {
                            vacant.insert(value);
                        }
                    }
                }
            }
        };
    }
}

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

    fn transform_operation(
        &mut self,
        operation: &OperationDefinition,
    ) -> Transformed<OperationDefinition> {
        self.default_transform_operation(operation);
        if self.abstract_type_map.is_empty() {
            Transformed::Keep
        } else {
            // Get this operation's type map, and also reset the state for the next operation.
            let abstract_type_map = mem::take(&mut self.abstract_type_map);

            let mut directives = operation.directives.clone();

            // Convert hashmap/hashset into Vecs in order to be compatible with
            // AssociatedData which must implmenent Hash.
            let mut abstract_types = abstract_type_map
                .into_iter()
                .map(|(name, concrete)| {
                    let mut concrete_vec = concrete.into_iter().collect::<Vec<_>>();
                    // Sort to ensure stable output
                    concrete_vec.sort();
                    AbstractTypeImplements {
                        name,
                        concrete: concrete_vec,
                    }
                })
                .collect::<Vec<_>>();

            // Sort to ensure stable output
            abstract_types.sort_by_key(|a| a.name);

            directives.push(ClientExtensionAbstractTypeMetadataDirective { abstract_types }.into());

            Transformed::Replace(OperationDefinition {
                directives,
                ..operation.clone()
            })
        }
    }

    fn transform_fragment(
        &mut self,
        _fragment: &FragmentDefinition,
    ) -> Transformed<FragmentDefinition> {
        // We don't care about fragment definitions themselves, only ones that
        // we encounter as fragment spreads. We traverse fragments only when we
        // encounter it being spread into an operation.
        Transformed::Keep
    }

    /// Record type information about inline fragments with type conditions, and
    /// traverse into the inline fragment's selections.
    fn transform_inline_fragment(&mut self, fragment: &InlineFragment) -> Transformed<Selection> {
        if let Some(type_condition) = fragment.type_condition {
            self.handle_type_condition(type_condition)
        }
        self.default_transform_inline_fragment(fragment)
    }

    /// Record type information about the type condition of a spread fragment,
    /// as well as any type information needed for the contents of the named fragment.
    fn transform_fragment_spread(&mut self, spread: &FragmentSpread) -> Transformed<Selection> {
        let maybe_fragment_definition = self.program.fragment(spread.fragment.item);

        if let Some(fragment_definition) = maybe_fragment_definition {
            self.handle_type_condition(fragment_definition.type_condition);

            self.traverse_into_fragment_spread(fragment_definition);
        }
        self.default_transform_fragment_spread(spread)
    }
}


/*
 * 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 graphql_ir::Field;
use graphql_ir::FragmentDefinition;
use graphql_ir::LinkedField;
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::string_key::StringKey;
use lazy_static::lazy_static;
use schema::SDLSchema;
use schema::Schema;
use schema::Type;
use schema::TypeWithFields;
use thiserror::Error;

use crate::connections::ConnectionInterface;
use crate::handle_fields::build_handle_field_directive;
use crate::handle_fields::HandleFieldDirectiveValues;

pub fn transform_declarative_connection(
    program: &Program,
    connection_interface: &ConnectionInterface,
) -> DiagnosticsResult<Program> {
    let mut transform = DeclarativeConnectionMutationTransform::new(program, connection_interface);
    let next_program = transform
        .transform_program(program)
        .replace_or_else(|| program.clone());

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

lazy_static! {
    static ref APPEND_EDGE: DirectiveName = DirectiveName("appendEdge".intern());
    static ref APPEND_NODE: DirectiveName = DirectiveName("appendNode".intern());
    static ref CONNECTIONS_ARG_NAME: ArgumentName = ArgumentName("connections".intern());
    static ref DELETE_RECORD: DirectiveName = DirectiveName("deleteRecord".intern());
    static ref DELETE_EDGE: DirectiveName = DirectiveName("deleteEdge".intern());
    static ref PREPEND_EDGE: DirectiveName = DirectiveName("prependEdge".intern());
    static ref PREPEND_NODE: DirectiveName = DirectiveName("prependNode".intern());
    static ref EDGE_TYPENAME_ARG: ArgumentName = ArgumentName("edgeTypeName".intern());
    static ref EMPTY_STRING: StringKey = "".intern();
}

struct DeclarativeConnectionMutationTransform<'a> {
    schema: &'a SDLSchema,
    errors: Vec<Diagnostic>,
    connection_interface: &'a ConnectionInterface,
}

impl<'a> DeclarativeConnectionMutationTransform<'a> {
    fn new(program: &'a Program, connection_interface: &'a ConnectionInterface) -> Self {
        Self {
            schema: &program.schema,
            connection_interface,
            errors: vec![],
        }
    }

    fn has_cursor_and_node_field(&self, type_: &impl TypeWithFields) -> bool {
        let mut has_cursor_field = false;
        let mut has_node_field = false;
        for field_id in type_.fields() {
            let current_field_name = self.schema.field(*field_id).name.item;
            if current_field_name == self.connection_interface.cursor {
                has_cursor_field = true;
            } else if current_field_name == self.connection_interface.node {
                has_node_field = true;
            }
        }
        has_cursor_field && has_node_field
    }
}

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

    fn transform_fragment(&mut self, _: &FragmentDefinition) -> Transformed<FragmentDefinition> {
        Transformed::Keep
    }

    fn transform_scalar_field(&mut self, field: &ScalarField) -> Transformed<Selection> {
        let linked_field_directive = field.directives.iter().find(|directive| {
            directive.name.item == *APPEND_EDGE
                || directive.name.item == *PREPEND_EDGE
                || directive.name.item == *APPEND_NODE
                || directive.name.item == *PREPEND_NODE
        });
        if let Some(linked_field_directive) = linked_field_directive {
            self.errors.push(Diagnostic::error(
                ValidationMessage::ConnectionMutationDirectiveOnScalarField {
                    directive_name: linked_field_directive.name.item,
                    field_name: field.alias_or_name(self.schema),
                },
                field.definition.location,
            ));
        }
        let delete_directive = field.directives.iter().find(|directive| {
            directive.name.item == *DELETE_RECORD || directive.name.item == *DELETE_EDGE
        });
        let field_definition = self.schema.field(field.definition.item);
        match delete_directive {
            None => Transformed::Keep,
            Some(delete_directive) => {
                let is_id = self.schema.is_id(field_definition.type_.inner());
                if !is_id {
                    self.errors.push(Diagnostic::error(
                        ValidationMessage::DeleteRecordDirectiveOnUnsupportedType {
                            directive_name: delete_directive.name.item,
                            field_name: field.alias_or_name(self.schema),
                            current_type: self.schema.get_type_string(&field_definition.type_),
                        },
                        delete_directive.name.location,
                    ));
                    Transformed::Keep
                } else {
                    let connections_arg = delete_directive.arguments.named(*CONNECTIONS_ARG_NAME);
                    let handle_directive =
                        build_handle_field_directive(HandleFieldDirectiveValues {
                            handle: delete_directive.name.item.0,
                            key: *EMPTY_STRING,
                            dynamic_key: None,
                            filters: None,
                            handle_args: connections_arg
                                .map(|connections_arg| vec![connections_arg.clone()]),
                        });
                    let mut next_directives: Vec<_> = field
                        .directives
                        .iter()
                        .filter(|directive| directive.name.item != *DELETE_RECORD)
                        .cloned()
                        .collect();
                    next_directives.push(handle_directive);
                    Transformed::Replace(Selection::ScalarField(Arc::new(ScalarField {
                        directives: next_directives,
                        ..field.clone()
                    })))
                }
            }
        }
    }

    fn transform_linked_field(&mut self, field: &LinkedField) -> Transformed<Selection> {
        let transformed_field = self.default_transform_linked_field(field);
        let delete_directive = field.directives.named(*DELETE_RECORD);
        if let Some(delete_directive) = delete_directive {
            self.errors.push(Diagnostic::error(
                ValidationMessage::DeleteRecordDirectiveOnLinkedField {
                    directive_name: delete_directive.name.item,
                    field_name: field.alias_or_name(self.schema),
                },
                field.definition.location,
            ));
        }
        let edge_directive = field.directives.iter().find(|directive| {
            directive.name.item == *APPEND_EDGE || directive.name.item == *PREPEND_EDGE
        });
        let node_directive = field.directives.iter().find(|directive| {
            directive.name.item == *APPEND_NODE || directive.name.item == *PREPEND_NODE
        });
        match (edge_directive, node_directive) {
            (Some(edge_directive), Some(node_directive)) => {
                self.errors.push(Diagnostic::error(
                    ValidationMessage::ConflictingEdgeAndNodeDirectives {
                        edge_directive_name: edge_directive.name.item,
                        node_directive_name: node_directive.name.item,
                        field_name: field.alias_or_name(self.schema),
                    },
                    edge_directive.name.location,
                ));
                transformed_field
            }
            (None, None) => transformed_field,
            (Some(edge_directive), None) => {
                let connections_arg = edge_directive.arguments.named(*CONNECTIONS_ARG_NAME);
                match connections_arg {
                    None => {
                        self.errors.push(Diagnostic::error(
                            ValidationMessage::ConnectionsArgumentRequired {
                                directive_name: edge_directive.name.item,
                            },
                            edge_directive.name.location,
                        ));
                        transformed_field
                    }
                    Some(connections_arg) => {
                        let field_definition = self.schema.field(field.definition.item);
                        let has_cursor_and_node_field = match field_definition.type_.inner() {
                            Type::Object(id) => {
                                let object = self.schema.object(id);
                                self.has_cursor_and_node_field(object)
                            }
                            Type::Interface(id) => {
                                let interface = self.schema.interface(id);
                                self.has_cursor_and_node_field(interface)
                            }
                            _ => false,
                        };
                        if has_cursor_and_node_field {
                            let handle_directive =
                                build_handle_field_directive(HandleFieldDirectiveValues {
                                    handle: edge_directive.name.item.0,
                                    key: *EMPTY_STRING,
                                    dynamic_key: None,
                                    filters: None,
                                    handle_args: Some(vec![connections_arg.clone()]),
                                });
                            let mut next_field = match transformed_field {
                                Transformed::Replace(Selection::LinkedField(linked_field)) => {
                                    (*linked_field).clone()
                                }
                                Transformed::Keep => field.clone(),
                                _ => panic!(
                                    "DeclarativeConnection got unexpected transform result: `{:?}`.",
                                    transformed_field
                                ),
                            };
                            let index = next_field
                                .directives
                                .iter()
                                .position(|directive| {
                                    directive.name.item == edge_directive.name.item
                                })
                                .expect("Expected the edge directive to exist.");
                            next_field.directives[index] = handle_directive;
                            Transformed::Replace(Selection::LinkedField(Arc::new(next_field)))
                        } else {
                            self.errors.push(Diagnostic::error(
                                ValidationMessage::EdgeDirectiveOnUnsupportedType {
                                    directive_name: edge_directive.name.item,
                                    field_name: field.alias_or_name(self.schema),
                                },
                                edge_directive.name.location,
                            ));
                            Transformed::Keep
                        }
                    }
                }
            }
            (None, Some(node_directive)) => {
                let connections_arg = node_directive.arguments.named(*CONNECTIONS_ARG_NAME);
                match connections_arg {
                    None => {
                        self.errors.push(Diagnostic::error(
                            ValidationMessage::ConnectionsArgumentRequired {
                                directive_name: node_directive.name.item,
                            },
                            node_directive.name.location,
                        ));
                        transformed_field
                    }
                    Some(connections_arg) => {
                        let edge_typename_arg = node_directive.arguments.named(*EDGE_TYPENAME_ARG);
                        if let Some(edge_typename_arg) = edge_typename_arg {
                            let field_definition = self.schema.field(field.definition.item);
                            match field_definition.type_.inner() {
                                Type::Object(_) | Type::Interface(_) | Type::Union(_) => {
                                    let handle_directive =
                                        build_handle_field_directive(HandleFieldDirectiveValues {
                                            handle: node_directive.name.item.0,
                                            key: *EMPTY_STRING,
                                            dynamic_key: None,
                                            filters: None,
                                            handle_args: Some(vec![
                                                connections_arg.clone(),
                                                edge_typename_arg.clone(),
                                            ]),
                                        });
                                    let mut next_field = match transformed_field {
                                        Transformed::Replace(Selection::LinkedField(
                                            linked_field,
                                        )) => (*linked_field).clone(),
                                        Transformed::Keep => field.clone(),
                                        _ => panic!(
                                            "DeclarativeConnection got unexpected transform result: `{:?}`.",
                                            transformed_field
                                        ),
                                    };
                                    let index = next_field
                                        .directives
                                        .iter()
                                        .position(|directive| {
                                            directive.name.item == node_directive.name.item
                                        })
                                        .expect("Expected the edge directive to exist.");
                                    next_field.directives[index] = handle_directive;
                                    Transformed::Replace(Selection::LinkedField(Arc::new(
                                        next_field,
                                    )))
                                }
                                _ => {
                                    self.errors.push(Diagnostic::error(
                                        ValidationMessage::NodeDirectiveOnUnsupportedType {
                                            directive_name: node_directive.name.item,
                                            field_name: field.alias_or_name(self.schema),
                                            current_type: self
                                                .schema
                                                .get_type_string(&field_definition.type_),
                                        },
                                        node_directive.name.location,
                                    ));
                                    Transformed::Keep
                                }
                            }
                        } else {
                            self.errors.push(Diagnostic::error(
                                ValidationMessage::NodeDirectiveMissesRequiredEdgeTypeName {
                                    directive_name: node_directive.name.item,
                                    field_name: field.alias_or_name(self.schema),
                                },
                                node_directive.name.location,
                            ));
                            Transformed::Keep
                        }
                    }
                }
            }
        }
    }
}

#[derive(Debug, Error)]
enum ValidationMessage {
    #[error(
        "Unsupported use of @{directive_name} on field '${field_name}', 'edgeTypeName' argument must be provided."
    )]
    NodeDirectiveMissesRequiredEdgeTypeName {
        directive_name: DirectiveName,
        field_name: StringKey,
    },

    #[error("Invalid use of @{directive_name} on scalar field '{field_name}'.")]
    ConnectionMutationDirectiveOnScalarField {
        directive_name: DirectiveName,
        field_name: StringKey,
    },

    #[error(
        "Invalid use of @{directive_name} on field '{field_name}'. Expected field type 'ID', got '{current_type}'."
    )]
    DeleteRecordDirectiveOnUnsupportedType {
        directive_name: DirectiveName,
        field_name: StringKey,
        current_type: String,
    },

    #[error("Invalid use of @{directive_name} on linked field '{field_name}'.")]
    DeleteRecordDirectiveOnLinkedField {
        directive_name: DirectiveName,
        field_name: StringKey,
    },

    #[error(
        "Invalid use of @{edge_directive_name} and @{node_directive_name} on field '{field_name}' - these directives cannot be used together."
    )]
    ConflictingEdgeAndNodeDirectives {
        edge_directive_name: DirectiveName,
        node_directive_name: DirectiveName,
        field_name: StringKey,
    },

    #[error("Expected the 'connections' argument to be defined on @{directive_name}.")]
    ConnectionsArgumentRequired { directive_name: DirectiveName },

    #[error(
        "Unsupported use of @{directive_name} on field '{field_name}', expected an edge field (a field with 'cursor' and 'node' selection)."
    )]
    EdgeDirectiveOnUnsupportedType {
        directive_name: DirectiveName,
        field_name: StringKey,
    },

    #[error(
        "Unsupported use of @{directive_name} on field '{field_name}'. Expected an object, union or interface, but got '{current_type}'."
    )]
    NodeDirectiveOnUnsupportedType {
        directive_name: DirectiveName,
        field_name: StringKey,
        current_type: String,
    },
}


/*
 * 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::Directive;
use graphql_ir::FragmentSpread;
use graphql_ir::InlineFragment;
use graphql_ir::LinkedField;
use graphql_ir::Selection;

/// A trait for implementating finding if a directive exists in an IR.
/// Returns `true` if the directive is found.
/// We can make it more general, if there are more similar use cases
pub trait DirectiveFinder {
    fn find(&mut self, mut to_visit: Vec<&Selection>) -> bool {
        while let Some(selection) = to_visit.pop() {
            match selection {
                Selection::FragmentSpread(f) => {
                    if self.visit_fragment_spread(f) {
                        return true;
                    }
                }
                Selection::InlineFragment(f) => {
                    if self.visit_inline_fragment(&mut to_visit, f) {
                        return true;
                    }
                }
                Selection::LinkedField(f) => {
                    if self.visit_linked_field(&mut to_visit, f) {
                        return true;
                    }
                }
                Selection::ScalarField(f) => {
                    if self.visit_directives(&f.directives) {
                        return true;
                    }
                }
                Selection::Condition(c) => {
                    to_visit.extend(c.selections.iter());
                }
            }
        }
        false
    }

    fn visit_directive(&self, directive: &Directive) -> bool;

    fn visit_directives(&self, directives: &[Directive]) -> bool {
        directives.iter().any(|d| self.visit_directive(d))
    }

    fn visit_fragment_spread(&mut self, _fragment_spread: &FragmentSpread) -> bool;

    fn visit_inline_fragment<'a>(
        &mut self,
        to_visit: &mut Vec<&'a Selection>,
        fragment: &'a InlineFragment,
    ) -> bool {
        if self.visit_directives(&fragment.directives) {
            return true;
        }
        to_visit.extend(fragment.selections.iter());
        false
    }

    fn visit_linked_field<'a>(
        &mut self,
        to_visit: &mut Vec<&'a Selection>,
        field: &'a LinkedField,
    ) -> bool {
        if self.visit_directives(&field.directives) {
            return true;
        }
        to_visit.extend(field.selections.iter());
        false
    }
}


/*
 * 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::DiagnosticDisplay;
use common::DirectiveName;
use common::InterfaceName;
use common::ObjectName;
use common::WithDiagnosticData;
use graphql_ir::FragmentDefinitionName;
use graphql_ir::VariableName;
use intern::string_key::StringKey;
use thiserror::Error;

#[derive(Clone, Debug, Error, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum ValidationMessage {
    #[error("This fragment spread already has a split normalization file generated.")]
    DuplicateRelayClientComponentSplitOperation,

    #[error(
        "The Relay Resolver backing this field has an `@relay_resolver` directive with an invalid '{key}' argument. Expected a literal string value."
    )]
    InvalidRelayResolverKeyArg { key: ArgumentName },

    #[error(
        "The Relay Resolver backing this field is missing a '{key}' argument in its `@relay_resolver` directive."
    )]
    MissingRelayResolverKeyArg { key: ArgumentName },

    #[error(
        "Unexpected directive on Relay Resolver field. Relay Resolver fields do not currently support directives."
    )]
    RelayResolverUnexpectedDirective,

    #[error(
        "The Relay Resolver backing this field is defined with an invalid `fragment_name`. Could not find a fragment named '{fragment_name}'."
    )]
    InvalidRelayResolverFragmentName {
        fragment_name: FragmentDefinitionName,
    },
    #[error(
        "The usage of global variable `${variable_name}` is not supported in the Relay resolvers fragments. Please, add this variable to the `@argumentDefinitions` of the `{fragment_name}` fragment."
    )]
    UnsupportedGlobalVariablesInResolverFragment {
        variable_name: VariableName,
        fragment_name: FragmentDefinitionName,
    },

    #[error(
        "Field with actor change (@as_actor) directive expected to have only one item in its selection, and it should be a fragment spread."
    )]
    ActorChangeInvalidSelection,

    #[error("Actor change directive (@as_actor) cannot be applied to scalar fields.")]
    ActorChangeCannotUseOnScalarFields,

    #[error(
        "Actor change has limited (experimental) support and is not allowed to use on this fragment spread."
    )]
    ActorChangeIsExperimental,

    #[error("Actor change does not support plural fields, yet.")]
    ActorChangePluralFieldsNotSupported,

    #[error(
        "Unexpected Relay Resolver field. The Relay Resolvers feature flag is not currently enabled for this project."
    )]
    RelayResolversDisabled,

    #[error(
        "The directive '{directive_name}' automatically adds '{actor_change_field}' to the selection of the field '{field_name}'. But the field '{actor_change_field}' does not exist on the type '{type_name}'. Please makes sure the GraphQL schema supports actor change on '{type_name}'."
    )]
    ActorChangeExpectViewerFieldOnType {
        directive_name: DirectiveName,
        actor_change_field: StringKey,
        field_name: StringKey,
        type_name: StringKey,
    },

    #[error(
        "The directive '{directive_name}' automatically adds '{actor_change_field}' to the selection of the field '{field_name}'. The field '{actor_change_field}' should be defined as a scalar field in the GraphQL Schema, but is defined with the type '{actor_change_field_type}' instead."
    )]
    ActorChangeViewerShouldBeScalar {
        directive_name: DirectiveName,
        actor_change_field: StringKey,
        field_name: StringKey,
        actor_change_field_type: StringKey,
    },

    #[error(
        "The '{fragment_name}' is transformed to use @no_inline implictly by `@module`, but it's also used in a regular fragment spread. It's required to explicitly add `@no_inline` to the definition of '{fragment_name}'."
    )]
    RequiredExplicitNoInlineDirective {
        fragment_name: FragmentDefinitionName,
    },

    #[error(
        "The `@relay_test_operation` directive is only allowed within test \
        files because it creates larger generated files we don't want to \
        include in production. File does not match test regex: {test_path_regex}"
    )]
    TestOperationOutsideTestDirectory { test_path_regex: String },

    #[error("Undefined fragment '{0}'")]
    UndefinedFragment(FragmentDefinitionName),

    #[error(
        "Each field on a given type can have only a single @module directive, but here there is more than one (perhaps within different spreads). To fix it, put each @module directive into its own aliased copy of the field with different aliases."
    )]
    ConflictingModuleSelections,

    #[error(
        "Client Edges that reference client-defined interface types are not currently supported in Relay."
    )]
    ClientEdgeToClientInterface,

    #[error(
        "Client Edges that reference client-defined union types are not currently supported in Relay."
    )]
    ClientEdgeToClientUnion,

    #[error("Invalid directive combination. @alias may not be combined with other directives.")]
    FragmentAliasIncompatibleDirective,

    #[error("Unexpected directive @alias. @alias is not currently enabled in this location.")]
    FragmentAliasDirectiveDisabled,

    #[error("Expected the `as` argument of the @alias directive to be a static string.")]
    FragmentAliasDirectiveDynamicNameArg,

    #[error(
        "Missing required argument `as`. The `as` argument of the @alias directive is required on inline fragments without a type condition."
    )]
    FragmentAliasDirectiveMissingAs,

    #[error(
        "Unexpected dynamic argument. {field_name}'s '{argument_name}' argument must be a constant value because it is read by the Relay compiler."
    )]
    InvalidStaticArgument {
        field_name: StringKey,
        argument_name: ArgumentName,
    },

    #[error(
        "Unexpected directive on Client Edge field. The `@{directive_name}` directive is not currently supported on fields backed by Client Edges."
    )]
    ClientEdgeUnsupportedDirective { directive_name: DirectiveName },

    #[error(
        "Invalid @RelayResolver output type for field `{field_name}`. Got input object `{type_name}`."
    )]
    RelayResolverOutputTypeInvalidInputObjectType {
        field_name: StringKey,
        type_name: StringKey,
    },

    #[error(
        "@RelayResolver {type_kind} type `{type_name}` for field `{field_name}` is not supported as @outputType, yet."
    )]
    RelayResolverOutputTypeUnsupported {
        type_kind: StringKey,
        field_name: StringKey,
        type_name: StringKey,
    },

    #[error(
        "No types implement the client interface {interface_name}. For a client interface to be used as a @RelayResolver @outputType, at least one Object type must implement the interface."
    )]
    RelayResolverClientInterfaceMustBeImplemented { interface_name: InterfaceName },

    #[error(
        "The interface {interface_name} is being used as an @outputType of a @RelayResolver. For this to be valid, all Object types that implement the interface must be client types. However, the {object_name}, which implements {interface_name}, is a server type."
    )]
    RelayResolverClientInterfaceImplementingTypeMustBeClientTypes {
        interface_name: InterfaceName,
        object_name: ObjectName,
    },

    #[error(
        "@RelayResolver type recursion detected for the output type `{type_name}`. This is not supported for `@outputType` resolvers. If you want to model a connection between two entities of the same GraphQL type, consider creating a new Relay Resolver with `@edgeTo` annotation."
    )]
    RelayResolverTypeRecursionDetected { type_name: StringKey },

    #[error(
        "Field `{field_name}` has output type `{type_name}`. `{type_name}` is a server type, and server types cannot be used with @outputType on @RelayResolver. Edges to server types can be exposed with @edgeTo and @waterfall."
    )]
    RelayResolverServerTypeNotSupported {
        field_name: StringKey,
        type_name: StringKey,
    },

    #[error(
        "Field name `{id_name}` is reserved for strong objects (objects that implement Node interface). Defining `{id_name}` fields is not currently supported on @RelayResolver with @outputType."
    )]
    RelayResolverIDFieldNotSupported { id_name: StringKey },

    #[error(
        "Arguments are not supported in the fields on the @outputType in @RelayResolvers. You'll need to expose these fields using @RelayResolver for them."
    )]
    RelayResolverArgumentsNotSupported,

    #[error(
        "Disallowed selection of field `{}{field_name}`.{}",
        parent_name.map_or("".to_string(), |name| format!("{}.", name)),
        reason.map_or("".to_string(), |reason| format!(" Reason: \"{}\"", reason)),
    )]
    UnselectableField {
        field_name: StringKey,
        parent_name: Option<StringKey>,
        reason: Option<StringKey>,
    },
}

#[derive(Clone, Debug, Error, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum ValidationMessageWithData {
    #[error(
        "Expected a `@waterfall` directive on this field. Consuming a Client Edge field incurs a network roundtrip or \"waterfall\". To make this explicit, a `@waterfall` directive is required on this field."
    )]
    RelayResolversMissingWaterfall { field_name: StringKey },

    #[error(
        "Unexpected `@waterfall` directive. Only fields that are backed by a Client Edge and point to a server object should be annotated with the `@waterfall` directive."
    )]
    RelayResolversUnexpectedWaterfall,
}

impl WithDiagnosticData for ValidationMessageWithData {
    fn get_data(&self) -> Vec<Box<dyn DiagnosticDisplay>> {
        match self {
            ValidationMessageWithData::RelayResolversMissingWaterfall { field_name } => {
                vec![Box::new(format!("{} @waterfall", field_name,))]
            }
            ValidationMessageWithData::RelayResolversUnexpectedWaterfall => {
                vec![Box::new("")]
            }
        }
    }
}