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

#[derive(Eq, PartialEq, Copy, Clone)]
enum Role {
    Admin,
    Guest,
}

pub 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::<Role>().ok() == Some(&self.role) {
            Ok(())
        } else {
            Err("Forbidden".into())
        }
    }
}

struct Username(String);

struct UserGuard<'a> {
    username: &'a str,
}

impl<'a> UserGuard<'a> {
    fn new(username: &'a str) -> Self {
        Self { username }
    }
}

#[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 UserGuard<'_> {
    async fn check(&self, ctx: &Context<'_>) -> Result<()> {
        if ctx.data::<Username>().ok().map(|name| name.0.as_str()) == Some(self.username) {
            Ok(())
        } else {
            Err("Forbidden".into())
        }
    }
}

#[tokio::test]
pub async fn test_guard_simple_rule() {
    #[derive(SimpleObject)]
    struct Query {
        #[graphql(guard = "RoleGuard::new(Role::Admin)")]
        value: i32,
    }

    struct Subscription;

    #[Subscription]
    impl Subscription {
        #[graphql(guard = "RoleGuard::new(Role::Admin)")]
        async fn values(&self) -> impl Stream<Item = i32> {
            futures_util::stream::iter(vec![1, 2, 3])
        }
    }

    let schema = Schema::new(Query { value: 10 }, EmptyMutation, Subscription);

    let query = "{ value }";
    assert_eq!(
        schema
            .execute(Request::new(query).data(Role::Admin))
            .await
            .data,
        value!({"value": 10})
    );

    let query = "{ value }";
    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: 1, column: 3 }],
            path: vec![PathSegment::Field("value".to_owned())],
            extensions: None,
        }]
    );

    assert_eq!(
        schema
            .execute_stream(Request::new("subscription { values }").data(Role::Admin))
            .map(|item| item.data)
            .collect::<Vec<_>>()
            .await,
        vec![
            value! ({"values": 1}),
            value! ({"values": 2}),
            value! ({"values": 3})
        ]
    );

    assert_eq!(
        schema
            .execute_stream(Request::new("subscription { values }").data(Role::Guest))
            .next()
            .await
            .unwrap()
            .errors,
        vec![ServerError {
            message: "Forbidden".to_string(),
            source: None,
            locations: vec![Pos {
                line: 1,
                column: 16
            }],
            path: vec![PathSegment::Field("values".to_owned())],
            extensions: None,
        }]
    );
}

#[tokio::test]
pub async fn test_guard_and_operator() {
    #[derive(SimpleObject)]
    struct Query {
        #[graphql(guard = r#"RoleGuard::new(Role::Admin).and(UserGuard::new("test"))"#)]
        value: i32,
    }

    let schema = Schema::new(Query { value: 10 }, EmptyMutation, EmptySubscription);

    let query = "{ value }";
    assert_eq!(
        schema
            .execute(
                Request::new(query)
                    .data(Role::Admin)
                    .data(Username("test".to_string()))
            )
            .await
            .data,
        value!({"value": 10})
    );

    let query = "{ value }";
    assert_eq!(
        schema
            .execute(
                Request::new(query)
                    .data(Role::Guest)
                    .data(Username("test".to_string()))
            )
            .await
            .into_result()
            .unwrap_err(),
        vec![ServerError {
            message: "Forbidden".to_string(),
            source: None,
            locations: vec![Pos { line: 1, column: 3 }],
            path: vec![PathSegment::Field("value".to_owned())],
            extensions: None,
        }]
    );

    let query = "{ value }";
    assert_eq!(
        schema
            .execute(
                Request::new(query)
                    .data(Role::Admin)
                    .data(Username("test1".to_string()))
            )
            .await
            .into_result()
            .unwrap_err(),
        vec![ServerError {
            message: "Forbidden".to_string(),
            source: None,
            locations: vec![Pos { line: 1, column: 3 }],
            path: vec![PathSegment::Field("value".to_owned())],
            extensions: None,
        }]
    );

    let query = "{ value }";
    assert_eq!(
        schema
            .execute(
                Request::new(query)
                    .data(Role::Guest)
                    .data(Username("test1".to_string()))
            )
            .await
            .into_result()
            .unwrap_err(),
        vec![ServerError {
            message: "Forbidden".to_string(),
            source: None,
            locations: vec![Pos { line: 1, column: 3 }],
            path: vec![PathSegment::Field("value".to_owned())],
            extensions: None,
        }]
    );
}

