/*
 * 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.
 */

#![deny(warnings)]
#![deny(rust_2018_idioms)]
#![deny(clippy::all)]

mod check;

pub mod definitions;
use common::SourceLocationKey;
use fnv::FnvHashMap;
use fnv::FnvHashSet;
use graphql_syntax::parse_schema_document;
use graphql_syntax::EnumTypeDefinition;
use graphql_syntax::FieldDefinition;
use graphql_syntax::Identifier;
use graphql_syntax::InputObjectTypeDefinition;
use graphql_syntax::InputValueDefinition;
use graphql_syntax::InterfaceTypeDefinition;
use graphql_syntax::List;
use graphql_syntax::ObjectTypeDefinition;
use graphql_syntax::ScalarTypeDefinition;
use graphql_syntax::TypeSystemDefinition;
use graphql_syntax::UnionTypeDefinition;
use intern::string_key::StringKey;

use crate::definitions::*;

fn add_definition(changes: &mut Vec<DefinitionChange>, def: &TypeSystemDefinition) -> bool {
    use DefinitionChange::*;

    match def {
        TypeSystemDefinition::ScalarTypeDefinition(ScalarTypeDefinition { name, .. }) => {
            changes.push(ScalarAdded(name.value));
            true
        }
        TypeSystemDefinition::UnionTypeDefinition(UnionTypeDefinition { name, .. }) => {
            changes.push(UnionAdded(name.value));
            true
        }
        TypeSystemDefinition::EnumTypeDefinition(EnumTypeDefinition { name, .. }) => {
            changes.push(EnumAdded(name.value));
            true
        }
        TypeSystemDefinition::InputObjectTypeDefinition(InputObjectTypeDefinition {
            name, ..
        }) => {
            changes.push(InputObjectAdded(name.value));
            true
        }
        TypeSystemDefinition::InterfaceTypeDefinition(InterfaceTypeDefinition { name, .. }) => {
            changes.push(InterfaceAdded(name.value));
            true
        }
        TypeSystemDefinition::ObjectTypeDefinition(ObjectTypeDefinition { name, .. }) => {
            changes.push(ObjectAdded(name.value));
            true
        }
        // We currently don't handle changes of unsupported types,
        // If those type changes occur, we should return early for a full rebuild.
        TypeSystemDefinition::InterfaceTypeExtension(..)
        | TypeSystemDefinition::ObjectTypeExtension(..)
        | TypeSystemDefinition::SchemaDefinition(..)
        | TypeSystemDefinition::DirectiveDefinition(..)
        | TypeSystemDefinition::SchemaExtension(..)
        | TypeSystemDefinition::EnumTypeExtension(..)
        | TypeSystemDefinition::UnionTypeExtension(..)
        | TypeSystemDefinition::InputObjectTypeExtension(..)
        | TypeSystemDefinition::ScalarTypeExtension(..) => false,
    }
}

fn build_current_map(
    current: &[TypeSystemDefinition],
) -> FnvHashMap<&StringKey, &TypeSystemDefinition> {
    let mut current_map = FnvHashMap::default();

    for def in current {
        match def {
            TypeSystemDefinition::EnumTypeDefinition(EnumTypeDefinition { name, .. })
            | TypeSystemDefinition::UnionTypeDefinition(UnionTypeDefinition { name, .. })
            | TypeSystemDefinition::InputObjectTypeDefinition(InputObjectTypeDefinition {
                name,
                ..
            })
            | TypeSystemDefinition::InterfaceTypeDefinition(InterfaceTypeDefinition {
                name, ..
            })
            | TypeSystemDefinition::ObjectTypeDefinition(ObjectTypeDefinition { name, .. })
            | TypeSystemDefinition::ScalarTypeDefinition(ScalarTypeDefinition { name, .. }) => {
                current_map.insert(&name.value, def);
            }
            // We skip diffing the following definitions
            TypeSystemDefinition::EnumTypeExtension(..)
            | TypeSystemDefinition::UnionTypeExtension(..)
            | TypeSystemDefinition::InputObjectTypeExtension(..)
            | TypeSystemDefinition::InterfaceTypeExtension(..)
            | TypeSystemDefinition::ObjectTypeExtension(..)
            | TypeSystemDefinition::ScalarTypeExtension(..)
            | TypeSystemDefinition::SchemaDefinition(..)
            | TypeSystemDefinition::SchemaExtension(..)
            | TypeSystemDefinition::DirectiveDefinition(..) => {}
        }
    }
    current_map
}

