use graphql_tools::parser::query::{self as query_ast, Definition, OperationDefinition};

pub mod context;
pub mod error;
pub mod pipeline;
pub mod utils;

use crate::ast;
use crate::ast::document::NormalizedDocument;
use crate::ast::normalization::context::{NormalizationContext, RootTypes};
use crate::ast::normalization::pipeline::drop_skipped_fields;
use crate::ast::normalization::pipeline::drop_unused_operations;
use crate::ast::normalization::pipeline::flatten_fragments;
use crate::ast::normalization::pipeline::flatten_matching_object_fragments;
use crate::ast::normalization::pipeline::inline_fragment_spreads;
use crate::ast::normalization::pipeline::merge_fields;
use crate::ast::normalization::pipeline::merge_inline_fragments;
use crate::ast::normalization::pipeline::normalize_fields;
use crate::ast::normalization::pipeline::type_expand;
use crate::ast::normalization::pipeline::{drop_duplicated_fields, drop_fragment_definitions};
use crate::state::supergraph_state::{OperationKind, SupergraphState};
use error::NormalizationError;

/// Normalizes the operation by mutating it
#[inline]
pub fn normalize_operation_mut(
    supergraph: &SupergraphState,
    query: &mut query_ast::Document<'static, String>,
    operation_name: Option<&str>,
    root_types_overwrite: Option<RootTypes>,
    subgraph_name: Option<&String>,
) -> Result<(), NormalizationError> {
    let mut ctx = NormalizationContext {
        supergraph,
        operation_name,
        document: query,
        root_types: root_types_overwrite.unwrap_or_else(|| supergraph.into()),
        subgraph_name,
    };

    drop_unused_operations(&mut ctx)?;
    drop_skipped_fields(&mut ctx)?;
    normalize_fields(&mut ctx)?;
    inline_fragment_spreads(&mut ctx)?;
    drop_fragment_definitions(&mut ctx)?;
    flatten_fragments(&mut ctx)?;
    type_expand(&mut ctx)?;
    merge_inline_fragments(&mut ctx)?;
    flatten_matching_object_fragments(&mut ctx)?;
    merge_fields(&mut ctx)?;
    drop_duplicated_fields(&mut ctx)?;

    Ok(())
}

#[inline]
pub fn normalize_operation(
    supergraph: &SupergraphState,
    query: &query_ast::Document<'static, String>,
    operation_name: Option<&str>,
) -> Result<NormalizedDocument, NormalizationError> {
    let mut query_mut = query.clone();
    normalize_operation_mut(supergraph, &mut query_mut, operation_name, None, None)?;
    let operation = query_mut.definitions.iter_mut().find_map(|def| match def {
        Definition::Operation(op) => Some(op),
        _ => None,
    });

    let op_def = operation.ok_or(NormalizationError::ExpectedTransformedOperationNotFound)?;

    // Use the operation name from the operation definition if available,
    // as a fallback if the user did not provide an operation name.
    let operation_name = operation_name
        .or(match op_def {
            OperationDefinition::Query(query) => query.name.as_deref(),
            OperationDefinition::Mutation(mutation) => mutation.name.as_deref(),
            OperationDefinition::Subscription(subscription) => subscription.name.as_deref(),
            _ => None,
        })
        .map(|n| n.to_string());

    Ok(create_normalized_document(
        supergraph,
        op_def.to_owned(),
        operation_name,
    ))
}

#[inline]
pub fn create_normalized_document<'a>(
    supergraph: &SupergraphState,
    operation_ast: query_ast::OperationDefinition<'a, String>,
    operation_name: Option<String>,
) -> NormalizedDocument {
    let mut operation: ast::operation::OperationDefinition = operation_ast.into();
    inject_skipped_typename_for_empty_composites(&mut operation, supergraph);

    NormalizedDocument {
        operation,
        operation_name,
    }
}

fn inject_skipped_typename_for_empty_composites(
    operation: &mut ast::operation::OperationDefinition,
    supergraph: &SupergraphState,
) {
    let root_type_name = match operation.operation_kind.as_ref() {
        Some(OperationKind::Query) | None => supergraph.query_type.as_str(),
        Some(OperationKind::Mutation) => supergraph
            .mutation_type
            .as_deref()
            .expect("mutation root type should exist"),
        Some(OperationKind::Subscription) => supergraph
            .subscription_type
            .as_deref()
            .expect("subscription root type should exist"),
    };
    inject_skipped_typename_for_selection_set(
        &mut operation.selection_set,
        root_type_name,
        supergraph,
    );
}