#[tokio::test]
pub async fn test_guard_or_operator() {
    #[derive(SimpleObject)]
    struct Query {
        #[graphql(guard = r#"RoleGuard::new(Role::Admin).or(UserGuard::new("test"))"#)]
        value: i32,
    }

    let schema = Schema::new(Query { value: 10 }, EmptyMutation, EmptySubscription);

    let query = "{ value }";
    assert_eq!(
        schema
            .execute(
                Request::new(query)
                    .data(Role::Admin)
                    .data(Username("test".to_string()))
            )
            .await
            .data,
        value!({"value": 10})
    );

    let query = "{ value }";
    assert_eq!(
        schema
            .execute(
                Request::new(query)
                    .data(Role::Guest)
                    .data(Username("test".to_string()))
            )
            .await
            .data,
        value!({"value": 10})
    );

    let query = "{ value }";
    assert_eq!(
        schema
            .execute(
                Request::new(query)
                    .data(Role::Admin)
                    .data(Username("test1".to_string()))
            )
            .await
            .data,
        value!({"value": 10})
    );

    let query = "{ value }";
    assert_eq!(
        schema
            .execute(
                Request::new(query)
                    .data(Role::Guest)
                    .data(Username("test1".to_string()))
            )
            .await
            .into_result()
            .unwrap_err(),
        vec![ServerError {
            message: "Forbidden".to_string(),
            source: None,
            locations: vec![Pos { line: 1, column: 3 }],
            path: vec![PathSegment::Field("value".to_owned())],
            extensions: None,
        }]
    );
}

#[tokio::test]
pub async fn test_guard_use_params() {
    struct EqGuard {
        expect: i32,
        actual: i32,
    }

    impl EqGuard {
        fn new(expect: i32, actual: i32) -> Self {
            Self { expect, actual }
        }
    }

    #[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 EqGuard {
        async fn check(&self, _ctx: &Context<'_>) -> Result<()> {
            if self.expect != self.actual {
                Err("Forbidden".into())
            } else {
                Ok(())
            }
        }
    }

    struct Query;

    #[Object]
    impl Query {
        #[graphql(guard = "EqGuard::new(100, value)")]
        async fn get(&self, value: i32) -> i32 {
            value
        }
    }

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

    assert_eq!(
        schema
            .execute(Request::new("{ get(value: 100) }"))
            .await
            .into_result()
            .unwrap()
            .data,
        value!({"get": 100})
    );

    assert_eq!(
        schema
            .execute(Request::new("{ get(value: 99) }"))
            .await
            .into_result()
            .unwrap_err(),
        vec![ServerError {
            message: "Forbidden".to_string(),
            source: None,
            locations: vec![Pos { line: 1, column: 3 }],
            path: vec![PathSegment::Field("get".to_owned())],
            extensions: None,
        }]
    );
}

#[tokio::test]
pub async fn test_guard_on_simple_object() {
    #[derive(SimpleObject)]
    #[graphql(guard = "RoleGuard::new(Role::Admin)")]
    struct Query {
        value: i32,
    }

    let schema = Schema::new(Query { value: 100 }, EmptyMutation, EmptySubscription);

    let query = "{ value }";
    assert_eq!(
        schema
            .execute(Request::new(query).data(Role::Admin))
            .await
            .data,
        value!({"value": 100})
    );

    let query = "{ value }";
    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: 1, column: 3 }],
            path: vec![PathSegment::Field("value".to_owned())],
            extensions: None,
        }]
    );
}