fn compare_fields(
    optional_current_fields: &Option<List<FieldDefinition>>,
    optional_previous_fields: Option<List<FieldDefinition>>,
) -> (Vec<TypeChange>, Vec<TypeChange>, Vec<ArgumentChange>) {
    let mut added: Vec<TypeChange> = vec![];
    let mut removed: Vec<TypeChange> = vec![];
    let mut field_changed: Vec<ArgumentChange> = vec![];

    match (optional_current_fields.as_ref(), optional_previous_fields) {
        (Some(current_fields), Some(previous_fields)) => {
            let mut previous_values = previous_fields
                .items
                .into_iter()
                .map(|input| (input.name.value, input))
                .collect::<FnvHashMap<_, _>>();

            for field in &current_fields.items {
                match previous_values.remove(&field.name.value) {
                    None => {
                        added.push(TypeChange {
                            name: field.name.value,
                            type_: Type::from(field.type_.clone()),
                        });
                    }
                    Some(previous_field) => {
                        let previous_field_type = Type::from(previous_field.type_.clone());
                        let field_type = Type::from(field.type_.clone());
                        if previous_field_type != field_type {
                            added.push(TypeChange {
                                name: field.name.value,
                                type_: field_type,
                            });
                            removed.push(TypeChange {
                                name: previous_field.name.value,
                                type_: previous_field_type,
                            });
                        } else {
                            match (field.arguments.as_ref(), previous_field.arguments) {
                                (Some(current_field_arguments), Some(previous_field_arguments)) => {
                                    let (args_added, args_removed) = compare_input_value_definition(
                                        &current_field_arguments.items,
                                        previous_field_arguments.items,
                                    );
                                    if !args_added.is_empty() || !args_removed.is_empty() {
                                        field_changed.push(ArgumentChange {
                                            name: field.name.value,
                                            added: args_added,
                                            removed: args_removed,
                                        });
                                    }
                                }
                                (Some(current_field_arguments), None) => {
                                    let current_field_arguments = current_field_arguments.clone();
                                    let args_added = current_field_arguments
                                        .items
                                        .into_iter()
                                        .map(|InputValueDefinition { name, type_, .. }| {
                                            TypeChange {
                                                name: name.value,
                                                type_: Type::from(type_),
                                            }
                                        })
                                        .collect();
                                    let args_removed = vec![];
                                    field_changed.push(ArgumentChange {
                                        name: field.name.value,
                                        added: args_added,
                                        removed: args_removed,
                                    });
                                }
                                (None, Some(previous_field_arguments)) => {
                                    let args_added = vec![];
                                    let args_removed = previous_field_arguments
                                        .items
                                        .into_iter()
                                        .map(|InputValueDefinition { name, type_, .. }| {
                                            TypeChange {
                                                name: name.value,
                                                type_: Type::from(type_),
                                            }
                                        })
                                        .collect();
                                    field_changed.push(ArgumentChange {
                                        name: field.name.value,
                                        added: args_added,
                                        removed: args_removed,
                                    });
                                }
                                (None, None) => {}
                            }
                        }
                    }
                }
            }
            removed.extend(previous_values.drain().map(|(_, field)| TypeChange {
                name: field.name.value,
                type_: Type::from(field.type_),
            }));
        }
        (None, Some(previous_fields)) => {
            removed.extend(previous_fields.items.into_iter().map(
                |FieldDefinition { name, type_, .. }| TypeChange {
                    name: name.value,
                    type_: Type::from(type_),
                },
            ));
        }
        (Some(current_fields), None) => {
            let current_field_items = current_fields.items.clone();
            added.extend(current_field_items.into_iter().map(
                |FieldDefinition { name, type_, .. }| TypeChange {
                    name: name.value,
                    type_: Type::from(type_),
                },
            ));
        }
        (None, None) => {}
    }

    (added, removed, field_changed)
}

fn compare_input_value_definition(
    current: &[InputValueDefinition],
    previous: Vec<InputValueDefinition>,
) -> (Vec<TypeChange>, Vec<TypeChange>) {
    let mut previous_values = previous
        .into_iter()
        .map(|value_def| (value_def.name.value, value_def.type_))
        .collect::<FnvHashMap<_, _>>();

    let mut added = vec![];
    let mut removed = vec![];

    for field in current {
        let previous_type = previous_values.remove(&field.name.value);
        match previous_type {
            None => {
                added.push(TypeChange {
                    name: field.name.value,
                    type_: Type::from(field.type_.clone()),
                });
            }
            Some(previous_type) => {
                let previous_type = Type::from(previous_type);
                let field_type = Type::from(field.type_.clone());
                if previous_type != field_type {
                    removed.push(TypeChange {
                        name: field.name.value,
                        type_: previous_type,
                    });
                    added.push(TypeChange {
                        name: field.name.value,
                        type_: field_type,
                    });
                }
            }
        }
    }
    removed.extend(previous_values.drain().map(|(name, type_)| TypeChange {
        name,
        type_: Type::from(type_),
    }));

    (added, removed)
}

fn compare_string_keys(
    current: &[Identifier],
    previous: Vec<Identifier>,
) -> (Vec<StringKey>, Vec<StringKey>) {
    let mut previous_values = previous
        .into_iter()
        .map(|ident| ident.value)
        .collect::<FnvHashSet<_>>();
    let mut added: Vec<StringKey> = vec![];
    for key in current {
        if !previous_values.remove(&key.value) {
            added.push(key.value);
        }
    }
    let removed: Vec<StringKey> = previous_values.drain().collect();
    (added, removed)
}

