use futures_util::stream::{Stream, StreamExt};
use gqlrs::*;

#[tokio::test]
pub async fn test_generic_object() {
    struct MyObj<T> {
        value: T,
    }

    #[Object(name = "MyObjI32")]
    impl MyObj<i32> {
        async fn value(&self) -> i32 {
            self.value
        }
    }

    #[Object(name = "MyObjBool")]
    impl MyObj<bool> {
        async fn value(&self) -> bool {
            self.value
        }
    }

    struct Query;

    #[Object]
    impl Query {
        async fn obj_i32(&self) -> MyObj<i32> {
            MyObj { value: 100 }
        }

        async fn obj_bool(&self) -> MyObj<bool> {
            MyObj { value: true }
        }
    }

    let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
    let query = r#"{
            objI32 { value }
            objBool { value }
        }"#
    .to_owned();
    assert_eq!(
        schema.execute(&query).await.into_result().unwrap().data,
        value!({
            "objI32": {"value": 100},
            "objBool": {"value": true},
        })
    );
}

#[tokio::test]
pub async fn test_input_object_generic() {
    #[derive(InputObject)]
    #[graphql(
        concrete(name = "IntEqualityFilter", params(i32)),
        concrete(name = "StringEqualityFilter", params(String))
    )]
    struct EqualityFilter<T: InputType> {
        equals: Option<T>,
        not_equals: Option<T>,
    }

    assert_eq!(EqualityFilter::<i32>::type_name(), "IntEqualityFilter");
    assert_eq!(
        EqualityFilter::<String>::type_name(),
        "StringEqualityFilter"
    );

    struct Query;

    #[Object]
    impl Query {
        async fn q1(&self, input: EqualityFilter<i32>) -> i32 {
            input.equals.unwrap_or_default() + input.not_equals.unwrap_or_default()
        }

        async fn q2(&self, input: EqualityFilter<String>) -> String {
            input.equals.unwrap_or_default() + &input.not_equals.unwrap_or_default()
        }
    }

    let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
    let query = r#"{
            q1(input: { equals: 7, notEquals: 8 } )
            q2(input: { equals: "ab", notEquals: "cd" } )
        }"#;
    assert_eq!(
        schema.execute(query).await.into_result().unwrap().data,
        value!({
            "q1": 15,
            "q2": "abcd",
        })
    );

    assert_eq!(
        schema
            .execute(
                r#"{ __type(name: "IntEqualityFilter") { inputFields { name type { name } } } }"#
            )
            .await
            .into_result()
            .unwrap()
            .data,
        value!({
            "__type": {
                "inputFields": [
                    {"name": "equals", "type": { "name": "Int" } },
                    {"name": "notEquals", "type": { "name": "Int" } },
                ]
            }
        })
    );

    assert_eq!(
        schema
            .execute(r#"{ __type(name: "Query") { fields { name args { name type { kind ofType { name } } } } } }"#)
            .await
            .into_result()
            .unwrap()
            .data,
        value!({
            "__type": {
                "fields": [
                    {
                        "name": "q1",
                        "args": [{
                            "name": "input",
                            "type": {
                                "kind": "NON_NULL",
                                "ofType": { "name": "IntEqualityFilter" },
                            },
                        }]
                    },
                    {
                        "name": "q2",
                        "args": [{
                            "name": "input",
                            "type": {
                                "kind": "NON_NULL",
                                "ofType": { "name": "StringEqualityFilter" },
                            },
                        }],
                    }
                ]
            }
        })
    );
}

#[tokio::test]
pub async fn test_generic_simple_object() {
    #[derive(SimpleObject)]
    #[graphql(concrete(name = "MyObjIntString", params(i32, String)))]
    #[graphql(concrete(name = "MyObji16u8", params(i16, u8)))]
    struct MyObj<A: OutputType, B: OutputType> {
        a: A,
        b: B,
    }

    struct Query;

    #[Object]
    impl Query {
        async fn q1(&self) -> MyObj<i32, String> {
            MyObj {
                a: 100,
                b: "abc".to_string(),
            }
        }

        async fn q2(&self) -> MyObj<i16, u8> {
            MyObj { a: 100, b: 28 }
        }
    }

    let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
    let query = "{ q1 { a b } q2 { a b } }";
    assert_eq!(
        schema.execute(query).await.into_result().unwrap().data,
        value!({
            "q1": {
                "a": 100,
                "b": "abc",
            },
            "q2": {
                "a": 100,
                "b": 28,
            }
        })
    );

    assert_eq!(
        schema
            .execute(r#"{ __type(name: "MyObjIntString") { fields { name type { kind ofType { name } } } } }"#)
            .await
            .into_result()
            .unwrap()
            .data,
        value!({
            "__type": {
                "fields": [
                    {
                        "name": "a",
                        "type": {
                            "kind": "NON_NULL",
                            "ofType": { "name": "Int" },
                        },
                    },
                    {
                        "name": "b",
                        "type": {
                            "kind": "NON_NULL",
                            "ofType": { "name": "String" },
                        },
                    },
                ]
            }
        })
    );

    assert_eq!(
        schema
            .execute(
                r#"{ __type(name: "Query") { fields { name type { kind ofType { name } } } } }"#
            )
            .await
            .into_result()
            .unwrap()
            .data,
        value!({
            "__type": {
                "fields": [
                    {
                        "name": "q1",
                        "type": {
                            "kind": "NON_NULL",
                            "ofType": { "name": "MyObjIntString" },
                        },
                    },
                    {
                        "name": "q2",
                        "type": {
                            "kind": "NON_NULL",
                            "ofType": { "name": "MyObji16u8" },
                        },
                    },
                ]
            }
        })
    );
}

#[tokio::test]
pub async fn test_generic_subscription() {
    struct MySubscription<T> {
        values: Vec<T>,
    }

    #[Subscription]
    impl<T: OutputType> MySubscription<T>
    where
        T: Clone + Send + Sync + Unpin,
    {
        async fn values(&self) -> Result<impl Stream<Item = T> + '_> {
            Ok(asynk_strim::stream_fn(move |mut yielder| async move {
                for value in self.values.iter().cloned() {
                    yielder.yield_item(value).await;
                }
            }))
        }
    }

    struct Query;

    #[Object]
    impl Query {
        async fn dummy(&self) -> bool {
            false
        }
    }

    let schema = Schema::new(Query, EmptyMutation, MySubscription { values: vec![1, 2] });
    {
        let mut stream = schema
            .execute_stream("subscription { values }")
            .map(|resp| resp.into_result().unwrap().data);
        for i in 1..=2 {
            assert_eq!(value!({ "values": i }), stream.next().await.unwrap());
        }
        assert!(stream.next().await.is_none());
    }
}

