#![cfg(feature = "dataloader")]
#![cfg(feature = "tokio")]
#![allow(unreachable_code)]
#![allow(dead_code)]
#![allow(clippy::diverging_sub_expression)]

use std::{collections::HashMap, convert::Infallible};

use gqlrs::{
    dataloader::{DataLoader, Loader},
    runtime::{TokioSpawner, TokioTimer},
    *,
};

#[tokio::test]
pub async fn test_nested_key() {
    #[derive(InputObject)]
    struct MyInputA {
        a: i32,
        b: i32,
        c: MyInputB,
    }

    #[derive(InputObject)]
    struct MyInputB {
        v: i32,
    }

    assert_eq!(MyInputB::federation_fields().as_deref(), Some("{ v }"));
    assert_eq!(
        MyInputA::federation_fields().as_deref(),
        Some("{ a b c { v } }")
    );

    struct Query;

    #[derive(SimpleObject)]
    struct MyObj {
        a: i32,
        b: i32,
        c: i32,
    }

    #[Object]
    impl Query {
        #[graphql(entity)]
        async fn find_obj(&self, input: MyInputA) -> MyObj {
            MyObj {
                a: input.a,
                b: input.b,
                c: input.c.v,
            }
        }
    }

    let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
    let query = r#"{
            _entities(representations: [{__typename: "MyObj", input: {a: 1, b: 2, c: { v: 3 }}}]) {
                __typename
                ... on MyObj {
                    a b c
                }
            }
        }"#;
    assert_eq!(
        schema.execute(query).await.into_result().unwrap().data,
        value!({
            "_entities": [
                {"__typename": "MyObj", "a": 1, "b": 2, "c": 3},
            ]
        })
    );
}

#[tokio::test]
pub async fn test_federation() {
    struct User {
        id: ID,
    }

    #[Object(extends)]
    impl User {
        #[graphql(external)]
        async fn id(&self) -> &ID {
            &self.id
        }

        async fn reviews(&self) -> Vec<Review> {
            todo!()
        }
    }

    struct Review;

    #[Object]
    impl Review {
        async fn body(&self) -> String {
            todo!()
        }

        async fn author(&self) -> User {
            todo!()
        }

        async fn product(&self) -> Product {
            todo!()
        }
    }

    struct Product {
        upc: String,
    }

    #[Object(extends)]
    impl Product {
        #[graphql(external)]
        async fn upc(&self) -> &str {
            &self.upc
        }

        async fn reviews(&self) -> Vec<Review> {
            todo!()
        }
    }

    struct Query;

    #[Object]
    impl Query {
        #[graphql(entity)]
        async fn find_user_by_id(&self, id: ID) -> User {
            User { id }
        }

        #[graphql(entity)]
        async fn find_product_by_upc(&self, upc: String) -> Product {
            Product { upc }
        }
    }

    let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
    let query = r#"{
            _entities(representations: [{__typename: "Product", upc: "B00005N5PF"}]) {
                __typename
                ... on Product {
                    upc
                }
            }
        }"#;
    assert_eq!(
        schema.execute(query).await.into_result().unwrap().data,
        value!({
            "_entities": [
                {"__typename": "Product", "upc": "B00005N5PF"},
            ]
        })
    );
}

#[tokio::test]
pub async fn test_find_entity_with_context() {
    struct MyLoader;

    #[cfg_attr(
        all(feature = "boxed-trait", not(feature = "no_send")),
        async_trait::async_trait
    )]
    #[cfg_attr(all(feature = "boxed-trait", feature = "no_send"), async_trait::async_trait(?Send))]
    impl Loader<ID, MyObj> for MyLoader {
        type Error = Infallible;

        async fn load(&self, keys: &[ID]) -> Result<HashMap<ID, MyObj>, Self::Error> {
            Ok(keys
                .iter()
                .filter(|id| id.as_str() != "999")
                .map(|id| {
                    (
                        id.clone(),
                        MyObj {
                            id: id.clone(),
                            value: 999,
                        },
                    )
                })
                .collect())
        }
    }

    #[derive(Clone, SimpleObject)]
    struct MyObj {
        id: ID,
        value: i32,
    }

    struct Query;

    #[Object]
    impl Query {
        #[graphql(entity)]
        async fn find_user_by_id(&self, ctx: &Context<'_>, id: ID) -> FieldResult<MyObj> {
            let loader = ctx.data::<DataLoader<MyLoader>>().unwrap();
            loader
                .load_one(id)
                .await
                .unwrap()
                .ok_or_else(|| "Not found".into())
        }
    }

    let schema = Schema::build(Query, EmptyMutation, EmptySubscription)
        .data(DataLoader::new(
            MyLoader,
            TokioSpawner::current(),
            TokioTimer::default(),
        ))
        .finish();
    let query = r#"{
            _entities(representations: [
                {__typename: "MyObj", id: "1"},
                {__typename: "MyObj", id: "2"},
                {__typename: "MyObj", id: "3"},
                {__typename: "MyObj", id: "4"}
            ]) {
                __typename
                ... on MyObj {
                    id
                    value
                }
            }
        }"#;
    assert_eq!(
        schema.execute(query).await.into_result().unwrap().data,
        value!({
            "_entities": [
                {"__typename": "MyObj", "id": "1", "value": 999 },
                {"__typename": "MyObj", "id": "2", "value": 999 },
                {"__typename": "MyObj", "id": "3", "value": 999 },
                {"__typename": "MyObj", "id": "4", "value": 999 },
            ]
        })
    );

    let query = r#"{
            _entities(representations: [
                {__typename: "MyObj", id: "999"}
            ]) {
                __typename
                ... on MyObj {
                    id
                    value
                }
            }
        }"#;
    assert_eq!(
        schema.execute(query).await.into_result().unwrap_err(),
        vec![ServerError {
            message: "Not found".to_string(),
            source: None,
            locations: vec![Pos {
                line: 2,
                column: 13
            }],
            path: vec![PathSegment::Field("_entities".to_owned())],
            extensions: None,
        }]
    );
}