fn diff(current: Vec<TypeSystemDefinition>, previous: Vec<TypeSystemDefinition>) -> SchemaChange {
    let mut changes = vec![];
    let mut current_map = build_current_map(&current);

    for definition in previous {
        match definition {
            TypeSystemDefinition::EnumTypeDefinition(EnumTypeDefinition {
                name,
                values: previous_values,
                ..
            }) => {
                let def = current_map.remove(&name.value);
                match (def, previous_values) {
                    (
                        Some(TypeSystemDefinition::EnumTypeDefinition(EnumTypeDefinition {
                            values: Some(values),
                            ..
                        })),
                        Some(previous_values),
                    ) => {
                        if values.items.len() != previous_values.items.len() {
                            changes.push(DefinitionChange::EnumChanged { name: name.value });
                        } else {
                            for i in 0..values.items.len() {
                                if values.items[i].name.value != previous_values.items[i].name.value
                                {
                                    changes
                                        .push(DefinitionChange::EnumChanged { name: name.value });
                                    break;
                                }
                            }
                        }
                    }
                    (None, _) => {
                        changes.push(DefinitionChange::EnumRemoved(name.value));
                    }
                    (Some(def), _) => {
                        if !add_definition(&mut changes, def) {
                            return SchemaChange::GenericChange;
                        }
                        changes.push(DefinitionChange::EnumRemoved(name.value));
                    }
                }
            }

            TypeSystemDefinition::UnionTypeDefinition(UnionTypeDefinition {
                name,
                members: previous_members,
                ..
            }) => {
                let def = current_map.remove(&name.value);
                match def {
                    Some(TypeSystemDefinition::UnionTypeDefinition(UnionTypeDefinition {
                        members,
                        ..
                    })) => {
                        let (added, removed) = compare_string_keys(members, previous_members);
                        if !added.is_empty() || !removed.is_empty() {
                            changes.push(DefinitionChange::UnionChanged {
                                name: name.value,
                                added,
                                removed,
                            });
                        }
                    }
                    None => {
                        changes.push(DefinitionChange::UnionRemoved(name.value));
                    }
                    Some(def) => {
                        if !add_definition(&mut changes, def) {
                            return SchemaChange::GenericChange;
                        }
                        changes.push(DefinitionChange::UnionRemoved(name.value));
                    }
                }
            }

            TypeSystemDefinition::InputObjectTypeDefinition(InputObjectTypeDefinition {
                name,
                fields: previous_fields,
                ..
            }) => {
                let def = current_map.remove(&name.value);
                match def {
                    Some(TypeSystemDefinition::InputObjectTypeDefinition(
                        InputObjectTypeDefinition {
                            fields: Some(fields),
                            ..
                        },
                    )) => {
                        let (added, removed) = compare_input_value_definition(
                            &fields.items,
                            previous_fields
                                .into_iter()
                                .flat_map(|list| list.items)
                                .collect(),
                        );
                        if !added.is_empty() || !removed.is_empty() {
                            changes.push(DefinitionChange::InputObjectChanged {
                                name: name.value,
                                added,
                                removed,
                            });
                        }
                    }
                    None
                    | Some(TypeSystemDefinition::InputObjectTypeDefinition(
                        InputObjectTypeDefinition { fields: None, .. },
                    )) => {
                        changes.push(DefinitionChange::InputObjectRemoved(name.value));
                    }
                    Some(def) => {
                        if !add_definition(&mut changes, def) {
                            return SchemaChange::GenericChange;
                        }
                        changes.push(DefinitionChange::InputObjectRemoved(name.value));
                    }
                }
            }

            TypeSystemDefinition::InterfaceTypeDefinition(InterfaceTypeDefinition {
                name,
                fields: optional_previous_fields,
                ..
            }) => {
                let def = current_map.remove(&name.value);
                match def {
                    Some(TypeSystemDefinition::InterfaceTypeDefinition(
                        InterfaceTypeDefinition {
                            fields: optional_current_fields,
                            ..
                        },
                    )) => {
                        let (added, removed, changed) =
                            compare_fields(optional_current_fields, optional_previous_fields);
                        if !added.is_empty() || !removed.is_empty() || !changed.is_empty() {
                            changes.push(DefinitionChange::InterfaceChanged {
                                name: name.value,
                                added,
                                removed,
                                changed,
                            });
                        }
                    }
                    None => {
                        changes.push(DefinitionChange::InterfaceRemoved(name.value));
                    }
                    Some(def) => {
                        if !add_definition(&mut changes, def) {
                            return SchemaChange::GenericChange;
                        }
                        changes.push(DefinitionChange::InterfaceRemoved(name.value));
                    }
                }
            }

            TypeSystemDefinition::ObjectTypeDefinition(ObjectTypeDefinition {
                name,
                interfaces: previous_interfaces,
                fields: optional_previous_fields,
                ..
            }) => {
                let def = current_map.remove(&name.value);
                match def {
                    Some(TypeSystemDefinition::ObjectTypeDefinition(ObjectTypeDefinition {
                        interfaces: current_interfaces,
                        fields: optional_current_fields,
                        ..
                    })) => {
                        let (added, removed, changed) =
                            compare_fields(optional_current_fields, optional_previous_fields);
                        let (interfaces_added, interfaces_removed) =
                            compare_string_keys(current_interfaces, previous_interfaces);
                        if !added.is_empty()
                            || !removed.is_empty()
                            || !changed.is_empty()
                            || !interfaces_added.is_empty()
                            || !interfaces_removed.is_empty()
                        {
                            changes.push(DefinitionChange::ObjectChanged {
                                name: name.value,
                                added,
                                removed,
                                changed,
                                interfaces_added,
                                interfaces_removed,
                            });
                        }
                    }
                    None => {
                        changes.push(DefinitionChange::ObjectRemoved(name.value));
                    }
                    Some(def) => {
                        if !add_definition(&mut changes, def) {
                            return SchemaChange::GenericChange;
                        }
                        changes.push(DefinitionChange::ObjectRemoved(name.value));
                    }
                }
            }

            TypeSystemDefinition::ScalarTypeDefinition(ScalarTypeDefinition { name, .. }) => {
                let def = current_map.remove(&name.value);
                match def {
                    None => {
                        changes.push(DefinitionChange::ScalarRemoved(name.value));
                    }
                    Some(TypeSystemDefinition::ScalarTypeDefinition(ScalarTypeDefinition {
                        ..
                    })) => {}
                    Some(def) => {
                        if !add_definition(&mut changes, def) {
                            return SchemaChange::GenericChange;
                        }
                        changes.push(DefinitionChange::ScalarRemoved(name.value));
                    }
                }
            }

            // We skip diffing the following definitions
            TypeSystemDefinition::InterfaceTypeExtension(..)
            | TypeSystemDefinition::ObjectTypeExtension(..)
            | TypeSystemDefinition::SchemaDefinition(..)
            | TypeSystemDefinition::DirectiveDefinition(..)
            | TypeSystemDefinition::SchemaExtension(_)
            | TypeSystemDefinition::EnumTypeExtension(_)
            | TypeSystemDefinition::UnionTypeExtension(_)
            | TypeSystemDefinition::InputObjectTypeExtension(_)
            | TypeSystemDefinition::ScalarTypeExtension(_) => {}
        }
    }

    for (_, definition) in current_map.drain().into_iter() {
        add_definition(&mut changes, definition);
    }

    if changes.is_empty() {
        // The schema has changed, but we currently don't detect directive definition changes,
        // schema definition changes, and we don't parse client extensions.
        // But we can add them later if some of the changes don't require full rebuilds.
        return SchemaChange::GenericChange;
    }

    SchemaChange::DefinitionChanges(changes)
}