#[tokio::test]
pub async fn test_concrete_object() {
    struct GbObject<A, B>(A, B);

    #[Object(
        concrete(name = "Obj_i32i16", params(i32, i16)),
        concrete(name = "Obj_f32f64", params(f32, f64))
    )]
    impl<A: OutputType, B: OutputType> GbObject<A, B> {
        async fn a(&self) -> &A {
            &self.0
        }

        async fn b(&self) -> &B {
            &self.1
        }
    }

    assert_eq!(GbObject::<i32, i16>::type_name(), "Obj_i32i16");
    assert_eq!(GbObject::<f32, f64>::type_name(), "Obj_f32f64");

    struct Query;

    #[Object]
    impl Query {
        async fn a(&self) -> GbObject<i32, i16> {
            GbObject(10, 20)
        }

        async fn b(&self) -> GbObject<f32, f64> {
            GbObject(88.0, 99.0)
        }
    }

    let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
    assert_eq!(
        schema
            .execute("{ a { __typename a b } b { __typename a b } }")
            .await
            .into_result()
            .unwrap()
            .data,
        value!({
            "a": {
                "__typename": "Obj_i32i16",
                "a": 10,
                "b": 20,
            },
            "b": {
                "__typename": "Obj_f32f64",
                "a": 88.0,
                "b": 99.0,
            }
        })
    );
}

#[tokio::test]
pub async fn test_concrete_object_with_lifetime() {
    #[derive(SimpleObject)]
    #[graphql(concrete(name = "Bar0", params(i32)))]
    #[graphql(concrete(name = "Bar1", params(i16)))]
    struct Foo<'a, T>
    where
        T: Sync + OutputType + 'a,
    {
        data: &'a T,
    }

    struct Query {
        value1: i32,
        value2: i16,
    }

    #[Object]
    impl Query {
        async fn a(&self) -> Foo<'_, i32> {
            Foo { data: &self.value1 }
        }

        async fn b(&self) -> Foo<'_, i16> {
            Foo { data: &self.value2 }
        }

        async fn static_a(&self) -> Foo<'static, i32> {
            Foo { data: &100 }
        }

        async fn static_b(&self) -> Foo<'static, i32> {
            Foo { data: &200 }
        }
    }

    let schema = Schema::new(
        Query {
            value1: 88,
            value2: 99,
        },
        EmptyMutation,
        EmptySubscription,
    );

    assert_eq!(
        schema
            .execute(
                r#"{ 
                a { data } 
                b { data }
                staticA { data }
                staticB { data }
            }"#
            )
            .await
            .into_result()
            .unwrap()
            .data,
        value!({
            "a": { "data": 88 },
            "b": { "data": 99 },
            "staticA": { "data": 100 },
            "staticB": { "data": 200 },
        })
    );
}

/// Regression test for https://github.com/async-graphql/async-graphql/issues/1451
/// Stack overflow when a generic type's ComplexObject method returns the same
/// generic type.
#[tokio::test]
pub async fn test_generic_complex_object_returns_self() {
    #[derive(SimpleObject)]
    #[graphql(complex)]
    #[graphql(concrete(name = "NodeInt", params(i32)))]
    #[graphql(concrete(name = "NodeString", params(String)))]
    struct Node<T: OutputType> {
        value: T,
    }

    #[ComplexObject]
    impl Node<i32> {
        async fn cloned(&self) -> Node<i32> {
            Node { value: self.value }
        }
    }

    #[ComplexObject]
    impl Node<String> {
        async fn cloned(&self) -> Node<String> {
            Node {
                value: self.value.clone(),
            }
        }
    }

    struct Query;

    #[Object]
    impl Query {
        async fn node_i32(&self) -> Node<i32> {
            Node { value: 42 }
        }

        async fn node_string(&self) -> Node<String> {
            Node {
                value: "hello".to_string(),
            }
        }
    }

    // Schema::new must not stack-overflow
    let schema = Schema::new(Query, EmptyMutation, EmptySubscription);

    let query = r#"{
        nodeI32 { value cloned { value } }
        nodeString { value cloned { value } }
    }"#;
    assert_eq!(
        schema.execute(query).await.into_result().unwrap().data,
        value!({
            "nodeI32": { "value": 42, "cloned": { "value": 42 } },
            "nodeString": { "value": "hello", "cloned": { "value": "hello" } },
        })
    );
}