#[tokio::test]
pub async fn test_entity_union() {
    #[derive(SimpleObject)]
    struct MyObj {
        a: i32,
    }

    struct Query;

    #[Object]
    impl Query {
        #[graphql(entity)]
        async fn find_obj(&self, _id: i32) -> MyObj {
            todo!()
        }
    }

    let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
    let query = r#"{
            __type(name: "_Entity") { possibleTypes { name } }
        }"#;
    assert_eq!(
        schema.execute(query).await.into_result().unwrap().data,
        value!({
            "__type": {
                "possibleTypes": [
                    {"name": "MyObj"},
                ]
            }
        })
    );
}

#[tokio::test]
pub async fn test_entity_shareable() {
    #[derive(SimpleObject)]
    struct MyObjFieldShareable {
        #[graphql(shareable)]
        field_shareable_a: i32,
    }

    #[derive(SimpleObject)]
    #[graphql(shareable)]
    struct MyObjShareable {
        a: i32,
    }

    struct Query;

    #[Object(extends)]
    impl Query {
        #[graphql(entity)]
        async fn find_obj_field_shareable(&self, _id: i32) -> MyObjFieldShareable {
            todo!()
        }
        #[graphql(entity)]
        async fn find_obj_shareable(&self, _id: i32) -> MyObjShareable {
            todo!()
        }
    }

    let schema_sdl = Schema::new(Query, EmptyMutation, EmptySubscription)
        .sdl_with_options(SDLExportOptions::new().federation());
    assert!(schema_sdl.contains("fieldShareableA: Int! @shareable"),);

    assert!(schema_sdl.contains(r#"MyObjShareable @key(fields: "id") @shareable"#),);
}

#[tokio::test]
pub async fn test_field_override_directive() {
    #[derive(SimpleObject)]
    struct MyObjFieldOverride {
        #[graphql(override_from = "AnotherSubgraph")]
        field_override_a: i32,
    }

    struct Query;

    #[Object(extends)]
    impl Query {
        #[graphql(entity)]
        async fn find_obj_field_override(&self, _id: i32) -> MyObjFieldOverride {
            todo!()
        }
    }

    let schema_sdl = Schema::new(Query, EmptyMutation, EmptySubscription)
        .sdl_with_options(SDLExportOptions::new().federation());
    assert!(schema_sdl.contains("fieldOverrideA: Int! @override(from: \"AnotherSubgraph\")"),);
}

#[tokio::test]
pub async fn test_entity_inaccessible() {
    struct MyCustomObjInaccessible;

    #[Object(inaccessible)]
    impl MyCustomObjInaccessible {
        async fn a(&self) -> i32 {
            todo!()
        }

        #[graphql(inaccessible)]
        async fn custom_object_inaccessible(&self) -> i32 {
            todo!()
        }
    }

    #[derive(SimpleObject)]
    struct MyObjFieldInaccessible {
        #[graphql(inaccessible)]
        obj_field_inaccessible_a: i32,
    }

    #[derive(SimpleObject)]
    #[graphql(inaccessible)]
    struct MyObjInaccessible {
        a: i32,
    }

    #[derive(InputObject)]
    struct MyInputObjFieldInaccessible {
        #[graphql(inaccessible)]
        input_field_inaccessible_a: i32,
    }

    #[derive(InputObject)]
    #[graphql(inaccessible)]
    struct MyInputObjInaccessible {
        a: i32,
    }

    #[derive(Enum, PartialEq, Eq, Copy, Clone)]
    enum MyEnumVariantInaccessible {
        #[graphql(inaccessible)]
        OptionAInaccessible,
        OptionB,
        OptionC,
    }

    #[derive(Enum, PartialEq, Eq, Copy, Clone)]
    #[graphql(inaccessible)]
    enum MyEnumInaccessible {
        OptionA,
        OptionB,
        OptionC,
    }

    #[derive(SimpleObject)]
    struct MyInterfaceObjA {
        inaccessible_interface_value: String,
    }

    #[derive(SimpleObject)]
    #[graphql(inaccessible)]
    struct MyInterfaceObjB {
        inaccessible_interface_value: String,
    }

    #[derive(Interface)]
    #[graphql(field(name = "inaccessible_interface_value", ty = "String", inaccessible))]
    #[graphql(inaccessible)]
    enum MyInterfaceInaccessible {
        MyInterfaceObjA(MyInterfaceObjA),
        MyInterfaceObjB(MyInterfaceObjB),
    }

    #[derive(Union)]
    #[graphql(inaccessible)]
    enum MyUnionInaccessible {
        MyInterfaceObjA(MyInterfaceObjA),
        MyInterfaceObjB(MyInterfaceObjB),
    }

    struct MyNumberInaccessible(i32);

    #[Scalar(inaccessible)]
    impl ScalarType for MyNumberInaccessible {
        fn parse(_value: Value) -> InputValueResult<Self> {
            todo!()
        }

        fn to_value(&self) -> Value {
            todo!()
        }
    }

    struct Query;

    #[Object(extends)]
    impl Query {
        #[graphql(entity)]
        async fn find_obj_field_inaccessible(&self, _id: i32) -> MyObjFieldInaccessible {
            todo!()
        }

        #[graphql(entity)]
        async fn find_obj_inaccessible(&self, _id: i32) -> MyObjInaccessible {
            todo!()
        }

        async fn enum_variant_inaccessible(&self, _id: i32) -> MyEnumVariantInaccessible {
            todo!()
        }

        async fn enum_inaccessible(&self, _id: i32) -> MyEnumInaccessible {
            todo!()
        }

        #[graphql(inaccessible)]
        async fn inaccessible_field(&self, _id: i32) -> i32 {
            todo!()
        }

        async fn inaccessible_argument(&self, #[graphql(inaccessible)] _id: i32) -> i32 {
            todo!()
        }

        async fn inaccessible_interface(&self) -> MyInterfaceInaccessible {
            todo!()
        }

        async fn inaccessible_union(&self) -> MyUnionInaccessible {
            todo!()
        }

        async fn inaccessible_scalar(&self) -> MyNumberInaccessible {
            todo!()
        }

        async fn inaccessible_input_field(&self, _value: MyInputObjFieldInaccessible) -> i32 {
            todo!()
        }

        async fn inaccessible_input(&self, _value: MyInputObjInaccessible) -> i32 {
            todo!()
        }

        async fn inaccessible_custom_object(&self) -> MyCustomObjInaccessible {
            todo!()
        }
    }

    let schema_sdl = Schema::new(Query, EmptyMutation, EmptySubscription)
        .sdl_with_options(SDLExportOptions::new().federation());

    // FIELD_DEFINITION
    assert!(schema_sdl.contains("inaccessibleField(id: Int!): Int! @inaccessible"));
    assert!(schema_sdl.contains("objFieldInaccessibleA: Int! @inaccessible"));
    assert!(schema_sdl.contains("inaccessibleInterfaceValue: String! @inaccessible"));
    assert!(schema_sdl.contains("customObjectInaccessible: Int! @inaccessible"));
    // INTERFACE
    assert!(schema_sdl.contains("interface MyInterfaceInaccessible @inaccessible"));
    // OBJECT
    assert!(schema_sdl.contains("type MyCustomObjInaccessible @inaccessible"));
    assert!(schema_sdl.contains(r#"type MyObjInaccessible @key(fields: "id") @inaccessible"#));
    assert!(
        schema_sdl
            .contains("type MyInterfaceObjB implements MyInterfaceInaccessible @inaccessible")
    );
    // UNION
    assert!(schema_sdl.contains("union MyUnionInaccessible @inaccessible ="));
    // ARGUMENT_DEFINITION
    assert!(schema_sdl.contains("inaccessibleArgument(id: Int! @inaccessible): Int!"));
    // SCALAR
    assert!(schema_sdl.contains("scalar MyNumberInaccessible @inaccessible"));
    // ENUM
    assert!(schema_sdl.contains("enum MyEnumInaccessible @inaccessible"));
    // ENUM_VALUE
    assert!(schema_sdl.contains("OPTION_A_INACCESSIBLE @inaccessible"));
    // INPUT_OBJECT
    assert!(schema_sdl.contains("input MyInputObjInaccessible @inaccessible"));
    // INPUT_FIELD_DEFINITION
    assert!(schema_sdl.contains("inputFieldInaccessibleA: Int! @inaccessible"));
    // no trailing spaces
    assert!(!schema_sdl.contains(" \n"));

    // compare to expected schema
    let path = std::path::Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
        .join("tests/schemas/test_entity_inaccessible.schema.graphql");
    let expected_schema = std::fs::read_to_string(&path).unwrap();
    if schema_sdl != expected_schema {
        std::fs::write(path, schema_sdl).unwrap();
        panic!("schema was not up-to-date. rerun")
    }
}

#[tokio::test]
pub async fn test_link_directive() {
    struct User {
        id: ID,
    }

    #[Object(extends)]
    impl User {
        #[graphql(external)]
        async fn id(&self) -> &ID {
            &self.id
        }

        async fn reviews(&self) -> Vec<Review> {
            todo!()
        }
    }

    struct Review;

    #[Object]
    impl Review {
        async fn body(&self) -> String {
            todo!()
        }

        async fn author(&self) -> User {
            todo!()
        }

        async fn product(&self) -> Product {
            todo!()
        }
    }

    struct Product {
        upc: String,
    }

    #[Object(extends)]
    impl Product {
        #[graphql(external)]
        async fn upc(&self) -> &str {
            &self.upc
        }

        async fn reviews(&self) -> Vec<Review> {
            todo!()
        }
    }

    struct Query;

    #[Object]
    impl Query {
        #[graphql(entity)]
        async fn find_user_by_id(&self, id: ID) -> User {
            User { id }
        }

        #[graphql(entity)]
        async fn find_product_by_upc(&self, upc: String) -> Product {
            Product { upc }
        }
    }

    let schema_sdl = Schema::build(Query, EmptyMutation, EmptySubscription)
        .finish()
        .sdl_with_options(SDLExportOptions::new().federation());

    let path = std::path::Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
        .join("tests/schemas/test_fed2_link.schema.graphqls");
    let expected_schema = std::fs::read_to_string(&path).unwrap();
    if schema_sdl != expected_schema {
        std::fs::write(path, schema_sdl).unwrap();
        panic!("schema was not up-to-date. verify changes and re-run if correct.")
    }
}

#[tokio::test]
pub async fn test_entity_tag() {
    struct MyCustomObjTagged;

    #[Object(
        tag = "tagged",
        tag = "object",
        tag = "with",
        tag = "multiple",
        tag = "tags"
    )]
    impl MyCustomObjTagged {
        async fn a(&self) -> i32 {
            todo!()
        }

        #[graphql(tag = "tagged_custom_object_field")]
        async fn custom_object_tagged(&self) -> i32 {
            todo!()
        }
    }

    #[derive(SimpleObject)]
    struct MyObjFieldTagged {
        #[graphql(tag = "tagged_field")]
        obj_field_tagged_a: i32,
    }

    #[derive(SimpleObject)]
    #[graphql(tag = "tagged_simple_object")]
    struct MyObjTagged {
        a: i32,
    }

    #[derive(InputObject)]
    struct MyInputObjFieldTagged {
        #[graphql(tag = "tagged_input_object_field")]
        input_field_tagged_a: i32,
    }

    #[derive(InputObject)]
    #[graphql(tag = "input_object_tag")]
    struct MyInputObjTagged {
        a: i32,
    }

    #[derive(Enum, PartialEq, Eq, Copy, Clone)]
    enum MyEnumVariantTagged {
        #[graphql(tag = "tagged_enum_option")]
        OptionATagged,
        OptionB,
        OptionC,
    }

    #[derive(Enum, PartialEq, Eq, Copy, Clone)]
    #[graphql(tag = "tagged_num")]
    enum MyEnumTagged {
        OptionA,
        OptionB,
        OptionC,
    }

    #[derive(SimpleObject)]
    struct MyInterfaceObjA {
        tagged_interface_value: String,
    }

    #[derive(SimpleObject)]
    #[graphql(tag = "interface_object")]
    struct MyInterfaceObjB {
        tagged_interface_value: String,
    }

    #[derive(Interface)]
    #[graphql(field(
        name = "tagged_interface_value",
        ty = "String",
        tag = "tagged_interface_field"
    ))]
    #[graphql(tag = "tagged_interface")]
    enum MyInterfaceTagged {
        MyInterfaceObjA(MyInterfaceObjA),
        MyInterfaceObjB(MyInterfaceObjB),
    }

    #[derive(Union)]
    #[graphql(tag = "tagged_union")]
    enum MyUnionTagged {
        MyInterfaceObjA(MyInterfaceObjA),
        MyInterfaceObjB(MyInterfaceObjB),
    }

    struct MyNumberTagged(i32);

    #[Scalar(tag = "tagged_scalar")]
    impl ScalarType for MyNumberTagged {
        fn parse(_value: Value) -> InputValueResult<Self> {
            todo!()
        }

        fn to_value(&self) -> Value {
            todo!()
        }
    }

    struct Query;

    #[Object(extends)]
    impl Query {
        #[graphql(entity)]
        async fn find_obj_field_tagged(&self, _id: i32) -> MyObjFieldTagged {
            todo!()
        }

        #[graphql(entity)]
        async fn find_obj_tagged(&self, _id: i32) -> MyObjTagged {
            todo!()
        }

        async fn enum_variant_tagged(&self, _id: i32) -> MyEnumVariantTagged {
            todo!()
        }

        async fn enum_tagged(&self, _id: i32) -> MyEnumTagged {
            todo!()
        }

        #[graphql(tag = "tagged_\"field\"")]
        async fn tagged_field(&self, _id: i32) -> i32 {
            todo!()
        }

        async fn tagged_argument(&self, #[graphql(tag = "tagged_argument")] _id: i32) -> i32 {
            todo!()
        }

        async fn tagged_interface(&self) -> MyInterfaceTagged {
            todo!()
        }

        async fn tagged_union(&self) -> MyUnionTagged {
            todo!()
        }

        async fn tagged_scalar(&self) -> MyNumberTagged {
            todo!()
        }

        async fn tagged_input_field(&self, _value: MyInputObjFieldTagged) -> i32 {
            todo!()
        }

        async fn tagged_input(&self, _value: MyInputObjTagged) -> i32 {
            todo!()
        }

        async fn tagged_custom_object(&self) -> MyCustomObjTagged {
            todo!()
        }
    }

    let schema_sdl = Schema::new(Query, EmptyMutation, EmptySubscription)
        .sdl_with_options(SDLExportOptions::new().federation());

    // FIELD_DEFINITION
    assert!(schema_sdl.contains(r#"taggedField(id: Int!): Int! @tag(name: "tagged_\"field\"")"#));
    assert!(schema_sdl.contains(r#"objFieldTaggedA: Int! @tag(name: "tagged_field")"#));
    assert!(
        schema_sdl
            .contains(r#"taggedInterfaceValue: String! @tag(name: "tagged_interface_field")"#)
    );
    assert!(
        schema_sdl.contains(r#"customObjectTagged: Int! @tag(name: "tagged_custom_object_field")"#)
    );
    // INTERFACE
    assert!(schema_sdl.contains(r#"interface MyInterfaceTagged @tag(name: "tagged_interface")"#));
    // OBJECT
    assert!(schema_sdl.contains(r#"type MyCustomObjTagged @tag(name: "tagged") @tag(name: "object") @tag(name: "with") @tag(name: "multiple") @tag(name: "tags") {"#));
    assert!(
        schema_sdl.contains(
            r#"type MyObjTagged @key(fields: "id") @tag(name: "tagged_simple_object") {"#
        )
    );
    assert!(schema_sdl.contains(
        r#"type MyInterfaceObjB implements MyInterfaceTagged @tag(name: "interface_object")"#
    ));
    // UNION
    assert!(schema_sdl.contains(r#"union MyUnionTagged @tag(name: "tagged_union") ="#));
    // ARGUMENT_DEFINITION
    assert!(schema_sdl.contains(r#"taggedArgument(id: Int! @tag(name: "tagged_argument")): Int!"#));
    // SCALAR
    assert!(schema_sdl.contains(r#"scalar MyNumberTagged @tag(name: "tagged_scalar")"#));
    // ENUM
    assert!(schema_sdl.contains(r#"enum MyEnumTagged @tag(name: "tagged_num")"#));
    // ENUM_VALUE
    assert!(schema_sdl.contains(r#"OPTION_A_TAGGED @tag(name: "tagged_enum_option")"#));
    // INPUT_OBJECT
    assert!(schema_sdl.contains(r#"input MyInputObjTagged @tag(name: "input_object_tag")"#));
    // INPUT_FIELD_DEFINITION
    assert!(
        schema_sdl.contains(r#"inputFieldTaggedA: Int! @tag(name: "tagged_input_object_field")"#)
    );
    // no trailing spaces
    assert!(!schema_sdl.contains(" \n"));

    // compare to expected schema
    let path = std::path::Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
        .join("tests/schemas/test_entity_tag.schema.graphql");
    let expected_schema = std::fs::read_to_string(&path).unwrap();
    if schema_sdl != expected_schema {
        std::fs::write(path, schema_sdl).unwrap();
        panic!("schema was not up-to-date. rerun")
    }
}

#[tokio::test]
pub async fn test_entity_requires_scopes() {
    struct MyCustomObjRequiresScoped;

    #[Object(
        requires_scopes = "requires scopes with",
        requires_scopes = "multiple scopes"
    )]
    impl MyCustomObjRequiresScoped {
        async fn a(&self) -> i32 {
            todo!()
        }

        #[graphql(requires_scopes = "custom:field")]
        async fn custom_object_requires_scoped(&self) -> i32 {
            todo!()
        }
    }

    #[derive(SimpleObject)]
    struct MyObjFieldRequiresScoped {
        #[graphql(
            requires_scopes = "fielda:scoped read:all",
            requires_scopes = "read:others"
        )]
        obj_field_requires_scoped_a: i32,
    }

    #[derive(SimpleObject)]
    #[graphql(requires_scopes = "myobj:read")]
    struct MyObjRequiresScoped {
        a: i32,
    }

    #[derive(Enum, PartialEq, Eq, Copy, Clone)]
    #[graphql(requires_scopes = "enum:read")]
    enum MyEnumRequiresScoped {
        OptionA,
        OptionB,
        OptionC,
    }

    #[derive(SimpleObject)]
    struct MyInterfaceObjA {
        requires_scoped_interface_value: String,
    }

    #[derive(SimpleObject)]
    #[graphql(requires_scopes = "objB:read")]
    struct MyInterfaceObjB {
        requires_scoped_interface_value: String,
    }

    #[derive(Interface)]
    #[graphql(field(
        name = "requires_scoped_interface_value",
        ty = "String",
        requires_scopes = "read:interface"
    ))]
    #[graphql(requires_scopes = "read:all")]
    enum MyInterfaceRequiresScoped {
        MyInterfaceObjA(MyInterfaceObjA),
        MyInterfaceObjB(MyInterfaceObjB),
    }

    struct MyNumberRequiresScoped(i32);

    #[Scalar(requires_scopes = "scalar:read")]
    impl ScalarType for MyNumberRequiresScoped {
        fn parse(_value: Value) -> InputValueResult<Self> {
            todo!()
        }

        fn to_value(&self) -> Value {
            todo!()
        }
    }

    #[derive(SimpleObject)]
    #[graphql(complex)]
    struct MyComplexObjRequiresScoped {
        complex_a: i32,
    }

    #[ComplexObject]
    impl MyComplexObjRequiresScoped {
        #[graphql(requires_scopes = "read:complex")]
        async fn complex_b(&self) -> i32 {
            self.complex_a
        }
    }

    struct Query;

    #[Object(extends)]
    impl Query {
        #[graphql(requires_scopes = "scoped:field")]
        async fn requires_scoped_field(&self, _id: i32) -> i32 {
            todo!()
        }

        #[graphql(entity)]
        async fn find_obj_field_requires_scoped(&self, _id: i32) -> MyObjFieldRequiresScoped {
            todo!()
        }

        #[graphql(entity)]
        async fn find_obj_requires_scoped(&self, _id: i32) -> MyObjRequiresScoped {
            todo!()
        }

        async fn requires_scoped_enum(&self, _id: i32) -> MyEnumRequiresScoped {
            todo!()
        }

        async fn requires_scoped_interface(&self) -> MyInterfaceRequiresScoped {
            todo!()
        }

        async fn requires_scoped_scalar(&self) -> MyNumberRequiresScoped {
            todo!()
        }

        async fn requires_scoped_custom_object(&self) -> MyCustomObjRequiresScoped {
            todo!()
        }

        async fn requires_scoped_complex_object(&self) -> MyComplexObjRequiresScoped {
            MyComplexObjRequiresScoped { complex_a: 10 }
        }
    }

    let schema_sdl = Schema::new(Query, EmptyMutation, EmptySubscription)
        .sdl_with_options(SDLExportOptions::new().federation());

    // FIELD_DEFINITION
    assert!(schema_sdl.contains(
        r#"requiresScopedField(id: Int!): Int! @requiresScopes(scopes: [["scoped:field"]])"#
    ));
    assert!(schema_sdl.contains(
        r#"objFieldRequiresScopedA: Int! @requiresScopes(scopes: [["fielda:scoped", "read:all"], ["read:others"]])"#
    ));
    assert!(schema_sdl.contains(
        r#"requiresScopedInterfaceValue: String! @requiresScopes(scopes: [["read:interface"]])"#
    ));
    assert!(schema_sdl.contains(
        r#"customObjectRequiresScoped: Int! @requiresScopes(scopes: [["custom:field"]])"#
    ));
    assert!(schema_sdl.contains(r#"complexB: Int! @requiresScopes(scopes: [["read:complex"]])"#));
    // INTERFACE
    assert!(schema_sdl.contains(
        r#"interface MyInterfaceRequiresScoped @requiresScopes(scopes: [["read:all"]])"#
    ));
    // OBJECT
    assert!(schema_sdl
        .contains(r#"type MyCustomObjRequiresScoped @requiresScopes(scopes: [["requires", "scopes", "with"], ["multiple", "scopes"]]) {"#));
    assert!(schema_sdl.contains(
        r#"type MyObjRequiresScoped @key(fields: "id") @requiresScopes(scopes: [["myobj:read"]]) {"#
    ));
    assert!(schema_sdl.contains(
        r#"type MyInterfaceObjB implements MyInterfaceRequiresScoped @requiresScopes(scopes: [["objB:read"]])"#
    ));
    // SCALAR
    assert!(
        schema_sdl.contains(
            r#"scalar MyNumberRequiresScoped @requiresScopes(scopes: [["scalar:read"]])"#
        )
    );
    // ENUM
    assert!(
        schema_sdl
            .contains(r#"enum MyEnumRequiresScoped @requiresScopes(scopes: [["enum:read"]])"#)
    );
    // no trailing spaces
    assert!(!schema_sdl.contains(" \n"));
}

#[tokio::test]
pub async fn test_interface_object() {
    #[derive(SimpleObject)]
    struct VariantA {
        pub id: i32,
    }

    #[derive(Interface)]
    #[graphql(field(name = "id", ty = "&i32"))]
    enum MyInterface {
        VariantA(VariantA),
    }

    #[derive(SimpleObject)]
    #[graphql(interface_object)]
    struct MyInterfaceObject1 {
        pub id: i32,
    }

    struct MyInterfaceObject2;

    #[Object(interface_object)]
    impl MyInterfaceObject2 {
        pub async fn id(&self) -> i32 {
            todo!()
        }
    }

    struct Query;

    #[Object(extends)]
    impl Query {
        #[graphql(entity)]
        async fn my_interface(&self, _id: i32) -> MyInterface {
            todo!()
        }

        #[graphql(entity)]
        async fn my_interface_object1(&self, _id: i32) -> MyInterfaceObject1 {
            todo!()
        }

        #[graphql(entity)]
        async fn my_interface_object2(&self, _id: i32) -> MyInterfaceObject2 {
            todo!()
        }
    }

    let schema_sdl = Schema::new(Query, EmptyMutation, EmptySubscription)
        .sdl_with_options(SDLExportOptions::new().federation());

    // Interface with @key directive
    assert!(schema_sdl.contains("interface MyInterface @key(fields: \"id\")"));

    // Object with @interfaceObject directive
    assert!(schema_sdl.contains("type MyInterfaceObject1 @key(fields: \"id\") @interfaceObject"));
    assert!(schema_sdl.contains("type MyInterfaceObject2 @key(fields: \"id\") @interfaceObject"));
}

/// Tests Federation v2.3 entity interfaces: when an interface has `@key`,
/// the implementing object types must appear in the `_Entity` union,
/// not the interface itself (GraphQL unions can only contain object types).
#[tokio::test]
pub async fn test_entity_interface_expands_to_implementors() {
    #[derive(SimpleObject)]
    struct ObjA {
        id: u64,
    }

    #[derive(SimpleObject)]
    struct ObjB {
        id: u64,
    }

    #[derive(Interface)]
    #[graphql(field(name = "id", ty = "&u64"))]
    enum NodeInterface {
        ObjA(ObjA),
        ObjB(ObjB),
    }

    struct Query;

    #[Object(extends)]
    impl Query {
        #[graphql(entity)]
        async fn node_interface(&self, id: u64) -> NodeInterface {
            NodeInterface::ObjB(ObjB { id })
        }

        #[graphql(entity)]
        async fn obj_a(&self, id: u64) -> ObjA {
            ObjA { id: id + 1 }
        }
    }

    let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
    let query = r#"{
            __type(name: "_Entity") { possibleTypes { name } }
        }"#;
    let result = schema.execute(query).await.into_result().unwrap().data;

    // The _Entity union should contain the implementing object types,
    // NOT the interface name itself
    assert_eq!(
        result,
        value!({
            "__type": {
                "possibleTypes": [
                    {"name": "ObjA"},
                    {"name": "ObjB"},
                ]
            }
        })
    );

    let query = r#"{
            _entities(representations: [
                {__typename: "ObjA", id: 42},
                {__typename: "ObjB", id: 7},
            ]) {
                __typename
                ... on ObjA { id }
                ... on ObjB { id }
            }
        }"#;
    let result = schema.execute(query).await.into_result().unwrap().data;

    assert_eq!(
        result,
        value!({
            "_entities": [
                {
                    "__typename": "ObjA",
                    "id": 43,
                },
                {
                    "__typename": "ObjB",
                    "id": 7,
                },
            ]
        })
    );
}

#[tokio::test]
pub async fn test_nested_entity_interface_expands_to_concrete_implementors() {
    #[derive(SimpleObject)]
    struct Company {
        id: ID,
    }

    #[derive(SimpleObject)]
    struct Organization {
        id: ID,
    }

    #[derive(Interface)]
    #[graphql(field(name = "id", ty = "&ID"))]
    enum EntityInterface {
        Company(Company),
        Organization(Organization),
    }

    #[derive(Interface)]
    #[graphql(field(name = "id", ty = "&ID"))]
    enum NodeInterface {
        EntityInterface(EntityInterface),
    }

    struct Query;

    #[Object(extends)]
    impl Query {
        #[graphql(entity)]
        async fn node_interface(&self, id: ID) -> NodeInterface {
            NodeInterface::EntityInterface(EntityInterface::Company(Company { id }))
        }
    }

    let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
    let query = r#"{
            __type(name: "_Entity") { possibleTypes { name } }
        }"#;
    let result = schema.execute(query).await.into_result().unwrap().data;

    assert_eq!(
        result,
        value!({
            "__type": {
                "possibleTypes": [
                    {"name": "Company"},
                    {"name": "Organization"},
                ]
            }
        })
    );
}

#[tokio::test]
pub async fn test_unresolvable_entity() {
    #[derive(SimpleObject)]
    struct ResolvableObject {
        id: i32,
    }

    #[derive(SimpleObject)]
    #[graphql(unresolvable = "id")]
    struct SimpleExplicitUnresolvable {
        id: i32,
    }

    #[derive(SimpleObject)]
    #[graphql(unresolvable)]
    struct SimpleImplicitUnresolvable {
        a: i32,
        #[graphql(skip)]
        _skipped: bool,
    }

    struct ExplicitUnresolvable;

    #[Object(unresolvable = "id1 id2")]
    impl ExplicitUnresolvable {
        async fn id1(&self) -> i32 {
            todo!()
        }

        async fn id2(&self) -> i32 {
            todo!()
        }
    }

    struct ImplicitUnresolvable;

    #[Object(unresolvable)]
    impl ImplicitUnresolvable {
        async fn a(&self) -> &'static str {
            todo!()
        }

        async fn b(&self) -> bool {
            todo!()
        }

        #[graphql(skip)]
        async fn _skipped(&self) {}
    }

    struct Query;

    #[Object]
    impl Query {
        async fn simple_explicit_reference(&self, _id: i32) -> SimpleExplicitUnresolvable {
            todo!()
        }

        async fn simple_implicit_reference(&self, _a: i32) -> SimpleImplicitUnresolvable {
            todo!()
        }

        async fn explicit_reference(&self, _id1: i32, _id2: i32) -> ExplicitUnresolvable {
            todo!()
        }

        async fn implicit_unresolvable(&self, _a: String, _b: bool) -> ImplicitUnresolvable {
            todo!()
        }

        #[graphql(entity)]
        async fn object_entity(&self, _id: i32) -> ResolvableObject {
            todo!()
        }
    }

    let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
    let schema_sdl = schema.sdl_with_options(SDLExportOptions::new().federation());

    assert!(schema_sdl.contains(r#"type ResolvableObject @key(fields: "id")"#));
    assert!(
        schema_sdl
            .contains(r#"type SimpleExplicitUnresolvable @key(fields: "id", resolvable: false)"#)
    );
    assert!(
        schema_sdl
            .contains(r#"type SimpleImplicitUnresolvable @key(fields: "a", resolvable: false)"#)
    );
    assert!(
        schema_sdl
            .contains(r#"type ExplicitUnresolvable @key(fields: "id1 id2", resolvable: false)"#)
    );
    assert!(
        schema_sdl.contains(r#"type ImplicitUnresolvable @key(fields: "a b", resolvable: false)"#)
    );

    let query = r#"{
            __type(name: "_Entity") { possibleTypes { name } }
        }"#;
    assert_eq!(
        schema.execute(query).await.into_result().unwrap().data,
        value!({
            "__type": {
                "possibleTypes": [
                    {"name": "ResolvableObject"},
                ]
            }
        })
    );
}

#[tokio::test]
pub async fn test_entity_guard() {
    #[derive(Eq, PartialEq, Copy, Clone)]
    enum Role {
        Admin,
        Guest,
    }

    struct RoleGuard {
        role: Role,
    }

    impl RoleGuard {
        fn new(role: Role) -> Self {
            Self { role }
        }
    }

    #[cfg_attr(
        all(feature = "boxed-trait", not(feature = "no_send")),
        async_trait::async_trait
    )]
    #[cfg_attr(all(feature = "boxed-trait", feature = "no_send"), async_trait::async_trait(?Send))]
    impl Guard for RoleGuard {
        async fn check(&self, ctx: &Context<'_>) -> Result<()> {
            if ctx.data_opt::<Role>() == Some(&self.role) {
                Ok(())
            } else {
                Err("Forbidden".into())
            }
        }
    }

    #[derive(SimpleObject)]
    struct MyObj {
        id: ID,
        value: i32,
    }

    struct Query;

    #[Object]
    impl Query {
        #[graphql(entity, guard = "RoleGuard::new(Role::Admin)")]
        async fn find_obj_by_id(&self, id: ID) -> MyObj {
            MyObj { id, value: 42 }
        }
    }

    let schema = Schema::new(Query, EmptyMutation, EmptySubscription);

    // Test that guard allows access when role matches
    let query = r#"{
            _entities(representations: [{__typename: "MyObj", id: "1"}]) {
                __typename
                ... on MyObj {
                    id
                    value
                }
            }
        }"#;
    assert_eq!(
        schema
            .execute(Request::new(query).data(Role::Admin))
            .await
            .into_result()
            .unwrap()
            .data,
        value!({
            "_entities": [
                {"__typename": "MyObj", "id": "1", "value": 42},
            ]
        })
    );

    // Test that guard denies access when role does not match
    assert_eq!(
        schema
            .execute(Request::new(query).data(Role::Guest))
            .await
            .into_result()
            .unwrap_err(),
        vec![ServerError {
            message: "Forbidden".to_string(),
            source: None,
            locations: vec![Pos {
                line: 2,
                column: 13,
            }],
            path: vec![PathSegment::Field("_entities".to_owned())],
            extensions: None,
        }]
    );
}

#[tokio::test]
pub async fn test_batch_entity_resolver() {
    use std::sync::{Arc, Mutex};

    #[derive(Clone, SimpleObject)]
    struct User {
        id: ID,
        name: String,
    }

    struct Query {
        /// Tracks how many times the batch resolver was called.
        batch_call_count: Arc<Mutex<usize>>,
    }

    #[Object]
    impl Query {
        #[graphql(entity_batch)]
        async fn find_users_by_ids(&self, #[graphql(name = "id")] ids: Vec<ID>) -> Vec<User> {
            *self.batch_call_count.lock().unwrap() += 1;
            ids.into_iter()
                .map(|id| User {
                    name: format!("User {}", id.as_str()),
                    id,
                })
                .collect()
        }
    }

    let batch_count = Arc::new(Mutex::new(0usize));
    let schema = Schema::build(
        Query {
            batch_call_count: batch_count.clone(),
        },
        EmptyMutation,
        EmptySubscription,
    )
    .finish();

    // Test with multiple representations of the same type
    let query = r#"{
            _entities(representations: [
                {__typename: "User", id: "1"},
                {__typename: "User", id: "2"},
                {__typename: "User", id: "3"}
            ]) {
                __typename
                ... on User {
                    id
                    name
                }
            }
        }"#;
    let result = schema.execute(query).await.into_result().unwrap();
    assert_eq!(
        result.data,
        value!({
            "_entities": [
                {"__typename": "User", "id": "1", "name": "User 1"},
                {"__typename": "User", "id": "2", "name": "User 2"},
                {"__typename": "User", "id": "3", "name": "User 3"},
            ]
        })
    );
    // Batch resolver should have been called exactly once
    assert_eq!(*batch_count.lock().unwrap(), 1);
}

#[tokio::test]
pub async fn test_batch_entity_with_individual_fallback() {
    #[derive(Clone, SimpleObject)]
    struct User {
        id: ID,
        name: String,
    }

    #[derive(Clone, SimpleObject)]
    struct Product {
        upc: String,
        title: String,
    }

    struct Query;

    #[Object]
    impl Query {
        #[graphql(entity_batch)]
        async fn find_users_by_ids(&self, #[graphql(name = "id")] ids: Vec<ID>) -> Vec<User> {
            ids.into_iter()
                .map(|id| User {
                    name: format!("User {}", id.as_str()),
                    id,
                })
                .collect()
        }

        #[graphql(entity)]
        async fn find_product_by_upc(&self, upc: String) -> Product {
            Product {
                upc,
                title: "Test Product".to_string(),
            }
        }
    }

    let schema = Schema::new(Query, EmptyMutation, EmptySubscription);

    // Test with mixed entity types
    let query = r#"{
            _entities(representations: [
                {__typename: "User", id: "1"},
                {__typename: "Product", upc: "B001"},
                {__typename: "User", id: "2"}
            ]) {
                __typename
                ... on User {
                    id
                    name
                }
                ... on Product {
                    upc
                    title
                }
            }
        }"#;
    let result = schema.execute(query).await.into_result().unwrap();
    assert_eq!(
        result.data,
        value!({
            "_entities": [
                {"__typename": "User", "id": "1", "name": "User 1"},
                {"__typename": "Product", "upc": "B001", "title": "Test Product"},
                {"__typename": "User", "id": "2", "name": "User 2"},
            ]
        })
    );
}