pub fn detect_changes(current_text: &[&str], previous_text: &[&str]) -> SchemaChange {
    if current_text == previous_text {
        return SchemaChange::None;
    }

    match (
        parse_schema_document(&current_text.join("\n"), SourceLocationKey::Generated),
        parse_schema_document(&previous_text.join("\n"), SourceLocationKey::Generated),
    ) {
        (Ok(current_schema), Ok(previous_schema)) => {
            diff(current_schema.definitions, previous_schema.definitions)
        }
        (_, _) => SchemaChange::InvalidSchema,
    }
}


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

use intern::string_key::Intern;
use schema::build_schema;
use schema_diff::definitions::*;
use schema_diff::*;

fn diff(current: &str, previous: &str) -> SchemaChange {
    let mut change = detect_changes(&[current], &[previous]);
    sort_change(&mut change);
    change
}

fn is_safe(current: &str, previous: &str) -> bool {
    let schema = build_schema(current).unwrap();
    let change = detect_changes(&[current], &[previous]);
    change.is_safe(&schema, &Default::default())
}

#[test]
fn test_same_text() {
    assert_eq!(
        diff(
            r"
         enum A {
             OK
             NOT_OK
             MAYBE
         }
         #",
            r"
         enum A {
             OK
             NOT_OK
             MAYBE
         }
         #",
        ),
        SchemaChange::None
    );
}