#[tokio::test]
pub async fn test_guard_on_simple_object_field() {
    #[derive(SimpleObject)]
    #[graphql]
    struct Query {
        #[graphql(guard = "RoleGuard::new(Role::Admin)")]
        value: i32,
    }

    let schema = Schema::new(Query { value: 100 }, EmptyMutation, EmptySubscription);

    let query = "{ value }";
    assert_eq!(
        schema
            .execute(Request::new(query).data(Role::Admin))
            .await
            .data,
        value!({"value": 100})
    );

    let query = "{ value }";
    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: 1, column: 3 }],
            path: vec![PathSegment::Field("value".to_owned())],
            extensions: None,
        }]
    );
}

#[tokio::test]
pub async fn test_guard_on_object() {
    struct Query;

    #[Object(guard = "RoleGuard::new(Role::Admin)")]
    impl Query {
        async fn value(&self) -> i32 {
            100
        }
    }

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

    let query = "{ value }";
    assert_eq!(
        schema
            .execute(Request::new(query).data(Role::Admin))
            .await
            .data,
        value!({"value": 100})
    );

    let query = "{ value }";
    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: 1, column: 3 }],
            path: vec![PathSegment::Field("value".to_owned())],
            extensions: None,
        }]
    );
}

#[tokio::test]
pub async fn test_guard_on_object_field() {
    struct Query;

    #[Object]
    impl Query {
        #[graphql(guard = "RoleGuard::new(Role::Admin)")]
        async fn value(&self) -> i32 {
            100
        }
    }

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

    let query = "{ value }";
    assert_eq!(
        schema
            .execute(Request::new(query).data(Role::Admin))
            .await
            .data,
        value!({"value": 100})
    );

    let query = "{ value }";
    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: 1, column: 3 }],
            path: vec![PathSegment::Field("value".to_owned())],
            extensions: None,
        }]
    );
}

#[tokio::test]
pub async fn test_guard_on_complex_object() {
    #[derive(SimpleObject)]
    #[graphql(complex)]
    struct Query {
        value1: i32,
    }

    #[ComplexObject(guard = "RoleGuard::new(Role::Admin)")]
    impl Query {
        async fn value2(&self) -> i32 {
            100
        }
    }

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

    let query = "{ value2 }";
    assert_eq!(
        schema
            .execute(Request::new(query).data(Role::Admin))
            .await
            .data,
        value!({"value2": 100})
    );

    let query = "{ value2 }";
    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: 1, column: 3 }],
            path: vec![PathSegment::Field("value2".to_owned())],
            extensions: None,
        }]
    );
}

#[tokio::test]
pub async fn test_guard_on_complex_object_field() {
    #[derive(SimpleObject)]
    #[graphql(complex)]
    struct Query {
        value1: i32,
    }

    #[ComplexObject]
    impl Query {
        #[graphql(guard = "RoleGuard::new(Role::Admin)")]
        async fn value2(&self) -> i32 {
            100
        }
    }

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

    let query = "{ value2 }";
    assert_eq!(
        schema
            .execute(Request::new(query).data(Role::Admin))
            .await
            .data,
        value!({"value2": 100})
    );

    let query = "{ value2 }";
    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: 1, column: 3 }],
            path: vec![PathSegment::Field("value2".to_owned())],
            extensions: None,
        }]
    );
}