fn inject_skipped_typename_for_selection_set(
    selection_set: &mut ast::selection_set::SelectionSet,
    parent_type_name: &str,
    supergraph: &SupergraphState,
) {
    use ast::selection_item::SelectionItem;
    use ast::selection_set::FieldSelection;

    for item in &mut selection_set.items {
        match item {
            SelectionItem::Field(field) => {
                let Some(field_type_name) = supergraph
                    .definitions
                    .get(parent_type_name)
                    .and_then(|definition| definition.fields().get(&field.name))
                    .map(|field_definition| field_definition.field_type.inner_type())
                else {
                    continue;
                };
                let is_composite = supergraph
                    .definitions
                    .get(field_type_name)
                    .is_some_and(|definition| definition.is_composite_type());
                if !is_composite {
                    continue;
                }
                inject_skipped_typename_for_selection_set(
                    &mut field.selections,
                    field_type_name,
                    supergraph,
                );
                if field.selections.items.is_empty() {
                    field
                        .selections
                        .items
                        .push(SelectionItem::Field(FieldSelection::new_skipped_typename()));
                }
            }
            SelectionItem::InlineFragment(fragment) => {
                inject_skipped_typename_for_selection_set(
                    &mut fragment.selections,
                    fragment.type_condition.as_str(),
                    supergraph,
                );
            }
            SelectionItem::FragmentSpread(_) => {
                // Fragment spreads are removed earlier in normalization.
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use graphql_tools::parser::parse_query;

    use crate::ast::normalization::normalize_operation;
    use crate::ast::selection_item::SelectionItem;
    use crate::state::supergraph_state::SupergraphState;
    use crate::utils::parsing::parse_schema;

    fn pretty_query(query_str: String) -> String {
        format!(
            "{}",
            parse_query::<&str>(&query_str).expect(&format!("failed to parse: {}", query_str),)
        )
    }

    // TODO: remove unused variables
    // TODO: maybe we should extract variables and add additional hashmap for variables passed to the execution?

    #[test]
    fn introspection() {
        let schema = parse_schema(
            r#"
            type Query {
              words(len: Int, sep: String): String
            }
          "#,
        );

        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                      query {
                        a: __typename
                        words:words
                        __typename
                        __schema {
                          __typename
                        }
                      }
                "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .unwrap()
                .to_string()
            ),
            @r"
        {
          a: __typename
          words
          __typename
          __schema {
            __typename
          }
        }
        "
        );
    }

    #[test]
    fn empty_composite_under_inline_fragment_gets_skipped_typename_sentinel() {
        let schema = parse_schema(
            r#"
              interface Node {
                id: ID!
              }

              type Profile {
                name: String
              }

              type Account implements Node {
                id: ID!
                profile: Profile
              }

              type Query {
                node: Node
              }
            "#,
        );

        let supergraph = SupergraphState::new(&schema);
        let normalized = normalize_operation(
            &supergraph,
            &parse_query(
                r#"
                  query {
                    node {
                      ... on Account {
                        profile {
                          name @include(if: false)
                        }
                      }
                    }
                  }
                "#,
            )
            .expect("to parse"),
            None,
        )
        .unwrap();

        insta::assert_snapshot!(
            pretty_query(normalized.to_string()),
            @r"
        {
          node {
            ... on Account {
              profile {
                __typename
              }
            }
          }
        }
        "
        );
    }

    #[test]
    fn explicit_typename_wins_over_skipped_typename() {
        let schema = parse_schema(
            r#"
              interface Node {
                id: ID!
              }

              type Profile {
                name: String
              }

              type Account implements Node {
                id: ID!
                profile: Profile
              }

              type Query {
                node: Node
              }
            "#,
        );

        let supergraph = SupergraphState::new(&schema);
        let normalized = normalize_operation(
            &supergraph,
            &parse_query(
                r#"
                  query {
                    node {
                      ... on Account {
                        profile {
                          name @include(if: false)
                          __typename
                        }
                      }
                    }
                  }
                "#,
            )
            .expect("to parse"),
            None,
        )
        .unwrap();

        insta::assert_snapshot!(
            pretty_query(normalized.to_string()),
            @r"
        {
          node {
            ... on Account {
              profile {
                __typename
              }
            }
          }
        }
        "
        );
    }

    #[test]
    fn normalize_fields() {
        let schema = parse_schema(
            r#"
              type Query {
                words(len: Int, sep: String): String
              }
            "#,
        );

        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                    query {
                      words: words(sep: ",")
                      foo: words(sep: ".", len: 10)
                    }
                  "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .unwrap()
                .to_string()
            ),
            @r#"
        {
          words(sep: ",")
          foo: words(len: 10, sep: ".")
        }
        "#
        );
    }

    #[test]
    fn drop_unused_operations() {
        let schema = parse_schema(
            r#"
              type Query {
                words: String
              }
            "#,
        );

        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                    query foo {
                      words
                    }

                    query bar {
                      words
                    }
                  "#,
                    )
                    .expect("to parse"),
                    Some("foo"),
                )
                .unwrap()
                .to_string()
            ),
            @r"
        query foo {
          words
        }
        "
        );
    }

    #[test]
    fn drop_duplicated_fields() {
        let schema = parse_schema(
            r#"
              type Query {
                words(len: Int, sep: String): String
              }
            "#,
        );
        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                    query {
                      words
                      c: words(len: 1)
                      b: words
                      a: words
                      words
                      c: words(len: 1)
                    }
                  "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .unwrap()
                .to_string()
            ),
            @r"
        {
          words
          c: words(len: 1)
          b: words
          a: words
        }
        "
        );
    }

    #[test]
    fn inline_fragment_spreads() {
        let schema = parse_schema(
            r#"
              interface Node
                @join__type(graph: A)
                @join__type(graph: B)
              {
                id: ID!
              }


              interface WithWarranty
                @join__type(graph: A)
                @join__type(graph: B)
              {
                warranty: Int
              }

              type Oven implements Node & WithWarranty
                @join__implements(graph: B, interface: "Node")
                @join__implements(graph: B, interface: "WithWarranty")
                @join__type(graph: A, key: "id")
                @join__type(graph: B, key: "id") {
                id: ID!
                warranty: Int
                o1: String
                o2: String
                o3: String
                toaster: Toaster
              }

              union Product
                @join__type(graph: A)
                @join__unionMember(graph: A, member: "Oven")
                @join__unionMember(graph: A, member: "Toaster")
               = Oven | Toaster

               type Query
                 @join__type(graph: A)
                 @join__type(graph: B)
               {
                 products: [Product] @join__field(graph: A)
                 node(id: ID!): Node @join__field(graph: A)
                 nodes: [Node] @join__field(graph: A)
                 toasters: [Toaster] @join__field(graph: A)
               }

               type Toaster implements Node & WithWarranty
                 @join__implements(graph: A, interface: "Node")
                 @join__implements(graph: A, interface: "WithWarranty")
                 @join__type(graph: A, key: "id")
               {
                id: ID!
                warranty: Int
                t1: String
                t2: String
                t3: String
                oven: Oven
              }
            "#,
        );
        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                        query {
                          products {
                            ...ToasterFragment
                            ... on Toaster {
                              t1
                              t2
                              oven {
                                warranty
                                o1
                                o2
                              }
                            }
                          }
                        }
                        fragment ToasterFragment on Toaster {
                          t1
                          oven {
                            o1
                            ... on Oven {
                              ... on Oven {
                                o3
                              }
                            }
                            ...OvenFragment
                            ...OvenFragment

                          }
                        }
                        fragment OvenFragment on Oven {
                          toaster {
                            warranty
                            t3
                          }
                          o3
                        }
                  "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .unwrap()
                .to_string()
            ),
            @r"
        {
          products {
            ... on Toaster {
              t1
              oven {
                o1
                o3
                toaster {
                  warranty
                  t3
                }
                warranty
                o2
              }
              t2
            }
          }
        }
        ",
        );

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                        query {
                          products {
                            ... on Node {
                              id
                            }
                            ... on WithWarranty {
                              warranty
                            }
                          }
                        }
                  "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .unwrap()
                .to_string()
            ),
            @r"
        {
          products {
            ... on Oven {
              id
              warranty
            }
            ... on Toaster {
              id
              warranty
            }
          }
        }
        ",
        );

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                        query {
                          toasters {
                            ...ToasterFragment
                            ...NodeFragment
                          }
                        }

                        fragment ToasterFragment on Toaster {
                          id
                        }

                        fragment NodeFragment on Node {
                          id
                          __typename
                        }
                  "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .unwrap()
                .to_string()
            ),
            @r"
        {
          toasters {
            id
            ... on Toaster {
              id
              __typename
            }
          }
        }
        ",
        );
    }

    // Makes sure that fields arguments are normalized and sorted correctly, because hashing relies on the order of arguments.
    #[test]
    fn normalize_fields_args() {
        let schema = parse_schema(
            r#"
              type Query {
                words(len: Int, sep: String): String
              }
            "#,
        );

        let supergraph = SupergraphState::new(&schema);
        let r = normalize_operation(
            &supergraph,
            &parse_query(
                r#"
            query {
              one: words(sep: ".", len: 10)
              two: words(len: 10, sep: ".")
            }
          "#,
            )
            .expect("to parse"),
            None,
        )
        .unwrap();

        match (
            &r.operation.selection_set.items[0],
            &r.operation.selection_set.items[1],
        ) {
            (SelectionItem::Field(f1), SelectionItem::Field(f2)) => {
                assert_eq!(f1.arguments_hash(), f2.arguments_hash());
            }
            _ => panic!("Unexpected selection items"),
        }

        insta::assert_snapshot!(
            pretty_query(r.to_string()),
            @r#"
        {
          one: words(len: 10, sep: ".")
          two: words(len: 10, sep: ".")
        }
        "#
        );
    }

    #[test]
    fn mutation() {
        let schema = parse_schema(
            r#"
                  type Mutation {
                    createProduct(name: String!): Product
                  }

                  type Query {
                    products: [Product]
                  }

                  type Product {
                    id: ID!
                    name: String
                  }
                "#,
        );
        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                              mutation NewProduct($name: String!) {
                                createProduct(name: $name) {
                                  id
                                  name
                                }
                              }
                            "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .unwrap()
                .to_string()
            ),
            @r"
            mutation NewProduct($name: String!) {
              createProduct(name: $name) {
                id
                name
              }
            }
            ",
        );
    }

    #[test]
    fn type_expansion() {
        let schema_str = std::fs::read_to_string(
            "./fixture/tests/corrupted-supergraph-node-id.supergraph.graphql",
        )
        .expect("Unable to read supergraph");
        let schema = parse_schema(&schema_str);
        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                        query nodeid($id: ID!) {
                          node(id: $id) {
                            id
                          }
                        }
                        "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .unwrap()
                .to_string()
            ),
            @r"
            query nodeid($id: ID!) {
              node(id: $id) {
                ... on Account {
                  id
                }
                ... on Chat {
                  id
                }
              }
            }
            ",
        );

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                          query nodeid($id: ID!) {
                            node(id: $id) {
                              ... on Chat {
                                id
                              }
                            }
                          }
                        "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .unwrap()
                .to_string()
            ),
            @r"
            query nodeid($id: ID!) {
              node(id: $id) {
                ... on Chat {
                  id
                }
              }
            }
            ",
        );
    }

    #[test]
    fn type_expansion_2() {
        let schema_str =
            std::fs::read_to_string("./fixture/tests/requires-with-fragments.supergraph.graphql")
                .expect("Unable to read supergraph");
        let schema = parse_schema(&schema_str);
        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                          query {
                            userFromA {
                              profile {
                                displayName
                                ... on Account {
                                  accountType
                                }
                              }
                            }
                          }
                        "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .unwrap()
                .to_string()
            ),
            @r"
        {
          userFromA {
            profile {
              displayName
              ... on AdminAccount {
                accountType
              }
              ... on GuestAccount {
                accountType
              }
            }
          }
        }
        ",
        );

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                          query {
                            userFromA {
                              profile {
                                displayName
                                ... on Account {
                                  accountType
                                  ... on AdminAccount {
                                    adminLevel
                                  }
                                  ... on GuestAccount {
                                    guestToken
                                  }
                                }
                              }
                            }
                          }
                        "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .unwrap()
                .to_string()
            ),
            @r"
        {
          userFromA {
            profile {
              displayName
              ... on AdminAccount {
                accountType
                adminLevel
              }
              ... on GuestAccount {
                accountType
                guestToken
              }
            }
          }
        }
        ",
        );
    }

    #[test]
    fn type_expansion_3() {
        let schema_str =
            std::fs::read_to_string("./fixture/tests/abstract-types.supergraph.graphql")
                .expect("Unable to read supergraph");
        let schema = parse_schema(&schema_str);
        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                        {
                          products {
                            id
                            reviews { id }
                          }
                        }
                        "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .unwrap()
                .to_string()
            ),
            @r"
        {
          products {
            id
            ... on Book {
              reviews {
                id
              }
            }
            ... on Magazine {
              reviews {
                id
              }
            }
          }
        }
        ",
        );
    }

    #[test]
    fn type_expansion_4() {
        let schema_str =
            std::fs::read_to_string("./fixture/tests/simple-interface-object.supergraph.graphql")
                .expect("Unable to read supergraph");
        let schema = parse_schema(&schema_str);
        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                        query {
                          anotherUsers {
                            id
                            name
                            username
                          }
                        }
                        "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .unwrap()
                .to_string()
            ),
            @r"
        {
          anotherUsers {
            id
            name
            username
          }
        }
        ",
        );
    }

    #[test]
    fn static_skip_include_test() {
        let schema_str =
            std::fs::read_to_string("./fixture/tests/requires-with-fragments.supergraph.graphql")
                .expect("Unable to read supergraph");
        let schema = parse_schema(&schema_str);
        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                        query {
                          userFromA @include(if: true) {
                            profile {
                              displayName @include(if: false)
                              ... on Account @skip(if: false) {
                                accountType
                              }
                            }
                          }
                        }
                      "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .unwrap()
                .to_string()
            ),
            @r"
        {
          userFromA {
            profile {
              ... on AdminAccount {
                accountType
              }
              ... on GuestAccount {
                accountType
              }
            }
          }
        }
        ",
        );
    }

    #[test]
    fn static_skip_include_test_2() {
        let schema_str =
            std::fs::read_to_string("./fixture/tests/requires-with-fragments.supergraph.graphql")
                .expect("Unable to read supergraph");
        let schema = parse_schema(&schema_str);
        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                        query {
                          userFromA {
                            profile {
                              displayName @include(if: false)
                              ... on Account {
                                accountType
                                ... on AdminAccount @skip(if: true) {
                                  adminLevel
                                }
                                ... on GuestAccount {
                                  guestToken
                                }
                              }
                            }
                          }
                        }
                      "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .unwrap()
                .to_string()
            ),
            @r"
        {
          userFromA {
            profile {
              ... on AdminAccount {
                accountType
              }
              ... on GuestAccount {
                accountType
                guestToken
              }
            }
          }
        }
        ",
        );
    }

    #[test]
    fn type_expansion_skip_include_test_1() {
        let schema_str =
            std::fs::read_to_string("./fixture/tests/requires-with-fragments.supergraph.graphql")
                .expect("Unable to read supergraph");
        let schema = parse_schema(&schema_str);
        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                        query($guest: Boolean!) {
                          userFromA {
                            profile {
                              displayName @include(if: $guest)
                              ... on Account @skip(if: $guest) {
                                accountType
                                ... on AdminAccount @skip(if: $guest) {
                                  adminLevel
                                }
                                ... on GuestAccount {
                                  guestToken
                                }
                              }
                            }
                          }
                        }
                      "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .unwrap()
                .to_string()
            ),
            @r"
      query($guest: Boolean!) {
        userFromA {
          profile {
            displayName @include(if: $guest)
            ... on AdminAccount @skip(if: $guest) {
              accountType
              adminLevel
            }
            ... on GuestAccount @skip(if: $guest) {
              accountType
              guestToken
            }
          }
        }
      }
      ",
        );
    }

    #[test]
    fn type_expansion_skip_include_test_2() {
        let schema_str =
            std::fs::read_to_string("./fixture/tests/requires-with-fragments.supergraph.graphql")
                .expect("Unable to read supergraph");
        let schema = parse_schema(&schema_str);
        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                        query($guest: Boolean!) {
                          userFromA {
                            profile {
                              displayName @include(if: $guest)
                              ... on Account {
                                accountType
                                ... on AdminAccount @skip(if: $guest) {
                                  adminLevel
                                }
                                ... on GuestAccount {
                                  guestToken
                                }
                              }
                            }
                          }
                        }
                      "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .unwrap()
                .to_string()
            ),
            @r"
        query($guest: Boolean!) {
          userFromA {
            profile {
              displayName @include(if: $guest)
              ... on AdminAccount {
                accountType
              }
              ... on AdminAccount @skip(if: $guest) {
                adminLevel
              }
              ... on GuestAccount {
                accountType
                guestToken
              }
            }
          }
        }
        ",
        );
    }

    #[test]
    fn type_expansion_skip_include_test_3() {
        let schema_str =
            std::fs::read_to_string("./fixture/tests/requires-with-fragments.supergraph.graphql")
                .expect("Unable to read supergraph");
        let schema = parse_schema(&schema_str);
        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                        query($guest: Boolean!) {
                          userFromA {
                            profile {
                              displayName @include(if: $guest)
                              ... on Account {
                                accountType @skip(if: $guest)
                                ... on AdminAccount @skip(if: $guest) {
                                  adminLevel
                                }
                                ... on GuestAccount {
                                  guestToken
                                }
                              }
                            }
                          }
                        }
                      "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .unwrap()
                .to_string()
            ),
            @r"
        query($guest: Boolean!) {
          userFromA {
            profile {
              displayName @include(if: $guest)
              ... on AdminAccount {
                accountType @skip(if: $guest)
              }
              ... on AdminAccount @skip(if: $guest) {
                adminLevel
              }
              ... on GuestAccount {
                accountType @skip(if: $guest)
                guestToken
              }
            }
          }
        }
        ",
        );
    }

    #[test]
    fn directive_only_inline_fragment_on_abstract_parent_include() {
        let schema_str =
            std::fs::read_to_string("./fixture/tests/requires-with-fragments.supergraph.graphql")
                .expect("Unable to read supergraph");
        let schema = parse_schema(&schema_str);
        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                        query($guest: Boolean!) {
                          userFromA {
                            profile {
                              ... @include(if: $guest) {
                                displayName
                                ... on Account {
                                  accountType
                                }
                              }
                            }
                          }
                        }
                      "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .unwrap()
                .to_string()
            ),
            @r"
        query($guest: Boolean!) {
          userFromA {
            profile {
              ... on AdminAccount @include(if: $guest) {
                displayName
                accountType
              }
              ... on GuestAccount @include(if: $guest) {
                displayName
                accountType
              }
            }
          }
        }
        "
        );
    }

    #[test]
    fn directive_only_inline_fragment_on_abstract_parent_skip() {
        let schema_str =
            std::fs::read_to_string("./fixture/tests/requires-with-fragments.supergraph.graphql")
                .expect("Unable to read supergraph");
        let schema = parse_schema(&schema_str);
        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                        query($guest: Boolean!) {
                          userFromA {
                            profile {
                              ... @skip(if: $guest) {
                                displayName
                                ... on Account {
                                  accountType
                                }
                              }
                            }
                          }
                        }
                      "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .unwrap()
                .to_string()
            ),
            @r"
        query($guest: Boolean!) {
          userFromA {
            profile {
              ... on AdminAccount @skip(if: $guest) {
                displayName
                accountType
              }
              ... on GuestAccount @skip(if: $guest) {
                displayName
                accountType
              }
            }
          }
        }
        "
        );
    }

    #[test]
    fn nested_fragment_spreads_1() {
        let schema_str =
            std::fs::read_to_string("./fixture/tests/nested-fragments.supergraph.graphql")
                .expect("Unable to read supergraph");
        let schema = parse_schema(&schema_str);
        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                        query {
                          results {
                            __typename
                            ...on Tshirt {
                              ...TshirtResult
                            }
                          }
                        }

                        fragment TshirtResult on Tshirt {
                          id
                          name
                          ...on MultipleColor {
                            colorOptions {
                              ...ColorDetails
                            }
                          }
                          ...on SingleColor {
                            colorOption {
                              ...ColorDetails
                            }
                          }
                        }

                        fragment ColorDetails on TshirtColorOption {
                          id
                          color
                        }
                  "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .expect("to normalize")
                .to_string()
            ),
            @r"
        {
          results {
            __typename
            ... on MultipleColor {
              id
              name
              colorOptions {
                id
                color
              }
            }
            ... on SingleColor {
              id
              name
              colorOption {
                id
                color
              }
            }
          }
        }
        ",
        );
    }

    #[test]
    fn inlining_nested_fragments_with_same_type() {
        let schema_str =
            std::fs::read_to_string("./fixture/tests/nested-fragments.supergraph.graphql")
                .expect("Unable to read supergraph");
        let schema = parse_schema(&schema_str);
        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            &pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                        query {
                          results {
                            __typename
                            ... on Tshirt {
                              ...TshirtResult1
                            }
                          }
                        }

                        fragment TshirtResult1 on Tshirt {
                          ...TshirtResult2
                        }

                        fragment TshirtResult2 on Tshirt {
                          id
                          name
                          ...on MultipleColor {
                            colorOptions {
                              ...ColorDetails
                            }
                          }
                          ...on SingleColor {
                            colorOption {
                              ...ColorDetails
                            }
                          }
                        }

                        fragment ColorDetails on TshirtColorOption {
                          id
                          color
                        }
                  "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .unwrap()
                .to_string()
            ),
            @r"
        {
          results {
            __typename
            ... on MultipleColor {
              id
              name
              colorOptions {
                id
                color
              }
            }
            ... on SingleColor {
              id
              name
              colorOption {
                id
                color
              }
            }
          }
        }
        "
        );
    }

    #[test]
    fn fragment_spread_on_interface_across_field_boundary_preserves_type_context() {
        let schema_str = std::fs::read_to_string("./fixture/issues/939.supergraph.graphql")
            .expect("Unable to read supergraph");
        let schema = parse_schema(&schema_str);
        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                        query SingleNode($id: ID!) {
                          node(id: $id) {
                            ... on MyNode {
                              content {
                                ... on ITextContent {
                                  fragments {
                                    contentNode {
                                      content {
                                        ...ITextContentPreview
                                      }
                                    }
                                  }
                                }
                              }
                            }
                          }
                        }

                        fragment ITextContentPreview on ITextContent {
                          id
                        }
                      "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .expect("to normalize")
                .to_string()
            ),
            @r#"
        query SingleNode($id: ID!) {
          node(id: $id) {
            ... on MyNode {
              content {
                ... on TextContent {
                  fragments {
                    contentNode {
                      content {
                        ... on TextContent {
                          id
                        }
                        ... on TextGroupContent {
                          id
                        }
                      }
                    }
                  }
                }
                ... on TextGroupContent {
                  fragments {
                    contentNode {
                      content {
                        ... on TextContent {
                          id
                        }
                        ... on TextGroupContent {
                          id
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
        "#
        );
    }

    #[test]
    fn same_type_fragment_spread_directives_are_preserved() {
        let schema = parse_schema(
            r#"
            type Query {
              account: Account
            }

            interface Node {
              id: ID!
            }

            type Account implements Node {
              id: ID!
              name: String!
            }
            "#,
        );
        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                        query($cond: Boolean!) {
                          account {
                            ...AccountFields @include(if: $cond)
                          }
                        }

                        fragment AccountFields on Account {
                          name
                        }
                        "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .expect("to normalize")
                .to_string()
            ),
            @r#"
        query($cond: Boolean!) {
          account {
            ... on Account @include(if: $cond) {
              name
            }
          }
        }
        "#
        );
    }

    #[test]
    fn same_type_fragment_spread_skip_is_preserved() {
        let schema = parse_schema(
            r#"
            type Query {
              account: Account
            }

            type Account {
              id: ID!
              name: String!
            }
            "#,
        );
        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                        query($cond: Boolean!) {
                          account {
                            ...AccountFields @skip(if: $cond)
                          }
                        }

                        fragment AccountFields on Account {
                          name
                        }
                        "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .expect("to normalize")
                .to_string()
            ),
            @r#"
        query($cond: Boolean!) {
          account {
            ... on Account @skip(if: $cond) {
              name
            }
          }
        }
        "#
        );
    }

    #[test]
    fn same_type_fragment_spread_include_and_skip_are_preserved() {
        let schema = parse_schema(
            r#"
            type Query {
              account: Account
            }

            type Account {
              id: ID!
              name: String!
            }
            "#,
        );
        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                        query($include: Boolean!, $skip: Boolean!) {
                          account {
                            ...AccountFields @include(if: $include) @skip(if: $skip)
                          }
                        }

                        fragment AccountFields on Account {
                          name
                        }
                        "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .expect("to normalize")
                .to_string()
            ),
            @r#"
        query($include: Boolean!, $skip: Boolean!) {
          account {
            ... on Account @skip(if: $skip) @include(if: $include) {
              name
            }
          }
        }
        "#
        );
    }

    #[test]
    fn outer_same_type_fragment_spread_directive_is_preserved_through_nested_spreads() {
        let schema = parse_schema(
            r#"
            type Query {
              account: Account
            }

            type Account {
              id: ID!
              name: String!
            }
            "#,
        );
        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                        query($cond: Boolean!) {
                          account {
                            ...AccountFields1 @include(if: $cond)
                          }
                        }

                        fragment AccountFields1 on Account {
                          ...AccountFields2
                        }

                        fragment AccountFields2 on Account {
                          name
                        }
                        "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .expect("to normalize")
                .to_string()
            ),
            @r#"
        query($cond: Boolean!) {
          account {
            ... on Account @include(if: $cond) {
              name
            }
          }
        }
        "#
        );
    }

    #[test]
    fn inner_same_type_fragment_spread_directive_is_preserved_through_nested_spreads() {
        let schema = parse_schema(
            r#"
            type Query {
              account: Account
            }

            type Account {
              id: ID!
              name: String!
            }
            "#,
        );
        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                        query($cond: Boolean!) {
                          account {
                            ...AccountFields1
                          }
                        }

                        fragment AccountFields1 on Account {
                          ...AccountFields2 @include(if: $cond)
                        }

                        fragment AccountFields2 on Account {
                          name
                        }
                        "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .expect("to normalize")
                .to_string()
            ),
            @r#"
        query($cond: Boolean!) {
          account {
            ... on Account @include(if: $cond) {
              name
            }
          }
        }
        "#
        );
    }

    #[test]
    fn field_boundary_fragment_spread_include_is_preserved() {
        let schema_str = std::fs::read_to_string("./fixture/issues/939.supergraph.graphql")
            .expect("Unable to read supergraph");
        let schema = parse_schema(&schema_str);
        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                        query SingleNode($cond: Boolean!, $id: ID!) {
                          node(id: $id) {
                            ... on MyNode {
                              content {
                                ... on ITextContent {
                                  fragments {
                                    contentNode {
                                      content {
                                        ...ITextContentPreview @include(if: $cond)
                                      }
                                    }
                                  }
                                }
                              }
                            }
                          }
                        }

                        fragment ITextContentPreview on ITextContent {
                          id
                        }
                        "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .expect("to normalize")
                .to_string()
            ),
            @r#"
        query SingleNode($cond: Boolean!, $id: ID!) {
          node(id: $id) {
            ... on MyNode {
              content {
                ... on TextContent {
                  fragments {
                    contentNode {
                      content {
                        ... on TextContent @include(if: $cond) {
                          id
                        }
                        ... on TextGroupContent @include(if: $cond) {
                          id
                        }
                      }
                    }
                  }
                }
                ... on TextGroupContent {
                  fragments {
                    contentNode {
                      content {
                        ... on TextContent @include(if: $cond) {
                          id
                        }
                        ... on TextGroupContent @include(if: $cond) {
                          id
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
        "#
        );
    }

    #[test]
    fn reusable_fragment_with_mixed_include_conditions_stays_separate() {
        let schema = parse_schema(
            r#"
            type Query {
              account: Account
            }

            type Account {
              id: ID!
              name: String!
            }
            "#,
        );
        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                        query($first: Boolean!, $second: Boolean!) {
                          account {
                            ...AccountFields @include(if: $first)
                            ...AccountFields @include(if: $second)
                          }
                        }

                        fragment AccountFields on Account {
                          name
                        }
                        "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .expect("to normalize")
                .to_string()
            ),
            @r#"
        query($first: Boolean!, $second: Boolean!) {
          account {
            ... on Account @include(if: $first) {
              name
            }
            ... on Account @include(if: $second) {
              name
            }
          }
        }
        "#
        );
    }

    #[test]
    fn deeply_nested_mixed_abstract_fragments_flatten_to_concrete_object() {
        let schema_str = std::fs::read_to_string(
            "./fixture/tests/corrupted-supergraph-node-id.supergraph.graphql",
        )
        .expect("Unable to read supergraph");
        let schema = parse_schema(&schema_str);
        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                        query {
                          account(id: "a1") {
                            ...NodeFields
                          }
                        }

                        fragment NodeFields on Node {
                          ... on Node {
                            ... {
                              ... on Node {
                                id
                              }
                              ... on Account {
                                username
                              }
                            }
                          }
                        }
                        "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .expect("to normalize")
                .to_string()
            ),
            @r#"
        {
          account(id: "a1") {
            id
            username
          }
        }
        "#
        );
    }

    #[test]
    fn deeply_nested_abstract_fragments_with_conflicting_include_directives_stay_scoped() {
        let schema_str = std::fs::read_to_string(
            "./fixture/tests/corrupted-supergraph-node-id.supergraph.graphql",
        )
        .expect("Unable to read supergraph");
        let schema = parse_schema(&schema_str);
        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                        query($outer: Boolean!, $inner: Boolean!) {
                          account(id: "a1") {
                            ... on Node @include(if: $outer) {
                              ... on Node @include(if: $inner) {
                                id
                              }
                              ... on Account {
                                username
                              }
                            }
                          }
                        }
                        "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .expect("to normalize")
                .to_string()
            ),
            @r#"
        query($outer: Boolean!, $inner: Boolean!) {
          account(id: "a1") {
            ... on Account @include(if: $outer) {
              ... on Account @include(if: $inner) {
                id
              }
              username
            }
          }
        }
        "#
        );
    }

    #[test]
    fn deeply_nested_abstract_fragments_with_skip_and_include_preserve_both_conditions() {
        let schema_str = std::fs::read_to_string(
            "./fixture/tests/corrupted-supergraph-node-id.supergraph.graphql",
        )
        .expect("Unable to read supergraph");
        let schema = parse_schema(&schema_str);
        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                        query($skip: Boolean!, $include: Boolean!) {
                          account(id: "a1") {
                            ... on Node @skip(if: $skip) {
                              ... on Node @include(if: $include) {
                                ... {
                                  id
                                }
                              }
                              ... on Account {
                                username
                              }
                            }
                          }
                        }
                        "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .expect("to normalize")
                .to_string()
            ),
            @r#"
        query($skip: Boolean!, $include: Boolean!) {
          account(id: "a1") {
            ... on Account @skip(if: $skip) @include(if: $include) {
              id
            }
            ... on Account @skip(if: $skip) {
              username
            }
          }
        }
        "#
        );
    }

    #[test]
    fn untyped_inline_fragment_inside_nested_abstract_fragment_inherits_concrete_object_context() {
        let schema_str = std::fs::read_to_string(
            "./fixture/tests/corrupted-supergraph-node-id.supergraph.graphql",
        )
        .expect("Unable to read supergraph");
        let schema = parse_schema(&schema_str);
        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                        query {
                          account(id: "a1") {
                            ... on Node {
                              ... {
                                id
                                ... on Account {
                                  username
                                }
                              }
                            }
                          }
                        }
                        "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .expect("to normalize")
                .to_string()
            ),
            @r#"
        {
          account(id: "a1") {
            id
            username
          }
        }
        "#
        );
    }

    #[test]
    fn named_fragment_on_abstract_type_with_nested_abstract_directives_preserves_all_conditions() {
        let schema_str = std::fs::read_to_string(
            "./fixture/tests/corrupted-supergraph-node-id.supergraph.graphql",
        )
        .expect("Unable to read supergraph");
        let schema = parse_schema(&schema_str);
        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                        query($outer: Boolean!, $inner: Boolean!, $skip: Boolean!) {
                          account(id: "a1") {
                            ...AccountNodeFields
                          }
                        }

                        fragment AccountNodeFields on Node {
                          ... on Node @include(if: $outer) {
                            id
                            ...InnerNode @include(if: $inner)
                          }
                          ... on Account @skip(if: $skip) {
                            username
                          }
                        }

                        fragment InnerNode on Node {
                          ... {
                            id
                          }
                        }
                        "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .expect("to normalize")
                .to_string()
            ),
            @r#"
        query($outer: Boolean!, $inner: Boolean!, $skip: Boolean!) {
          account(id: "a1") {
            ... on Account @include(if: $outer) {
              id
              ... on Account @include(if: $inner) {
                id
              }
            }
            ... on Account @skip(if: $skip) {
              username
            }
          }
        }
        "#
        );
    }

    #[test]
    fn matching_concrete_wrapper_with_typename_and_directive_is_not_flattened() {
        let schema = parse_schema(
            r#"
            type Query {
              account: Account
            }

            type Account {
              id: ID!
              username: String!
            }
            "#,
        );
        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                        query($cond: Boolean!) {
                          account {
                            ... on Account @include(if: $cond) {
                              id
                              __typename
                            }
                            username
                          }
                        }
                        "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .expect("to normalize")
                .to_string()
            ),
            @r#"
        query($cond: Boolean!) {
          account {
            ... on Account @include(if: $cond) {
              id
              __typename
            }
            username
          }
        }
        "#
        );
    }

    #[test]
    fn matching_concrete_wrapper_with_both_directives_is_not_flattened() {
        let schema = parse_schema(
            r#"
            type Query {
              account: Account
            }

            type Account {
              id: ID!
              username: String!
            }
            "#,
        );
        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                        query($include: Boolean!, $skip: Boolean!) {
                          account {
                            ... on Account @include(if: $include) @skip(if: $skip) {
                              id
                            }
                            username
                          }
                        }
                        "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .expect("to normalize")
                .to_string()
            ),
            @r#"
        query($include: Boolean!, $skip: Boolean!) {
          account {
            ... on Account @skip(if: $skip) @include(if: $include) {
              id
            }
            username
          }
        }
        "#
        );
    }

    #[test]
    fn matching_concrete_wrapper_with_directive_is_not_flattened() {
        let schema = parse_schema(
            r#"
            type Query {
              account: Account
            }

            type Account {
              id: ID!
              username: String!
            }
            "#,
        );
        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                        query($cond: Boolean!) {
                          account {
                            ... on Account @include(if: $cond) {
                              id
                            }
                            username
                          }
                        }
                        "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .expect("to normalize")
                .to_string()
            ),
            @r#"
        query($cond: Boolean!) {
          account {
            ... on Account @include(if: $cond) {
              id
            }
            username
          }
        }
        "#
        );
    }

    #[test]
    fn multiple_directives_and_conditions_and_fragments() {
        let schema = parse_schema(
            r#"
            schema
                @link(url: "https://specs.apollo.dev/link/v1.0")
                @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) {
                query: Query
            }

            directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE

            directive @join__graph(name: String!, url: String!) on ENUM_VALUE

            directive @join__field(
                graph: join__Graph
                requires: join__FieldSet
                provides: join__FieldSet
                type: String
                external: Boolean
                override: String
                usedOverridden: Boolean
            ) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION

            directive @join__implements(
                graph: join__Graph!
                interface: String!
            ) repeatable on OBJECT | INTERFACE

            directive @join__type(
                graph: join__Graph!
                key: join__FieldSet
                extension: Boolean! = false
                resolvable: Boolean! = true
                isInterfaceObject: Boolean! = false
            ) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR

            directive @join__unionMember(
                graph: join__Graph!
                member: String!
            ) repeatable on UNION

            scalar join__FieldSet

            directive @link(
                url: String
                as: String
                for: link__Purpose
                import: [link__Import]
            ) repeatable on SCHEMA

            scalar link__Import

            enum link__Purpose {
                """
                `SECURITY` features provide metadata necessary to securely resolve fields.
                """
                SECURITY

                """
                `EXECUTION` features provide metadata necessary for operation execution.
                """
                EXECUTION
            }

            enum join__Graph {
                LOCAL @join__graph(name: "local", url: "")
            }

            type Dog implements Pet & Animal & Node
                @join__type(graph: LOCAL)
                @join__implements(graph: LOCAL, interface: "Pet")
                @join__implements(graph: LOCAL, interface: "Animal")
                @join__implements(graph: LOCAL, interface: "Node") {
                id: ID!
                name: String!
                bestFriend: Animal
                weight(unit: WeightUnit = KG): Float
                nickname: String
                tags: [String!]
            }

            type Cat implements Pet & Animal & Node
                @join__type(graph: LOCAL)
                @join__implements(graph: LOCAL, interface: "Pet")
                @join__implements(graph: LOCAL, interface: "Animal")
                @join__implements(graph: LOCAL, interface: "Node") {
                id: ID!
                name: String!
                bestFriend: Cat
                weight(unit: WeightUnit = KG): Float
                age: Int
                tags: [String!]
            }

            type Robot implements Node
                @join__type(graph: LOCAL)
                @join__implements(graph: LOCAL, interface: "Node") {
                id: ID!
                model: String!
                weight(unit: WeightUnit = KG): Float
            }

            type Owner implements Node
                @join__type(graph: LOCAL)
                @join__implements(graph: LOCAL, interface: "Node") {
                id: ID!
                name: String!
                pets: [Pet!]!
                primaryPet: Pet
            }

            type Query @join__type(graph: LOCAL) {
                pet: Pet
                animal: Animal
                node: Node
                search: SearchResult
                searchMany: [SearchResult!]!
                pets: [Pet!]!
                owner: Owner
            }

            interface Node @join__type(graph: LOCAL) {
                id: ID!
            }

            interface Animal implements Node
                @join__type(graph: LOCAL)
                @join__implements(graph: LOCAL, interface: "Node") {
                id: ID!
                name: String!
            }

            interface Pet implements Animal & Node
                @join__type(graph: LOCAL)
                @join__implements(graph: LOCAL, interface: "Animal")
                @join__implements(graph: LOCAL, interface: "Node") {
                id: ID!
                name: String!
                bestFriend: Animal
                weight(unit: WeightUnit = KG): Float
            }

            union SearchResult
                @join__type(graph: LOCAL)
                @join__unionMember(graph: LOCAL, member: "Dog")
                @join__unionMember(graph: LOCAL, member: "Cat")
                @join__unionMember(graph: LOCAL, member: "Robot") =
                | Dog
                | Cat
                | Robot

            enum WeightUnit @join__type(graph: LOCAL) {
                KG @join__enumValue(graph: LOCAL)
                LB @join__enumValue(graph: LOCAL)
                G @join__enumValue(graph: LOCAL)
            }

            "#,
        );
        let supergraph = SupergraphState::new(&schema);

        insta::assert_snapshot!(
            pretty_query(
                normalize_operation(
                    &supergraph,
                    &parse_query(
                        r#"
                        query SpreadDirective($withName: Boolean!, $deep: Boolean!) {
                          node {
                            id
                            ...AnimalBits @include(if: $withName)
                            ... on Dog @skip(if: $deep) { nickname }
                          }
                        }

                        fragment AnimalBits on Animal {
                          name
                          ... on Dog { nickname }
                        }
                        "#,
                    )
                    .expect("to parse"),
                    None,
                )
                .expect("to normalize")
                .to_string()
            ),
            @"
        query SpreadDirective($withName: Boolean!, $deep: Boolean!) {
          node {
            id
            ... on Cat @include(if: $withName) {
              name
            }
            ... on Dog @include(if: $withName) {
              name
              nickname
            }
            ... on Dog @skip(if: $deep) {
              nickname
            }
          }
        }
        "
        );
    }

    #[test]
    fn acyclic_fragment_chain_does_not_overflow_stack() {
        let schema = parse_schema(
            r#"
            type Query {
              field: String
            }
            "#,
        );
        let supergraph = SupergraphState::new(&schema);

        // very long acyclic fragment chain that would overflow the stack if protections were not in place
        let n = 10_000;
        let mut doc = String::from("query { ...F1 }\n");
        for i in 1..n {
            doc.push_str(&format!("fragment F{i} on Query {{ ...F{} }}\n", i + 1));
        }
        doc.push_str(&format!("fragment F{n} on Query {{ __typename }}\n"));

        let parsed = parse_query::<String>(&doc)
            .expect("valid graphql")
            .into_static();

        // must return without overflowing; Ok or Err (i.e. no crash) both mean we survived
        let _ = normalize_operation(&supergraph, &parsed, None);
    }

    #[test]
    fn cyclic_fragment_self_reference_returns_error() {
        // fragment A on Query { ...A } — inlineable single-spread self-cycle
        let schema = parse_schema(
            r#"
            type Query {
              field: String
            }
            "#,
        );
        let supergraph = SupergraphState::new(&schema);
        let parsed = parse_query::<String>(
            r#"
            query { ...A }
            fragment A on Query { ...A }
            "#,
        )
        .expect("to parse")
        .into_static();

        // run in a thread with a deadline so an infinite loop fails fast
        let (tx, rx) = std::sync::mpsc::channel();
        std::thread::spawn(move || {
            let result = normalize_operation(&supergraph, &parsed, None);
            let _ = tx.send(result);
        });
        let result = rx
            .recv_timeout(std::time::Duration::from_secs(1))
            .expect("normalize_operation hung on a self-referential fragment cycle");
        assert!(
            result.is_err(),
            "expected an error for a cyclic fragment, got Ok"
        );
    }

    #[test]
    fn cyclic_fragment_mutual_reference_returns_error() {
        // fragment A on Query { ...B }, fragment B on Query { ...A } — mutual inlineable cycle
        let schema = parse_schema(
            r#"
            type Query {
              field: String
            }
            "#,
        );
        let supergraph = SupergraphState::new(&schema);
        let parsed = parse_query::<String>(
            r#"
            query { ...A }
            fragment A on Query { ...B }
            fragment B on Query { ...A }
            "#,
        )
        .expect("to parse")
        .into_static();

        // run in a thread with a deadline so an infinite loop fails fast
        let (tx, rx) = std::sync::mpsc::channel();
        std::thread::spawn(move || {
            let result = normalize_operation(&supergraph, &parsed, None);
            let _ = tx.send(result);
        });
        let result = rx
            .recv_timeout(std::time::Duration::from_secs(1))
            .expect("normalize_operation hung on a mutual fragment cycle");
        assert!(
            result.is_err(),
            "expected an error for a cyclic fragment, got Ok"
        );
    }

    #[test]
    fn cyclic_fragment_self_reference_with_sibling_field_returns_error() {
        // fragment A on Query { x ...A } - self-cycle with a sibling field breaks the
        // single-bare-spread fast path and must still be rejected, not overflow the stack.
        let schema = parse_schema(
            r#"
            type Query {
              x: String
            }
            "#,
        );
        let supergraph = SupergraphState::new(&schema);
        let parsed = parse_query::<String>(
            r#"
            query { ...A }
            fragment A on Query { x ...A }
            "#,
        )
        .expect("to parse")
        .into_static();

        // run in a thread with a deadline so a stack overflow / hang fails as a test failure
        // instead of aborting the whole test process.
        let (tx, rx) = std::sync::mpsc::channel();
        std::thread::spawn(move || {
            let result = normalize_operation(&supergraph, &parsed, None);
            let _ = tx.send(result);
        });
        let result = rx
            .recv_timeout(std::time::Duration::from_secs(1))
            .expect("normalize_operation hung or crashed on a self-cycle with a sibling field");
        assert!(
            result.is_err(),
            "expected an error for a cyclic fragment, got Ok"
        );
    }

    #[test]
    fn cyclic_fragment_self_reference_with_directive_returns_error() {
        // fragment A on Query { ...A @include(if: $c) } - a directive on the cycling spread
        // also breaks the single-bare-spread fast path and must still be rejected.
        let schema = parse_schema(
            r#"
            type Query {
              field: String
            }
            "#,
        );
        let supergraph = SupergraphState::new(&schema);
        let parsed = parse_query::<String>(
            r#"
            query ($c: Boolean!) { ...A }
            fragment A on Query { ...A @include(if: $c) }
            "#,
        )
        .expect("to parse")
        .into_static();

        let (tx, rx) = std::sync::mpsc::channel();
        std::thread::spawn(move || {
            let result = normalize_operation(&supergraph, &parsed, None);
            let _ = tx.send(result);
        });
        let result = rx
            .recv_timeout(std::time::Duration::from_secs(1))
            .expect("normalize_operation hung or crashed on a self-cycle with a directive");
        assert!(
            result.is_err(),
            "expected an error for a cyclic fragment, got Ok"
        );
    }
}