#[test]
fn test_invalid_text() {
    assert_eq!(
        diff(
            r"
         enum A {
             OK
             NOT_OK
             MAYBE

         #",
            r"
         enum A {
             OK
             NOT_OK
             MAYBE
         }
         #",
        ),
        SchemaChange::InvalidSchema
    );
}

#[test]
fn test_add_enum_value() {
    assert_eq!(
        diff(
            r"
         enum A {
             OK
             NOT_OK
             MAYBE
           }
         #",
            r"
         enum A {
             OK
             NOT_OK
           }
         #",
        ),
        SchemaChange::DefinitionChanges(vec![DefinitionChange::EnumChanged { name: "A".intern() }])
    );
}

#[test]
fn test_sort_enum_value() {
    assert_eq!(
        diff(
            r"
         enum A {
             OK
             NOT_OK
             MAYBE
           }
         #",
            r"
         enum A {
             OK
             MAYBE
             NOT_OK
           }
         #",
        ),
        SchemaChange::DefinitionChanges(vec![DefinitionChange::EnumChanged { name: "A".intern() }])
    );
}

#[test]
fn test_remove_enum_value() {
    assert_eq!(
        diff(
            r"
         enum A {
             OK
             NOT_OK
           }
         #",
            r"
         enum A {
             OK
             NOT_OK
             MAYBE
           }
         #",
        ),
        SchemaChange::DefinitionChanges(vec![DefinitionChange::EnumChanged { name: "A".intern() }])
    );
}

#[test]
fn test_add_remove_enum_value() {
    assert_eq!(
        diff(
            r"
         enum A {
             OK
             NOT_OK
             ZUCK
             NOT_ZUCK
           }
         #",
            r"
         enum A {
             OK
             MARK
             NOT_MARK
             NOT_OK
           }
         #",
        ),
        SchemaChange::DefinitionChanges(vec![DefinitionChange::EnumChanged { name: "A".intern() }])
    );
}

#[test]
fn test_add_comment_to_enum() {
    assert_eq!(
        diff(
            r"
            enum A {
                OK
                NOT_OK
            }
         #",
            r"
            # comment
            enum A {
                OK
                NOT_OK
            }
         #",
        ),
        SchemaChange::GenericChange
    );
}

#[test]
fn test_add_remove_enum_type() {
    assert_eq!(
        diff(
            r"
        enum A {
            OK
            NOT_OK
        }
        enum B {
            OK
        }
         #",
            r"
        enum A {
            OK
            NOT_OK
        }
        enum C {
            OK
        }
         #",
        ),
        SchemaChange::DefinitionChanges(vec![
            DefinitionChange::EnumAdded("B".intern()),
            DefinitionChange::EnumRemoved("C".intern()),
        ])
    );
}

#[test]
fn test_add_remove_union_value() {
    assert_eq!(
        diff(
            r"
           type A {
             key: String
           }

           type B {
             key: String
           }

           type C {
             key: String
           }

           type D {
             key: String
           }

           union AB = A | B | D
         #",
            r"
         type A {
             key: String
         }

         type B {
             key: String
         }

         type C {
             key: String
         }

         type D {
             key: String
         }

         union AB = A | C
         #",
        ),
        SchemaChange::DefinitionChanges(vec![DefinitionChange::UnionChanged {
            name: "AB".intern(),
            added: vec!["B".intern(), "D".intern()],
            removed: vec!["C".intern()],
        }])
    );
}

#[test]
fn test_add_remove_union_type() {
    assert_eq!(
        diff(
            r"
           type A {
             key: String
           }

           type B {
             key: String
           }

           type C {
             key: String
           }

           type D {
             key: String
           }

           union union1 = A | B | D
           union union2 = A | B | D
         #",
            r"
         type A {
             key: String
         }

         type B {
             key: String
         }

         type C {
             key: String
         }

         type D {
             key: String
         }

         union union3 = A | B | D
         union union4 = A | B | D
         #",
        ),
        SchemaChange::DefinitionChanges(vec![
            DefinitionChange::UnionAdded("union1".intern()),
            DefinitionChange::UnionAdded("union2".intern()),
            DefinitionChange::UnionRemoved("union3".intern()),
            DefinitionChange::UnionRemoved("union4".intern()),
        ])
    );
}

#[test]
fn test_add_remove_scalar_type() {
    assert_eq!(
        diff(
            r"
        scalar A
        scalar B
         #",
            r"
        scalar A
        scalar C
        scalar D
         #",
        ),
        SchemaChange::DefinitionChanges(vec![
            DefinitionChange::ScalarAdded("B".intern()),
            DefinitionChange::ScalarRemoved("C".intern()),
            DefinitionChange::ScalarRemoved("D".intern()),
        ])
    );
}

#[test]
fn test_add_remove_input_object() {
    assert_eq!(
        diff(
            r"
        input Add {
            value: Float
        }
         #",
            r"
        input Remove {
            name: String
        }
         #",
        ),
        SchemaChange::DefinitionChanges(vec![
            DefinitionChange::InputObjectAdded("Add".intern()),
            DefinitionChange::InputObjectRemoved("Remove".intern()),
        ])
    );
}

#[test]
fn test_add_remove_input_object_fields() {
    assert_eq!(
        diff(
            r"
        input Input {
            add: String
            value: Float
        }
         #",
            r"
        input Input {
            value: Float
            remove: Int
        }
         #",
        ),
        SchemaChange::DefinitionChanges(vec![DefinitionChange::InputObjectChanged {
            name: "Input".intern(),
            added: vec![TypeChange {
                name: "add".intern(),
                type_: Type::Named("String".intern()),
            }],
            removed: vec![TypeChange {
                name: "remove".intern(),
                type_: Type::Named("Int".intern()),
            }],
        },])
    );
}

#[test]
fn test_change_input_object_fields() {
    assert_eq!(
        diff(
            r"
        input Input {
           value: Float
           key: String
        }
         #",
            r"
        input Input {
            value: Float
            key: Int
        }
         #",
        ),
        SchemaChange::DefinitionChanges(vec![DefinitionChange::InputObjectChanged {
            name: "Input".intern(),
            added: vec![TypeChange {
                name: "key".intern(),
                type_: Type::Named("String".intern()),
            }],
            removed: vec![TypeChange {
                name: "key".intern(),
                type_: Type::Named("Int".intern()),
            }],
        },])
    );
}

#[test]
fn test_add_remove_interface() {
    assert_eq!(
        diff(
            r"
        interface Add {
           value: Float
           key: String
        }
         #",
            r"
        interface Remove {
            value: Float
            key: Int
        }
         #",
        ),
        SchemaChange::DefinitionChanges(vec![
            DefinitionChange::InterfaceAdded("Add".intern()),
            DefinitionChange::InterfaceRemoved("Remove".intern()),
        ])
    );
}

#[test]
fn test_add_remove_interface_fields() {
    assert_eq!(
        diff(
            r"
        interface I {
           value: Float
           key: String
        }
         #",
            r"
        interface I {
            value: Float
            key: Int
        }
         #",
        ),
        SchemaChange::DefinitionChanges(vec![DefinitionChange::InterfaceChanged {
            name: "I".intern(),
            added: vec![TypeChange {
                name: "key".intern(),
                type_: Type::Named("String".intern()),
            }],
            removed: vec![TypeChange {
                name: "key".intern(),
                type_: Type::Named("Int".intern()),
            }],
            changed: vec![],
        },])
    );
}

#[test]
fn test_add_remove_object() {
    assert_eq!(
        diff(
            r"
        type Add {
            value: Float
        }
         #",
            r"
        type Remove {
            name: String
        }
         #",
        ),
        SchemaChange::DefinitionChanges(vec![
            DefinitionChange::ObjectAdded("Add".intern()),
            DefinitionChange::ObjectRemoved("Remove".intern()),
        ])
    );
}

#[test]
fn test_change_object_interfaces() {
    assert_eq!(
        diff(
            r"
        type User implements Node {
            id: ID
            value: Float
        }
         #",
            r"
        type User implements Actor {
            id: ID
            value: Float
        }
         #",
        ),
        SchemaChange::DefinitionChanges(vec![DefinitionChange::ObjectChanged {
            name: "User".intern(),
            added: vec![],
            removed: vec![],
            changed: vec![],
            interfaces_removed: vec!["Actor".intern()],
            interfaces_added: vec!["Node".intern()],
        }])
    );
}

#[test]
fn test_change_object_fields() {
    assert_eq!(
        diff(
            r"
        type User implements Actor {
            id: ID
            value: Float
        }
         #",
            r"
        type User implements Actor {
            id: ID!
        }
         #",
        ),
        SchemaChange::DefinitionChanges(vec![DefinitionChange::ObjectChanged {
            name: "User".intern(),
            added: vec![
                TypeChange {
                    name: "id".intern(),
                    type_: Type::Named("ID".intern()),
                },
                TypeChange {
                    name: "value".intern(),
                    type_: Type::Named("Float".intern()),
                },
            ],
            removed: vec![TypeChange {
                name: "id".intern(),
                type_: Type::NonNull(Box::new(Type::Named("ID".intern()))),
            }],
            changed: vec![],
            interfaces_removed: vec![],
            interfaces_added: vec![],
        }])
    );
}

#[test]
fn test_change_object_field_arguments() {
    assert_eq!(
        diff(
            r"
        type User implements Actor {
            key(a: ID): String
            name: String
            user(a: ID): String
        }
         #",
            r"
        type User implements Actor {
            key: String
            name(a: ID!): String
            user(a: ID!): String
        }
         #",
        ),
        SchemaChange::DefinitionChanges(vec![DefinitionChange::ObjectChanged {
            name: "User".intern(),
            added: vec![],
            removed: vec![],
            changed: vec![
                ArgumentChange {
                    name: "key".intern(),
                    added: vec![TypeChange {
                        name: "a".intern(),
                        type_: Type::Named("ID".intern()),
                    }],
                    removed: vec![],
                },
                ArgumentChange {
                    name: "name".intern(),
                    added: vec![],
                    removed: vec![TypeChange {
                        name: "a".intern(),
                        type_: Type::NonNull(Box::new(Type::Named("ID".intern()))),
                    }],
                },
                ArgumentChange {
                    name: "user".intern(),
                    added: vec![TypeChange {
                        name: "a".intern(),
                        type_: Type::Named("ID".intern()),
                    }],
                    removed: vec![TypeChange {
                        name: "a".intern(),
                        type_: Type::NonNull(Box::new(Type::Named("ID".intern()))),
                    }],
                }
            ],
            interfaces_removed: vec![],
            interfaces_added: vec![],
        }])
    );
}

#[test]
fn test_change_type_input_object() {
    assert_eq!(
        diff(
            r"
        input User {
            value: Float
        }
         #",
            r"
        type User {
            name: String
        }
         #",
        ),
        SchemaChange::DefinitionChanges(vec![
            DefinitionChange::InputObjectAdded("User".intern()),
            DefinitionChange::ObjectRemoved("User".intern()),
        ])
    );
}

#[test]
fn test_change_type_object_interface() {
    assert_eq!(
        diff(
            r"
        type User {
            value: Float
        }
         #",
            r"
        interface User {
            name: String
        }
         #",
        ),
        SchemaChange::DefinitionChanges(vec![
            DefinitionChange::InterfaceRemoved("User".intern()),
            DefinitionChange::ObjectAdded("User".intern()),
        ])
    );
}

#[test]
fn test_change_type_enum_scalar() {
    assert_eq!(
        diff(
            r"
        scalar A
         #",
            r"
        enum A {
            OK
            NOT_OK
        }
         #",
        ),
        SchemaChange::DefinitionChanges(vec![
            DefinitionChange::EnumRemoved("A".intern()),
            DefinitionChange::ScalarAdded("A".intern()),
        ])
    );
}

#[test]
fn test_change_type_enum_union() {
    assert_eq!(
        diff(
            r"
        type A {
            id: ID
        }
        enum B {
            OK
            NOT_OK
        }
        union C = A
         #",
            r"
        type A {
            id: ID
        }
        enum C {
            OK
            NOT_OK
        }
        union B = A
         #",
        ),
        SchemaChange::DefinitionChanges(vec![
            DefinitionChange::EnumAdded("B".intern()),
            DefinitionChange::EnumRemoved("C".intern()),
            DefinitionChange::UnionAdded("C".intern()),
            DefinitionChange::UnionRemoved("B".intern()),
        ])
    );
}

#[test]
fn test_add_object_without_id() {
    assert!(is_safe(
        r"
            type A {
                key: String
            }
            type B {
                key: String
            }
            #",
        r"
            type A {
                key: String
            }
            #"
    ))
}

#[test]
fn test_change_object_interface() {
    assert!(!is_safe(
        r"
            type A implements B {
                key: String
            }
            interface B {
                id: ID!
            }
            #",
        r"
            type A {
                key: String
            }
            interface B {
                id: ID!
            }
            #"
    ))
}

#[test]
fn test_add_object_with_id_node_interface() {
    assert!(is_safe(
        r"
            type A {
                key: String
            }
            type B implements Node {
                id: ID
                key: String
            }
            interface Node {
                id: ID!
            }
            #",
        r"
            type A {
                key: String
            }
            interface Node {
                id: ID!
            }
            #"
    ))
}

#[test]
fn test_object_special_field_added() {
    assert!(is_safe(
        r"
            type A {
                key: String
                foo: String # regular field is okay
            }
        #",
        r"
            type A {
                key: String
            }
        #",
    ));
    assert!(!is_safe(
        r"
            type A {
                key: String
                id: String # id field is breaking
            }
        #",
        r"
            type A {
                key: String
            }
        #",
    ));
    assert!(!is_safe(
        r"
            type A {
                key: String
                js: String # js field is breaking
            }
        #",
        r"
            type A {
                key: String
            }
        #",
    ));
}

#[test]
fn test_add_type_with_id_actor_interface() {
    assert!(!is_safe(
        r"
            type A {
                key: String
            }
            type B implements Actor {
                id: ID
                key: String
            }
            interface Actor {
                name: String
            }
            #",
        r"
            type A {
                key: String
            }
            interface Actor {
                name: String
            }
            #"
    ))
}

#[test]
fn test_add_optional_field_arg() {
    assert!(is_safe(
        r"
            type A {
                key(a: ID): String
            }

            #",
        r"
            type A {
                key: String
            }
            #"
    ))
}

#[test]
fn test_add_required_field_arg() {
    assert!(!is_safe(
        r"
            type A {
                key(a: ID!): String
            }

            #",
        r"
            type A {
                key: String
            }
            #"
    ))
}

#[test]
fn test_remove_field_arg() {
    assert!(!is_safe(
        r"
            type A {
                key: String
            }

            #",
        r"
            type A {
                key(a: ID): String
            }
            #"
    ))
}

#[test]
fn test_add_safe_types() {
    assert!(is_safe(
        r"
            type A {
                key: String
            }
            interface B {
                key: String
            }
            type C {
                key: String
            }
            union D = A
            enum E {
                OK
                NOT_OK
            }
            input F {
                key: String
            }
            scalar Mark
            #",
        r"
            type A {
                key: String
            }
            #"
    ))
}

#[test]
fn test_unimplemented_changes() {
    assert!(!is_safe(
        r"
            type A {
                key: String
            }
            extend type A {
                name: String
            }
            #",
        r"
            type A {
                key: String
            }
            #"
    ))
}

fn sort_change(change: &mut SchemaChange) {
    if let SchemaChange::DefinitionChanges(changes) = change {
        changes.sort();
        for c in changes {
            match c {
                DefinitionChange::UnionChanged {
                    ref mut added,
                    ref mut removed,
                    ..
                } => {
                    added.sort();
                    removed.sort();
                }
                DefinitionChange::InputObjectChanged {
                    ref mut added,
                    ref mut removed,
                    ..
                } => {
                    added.sort_by_key(|item| item.name);
                    removed.sort_by_key(|item| item.name);
                }
                DefinitionChange::InterfaceChanged {
                    ref mut added,
                    ref mut removed,
                    ..
                } => {
                    added.sort_by_key(|item| item.name);
                    removed.sort_by_key(|item| item.name);
                }
                DefinitionChange::ObjectChanged {
                    ref mut added,
                    ref mut removed,
                    ref mut changed,
                    ref mut interfaces_added,
                    ref mut interfaces_removed,
                    ..
                } => {
                    added.sort_by_key(|item| item.name);
                    removed.sort_by_key(|item| item.name);
                    changed.sort_by_key(|item| item.name);
                    interfaces_added.sort();
                    interfaces_removed.sort();
                }
                _ => {}
            }
        }
    }
}


/*
 * 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 crate::SchemaDocumentation;

pub struct CombinedSchemaDocumentation<
    TPrimary: SchemaDocumentation,
    TSecondary: SchemaDocumentation,
> {
    primary: TPrimary,
    secondary: TSecondary,
}

impl<TPrimary: SchemaDocumentation, TSecondary: SchemaDocumentation> SchemaDocumentation
    for CombinedSchemaDocumentation<TPrimary, TSecondary>
{
    fn get_type_description(&self, type_name: &str) -> Option<&str> {
        self.primary
            .get_type_description(type_name)
            .or_else(|| self.secondary.get_type_description(type_name))
    }

    fn get_field_description(&self, type_name: &str, field_name: &str) -> Option<&str> {
        self.primary
            .get_field_description(type_name, field_name)
            .or_else(|| self.secondary.get_field_description(type_name, field_name))
    }

    fn get_field_argument_description(
        &self,
        type_name: &str,
        field_name: &str,
        argument_name: &str,
    ) -> Option<&str> {
        self.primary
            .get_field_argument_description(type_name, field_name, argument_name)
            .or_else(|| {
                self.secondary
                    .get_field_argument_description(type_name, field_name, argument_name)
            })
    }
    fn get_hack_source(&self, type_name: &str) -> Option<&str> {
        self.primary
            .get_hack_source(type_name)
            .or_else(|| self.secondary.get_hack_source(type_name))
    }
    fn get_field_hack_source(&self, type_name: &str, field_name: &str) -> Option<&str> {
        self.primary
            .get_field_hack_source(type_name, field_name)
            .or_else(|| self.secondary.get_field_hack_source(type_name, field_name))
    }
}

impl<'a, TPrimary: SchemaDocumentation, TSecondary: SchemaDocumentation>
    CombinedSchemaDocumentation<TPrimary, TSecondary>
{
    pub fn new(primary: TPrimary, secondary: TSecondary) -> Self {
        Self { primary, secondary }
    }
}


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

mod combined_schema_documentation;
mod sdl_schema_impl;

use std::sync::Arc;

pub use combined_schema_documentation::CombinedSchemaDocumentation;

pub trait SchemaDocumentationLoader<T: SchemaDocumentation>: Send + Sync {
    fn get_schema_documentation(&self, schema_name: &str) -> Arc<T>;
}

pub trait SchemaDocumentation: Send + Sync {
    fn get_type_description(&self, _type_name: &str) -> Option<&str> {
        None
    }
    fn get_field_description(&self, _type_name: &str, _field_name: &str) -> Option<&str> {
        None
    }
    fn get_field_argument_description(
        &self,
        _type_name: &str,
        _field_name: &str,
        _argument_name: &str,
    ) -> Option<&str> {
        None
    }
    fn get_hack_source(&self, _type_name: &str) -> Option<&str> {
        None
    }
    fn get_field_hack_source(&self, _type_name: &str, _field_name: &str) -> Option<&str> {
        None
    }
}

// This can probably be implemented more generically for AsRef<TSchemaDocumentation>
impl<TSchemaDocumentation: SchemaDocumentation> SchemaDocumentation for Arc<TSchemaDocumentation> {
    fn get_type_description(&self, type_name: &str) -> Option<&str> {
        self.as_ref().get_type_description(type_name)
    }

    fn get_field_description(&self, type_name: &str, field_name: &str) -> Option<&str> {
        self.as_ref().get_field_description(type_name, field_name)
    }

    fn get_field_argument_description(
        &self,
        type_name: &str,
        field_name: &str,
        argument_name: &str,
    ) -> Option<&str> {
        self.as_ref()
            .get_field_argument_description(type_name, field_name, argument_name)
    }
    fn get_hack_source(&self, type_name: &str) -> Option<&str> {
        self.as_ref().get_hack_source(type_name)
    }
    fn get_field_hack_source(&self, type_name: &str, field_name: &str) -> Option<&str> {
        self.as_ref().get_field_hack_source(type_name, field_name)
    }
}

impl<TSchemaDocumentation: SchemaDocumentation> SchemaDocumentation
    for Option<TSchemaDocumentation>
{
    fn get_type_description(&self, type_name: &str) -> Option<&str> {
        self.as_ref()
            .and_then(|s| s.get_type_description(type_name))
    }

    fn get_field_description(&self, type_name: &str, field_name: &str) -> Option<&str> {
        self.as_ref()
            .and_then(|s| s.get_field_description(type_name, field_name))
    }

    fn get_field_argument_description(
        &self,
        type_name: &str,
        field_name: &str,
        argument_name: &str,
    ) -> Option<&str> {
        self.as_ref()
            .and_then(|s| s.get_field_argument_description(type_name, field_name, argument_name))
    }
    fn get_hack_source(&self, type_name: &str) -> Option<&str> {
        self.as_ref().and_then(|s| s.get_hack_source(type_name))
    }
    fn get_field_hack_source(&self, type_name: &str, field_name: &str) -> Option<&str> {
        self.as_ref()
            .and_then(|s| s.get_field_hack_source(type_name, field_name))
    }
}


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

use intern::string_key::Intern;
use intern::string_key::StringKey;
use intern::Lookup;
use schema::Field;
use schema::SDLSchema;
use schema::Schema;
use schema::Type;

use crate::SchemaDocumentation;

impl SchemaDocumentation for SDLSchema {
    fn get_type_description(&self, type_name: &str) -> Option<&str> {
        self.get_type(type_name.intern())
            .and_then(|type_| get_description_from_type(type_, self))
            .map(|string_key| string_key.lookup())
    }

    fn get_field_description(&self, type_name: &str, field_name: &str) -> Option<&str> {
        let field_name_string_key = field_name.intern();
        self.get_type(type_name.intern())
            .and_then(|type_| get_field_from_type(type_, self, field_name_string_key))
            .and_then(|field| field.description)
            .map(|string_key| string_key.lookup())
    }

    fn get_field_argument_description(
        &self,
        type_name: &str,
        field_name: &str,
        argument_name: &str,
    ) -> Option<&str> {
        let field_name_string_key = field_name.intern();
        let argument_name_string_key = argument_name.intern();
        self.get_type(type_name.intern())
            .and_then(|type_| get_field_from_type(type_, self, field_name_string_key))
            .and_then(|field| {
                field
                    .arguments
                    .iter()
                    .find(|argument| argument.name.0 == argument_name_string_key)
            })
            .and_then(|argument| argument.description)
            .map(|string_key| string_key.lookup())
    }

    fn get_hack_source(&self, type_name: &str) -> Option<&str> {
        self.get_type(type_name.intern())
            .and_then(|type_| get_hack_source_from_type(type_, self))
            .map(|string_key| string_key.lookup())
    }

    fn get_field_hack_source(&self, type_name: &str, field_name: &str) -> Option<&str> {
        let field_name_string_key = field_name.intern();
        self.get_type(type_name.intern())
            .and_then(|type_| get_field_from_type(type_, self, field_name_string_key))
            .and_then(|field| field.hack_source)
            .map(|string_key| string_key.lookup())
    }
}

fn get_description_from_type(type_: Type, schema: &SDLSchema) -> Option<StringKey> {
    match type_ {
        Type::Enum(id) => schema.enum_(id).description,
        Type::InputObject(id) => schema.input_object(id).description,
        Type::Interface(id) => schema.interface(id).description,
        Type::Object(id) => schema.object(id).description,
        Type::Scalar(id) => schema.scalar(id).description,
        Type::Union(id) => schema.union(id).description,
    }
}

fn get_field_from_type(type_: Type, schema: &SDLSchema, field_name: StringKey) -> Option<&Field> {
    let fields = match type_ {
        Type::Enum(_id) => None,
        // Note: InputObjects have Arguments (akin to fields). These Arguments cannot have descriptions.
        Type::InputObject(_id) => None,
        Type::Interface(id) => Some(&schema.interface(id).fields),
        Type::Object(id) => Some(&schema.object(id).fields),
        Type::Scalar(_id) => None,
        Type::Union(_id) => None,
    }?;
    fields.iter().find_map(|field_id| {
        let field = schema.field(*field_id);
        if field.name.item == field_name {
            Some(field)
        } else {
            None
        }
    })
}

fn get_hack_source_from_type(type_: Type, schema: &SDLSchema) -> Option<StringKey> {
    match type_ {
        Type::Enum(id) => schema.enum_(id).hack_source,
        Type::InputObject(id) => schema.input_object(id).hack_source,
        Type::Interface(id) => schema.interface(id).hack_source,
        Type::Object(id) => schema.object(id).hack_source,
        Type::Scalar(id) => schema.scalar(id).hack_source,
        Type::Union(id) => schema.union(id).hack_source,
    }
}