#[tokio::test]
pub async fn test_guard_on_nullable_fields_returns_null() {
    fn forbidden_error() -> ServerError {
        ServerError {
            message: "Forbidden".to_string(),
            source: None,
            locations: vec![Pos { line: 1, column: 3 }],
            path: vec![PathSegment::Field("value".to_owned())],
            extensions: None,
        }
    }

    #[derive(SimpleObject)]
    struct SimpleQuery {
        #[graphql(guard = "RoleGuard::new(Role::Admin)")]
        value: Option<i32>,
        other: i32,
    }

    let schema = Schema::new(
        SimpleQuery {
            value: Some(100),
            other: 1,
        },
        EmptyMutation,
        EmptySubscription,
    );
    let response = schema
        .execute(Request::new("{ value other }").data(Role::Guest))
        .await;
    assert_eq!(response.data, value!({ "value": null, "other": 1 }));
    assert_eq!(response.errors, vec![forbidden_error()]);

    struct ObjectQuery;

    #[Object]
    impl ObjectQuery {
        #[graphql(guard = "RoleGuard::new(Role::Admin)")]
        async fn value(&self) -> Option<i32> {
            Some(100)
        }

        async fn other(&self) -> i32 {
            2
        }
    }

    let schema = Schema::new(ObjectQuery, EmptyMutation, EmptySubscription);
    let response = schema
        .execute(Request::new("{ value other }").data(Role::Guest))
        .await;
    assert_eq!(response.data, value!({ "value": null, "other": 2 }));
    assert_eq!(response.errors, vec![forbidden_error()]);

    #[derive(SimpleObject)]
    #[graphql(complex)]
    struct ComplexQuery {
        other: i32,
    }

    #[ComplexObject]
    impl ComplexQuery {
        #[graphql(guard = "RoleGuard::new(Role::Admin)")]
        async fn value(&self) -> Option<i32> {
            Some(100)
        }
    }

    let schema = Schema::new(ComplexQuery { other: 3 }, EmptyMutation, EmptySubscription);
    let response = schema
        .execute(Request::new("{ value other }").data(Role::Guest))
        .await;
    assert_eq!(response.data, value!({ "value": null, "other": 3 }));
    assert_eq!(response.errors, vec![forbidden_error()]);
}

#[tokio::test]
pub async fn test_guard_on_nullable_result_field_returns_null() {
    fn forbidden_error() -> ServerError {
        ServerError {
            message: "Forbidden".to_string(),
            source: None,
            locations: vec![Pos { line: 1, column: 3 }],
            path: vec![PathSegment::Field("value".to_owned())],
            extensions: None,
        }
    }

    struct OptionResultQuery;

    #[Object]
    impl OptionResultQuery {
        #[graphql(guard = "RoleGuard::new(Role::Admin)")]
        async fn value(&self) -> Option<Result<i32>> {
            Some(Ok(100))
        }
    }

    let schema = Schema::new(OptionResultQuery, EmptyMutation, EmptySubscription);
    let response = schema
        .execute(Request::new("{ value }").data(Role::Guest))
        .await;

    assert_eq!(response.data, value!({ "value": null }));
    assert_eq!(response.errors, vec![forbidden_error()]);

    struct ResultOptionQuery;

    #[Object]
    impl ResultOptionQuery {
        #[graphql(guard = "RoleGuard::new(Role::Admin)")]
        async fn value(&self) -> Result<Option<i32>> {
            Ok(Some(100))
        }
    }

    let schema = Schema::new(ResultOptionQuery, EmptyMutation, EmptySubscription);
    let response = schema
        .execute(Request::new("{ value }").data(Role::Guest))
        .await;

    assert_eq!(response.data, value!({ "value": null }));
    assert_eq!(response.errors, vec![forbidden_error()]);

    let response = schema
        .execute(Request::new("{ value }").data(Role::Admin))
        .await;

    assert_eq!(response.data, value!({ "value": 100 }));
    assert!(response.errors.is_empty());
}

#[tokio::test]
pub async fn test_guard_with_fn() {
    fn is_admin(ctx: &Context<'_>) -> Result<()> {
        if ctx.data::<Role>().ok() == Some(&Role::Admin) {
            Ok(())
        } else {
            Err("Forbidden".into())
        }
    }

    #[derive(SimpleObject)]
    struct Query {
        #[graphql(guard = "is_admin")]
        value: i32,
    }

    let schema = Schema::new(Query { value: 10 }, EmptyMutation, EmptySubscription);

    let query = "{ value }";
    assert_eq!(
        schema
            .execute(Request::new(query).data(Role::Admin))
            .await
            .data,
        value!({"value": 10})
    );

    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: 1, column: 3 }],
            path: vec![PathSegment::Field("value".to_owned())],
            extensions: None,
        }]
    );
}
