use crate::parser::types::{Directive, Field};
use crate::registry::MetaTypeName;
use crate::validation::visitor::{Visitor, VisitorContext};
use crate::Positioned;

#[derive(Default)]
pub struct ProvidedNonNullArguments;

impl<'a> Visitor<'a> for ProvidedNonNullArguments {
    fn enter_directive(
        &mut self,
        ctx: &mut VisitorContext<'a>,
        directive: &'a Positioned<Directive>,
    ) {
        if let Some(schema_directive) = ctx
            .registry
            .directives
            .get(directive.node.name.node.as_str())
        {
            for arg in schema_directive.args.values() {
                if MetaTypeName::create(&arg.ty).is_non_null()
                    && arg.default_value.is_none()
                    && directive
                        .node
                        .arguments
                        .iter()
                        .find(|(name, _)| name.node == arg.name)
                        .is_none()
                {
                    ctx.report_error(vec![directive.pos],
                            format!(
                                "Directive \"@{}\" argument \"{}\" of type \"{}\" is required but not provided",
                                directive.node.name, arg.name, arg.ty
                            ));
                }
            }
        }
    }

    fn enter_field(&mut self, ctx: &mut VisitorContext<'a>, field: &'a Positioned<Field>) {
        if let Some(parent_type) = ctx.parent_type() {
            if let Some(schema_field) = parent_type.field_by_name(&field.node.name.node) {
                for arg in schema_field.args.values() {
                    if MetaTypeName::create(&arg.ty).is_non_null()
                        && arg.default_value.is_none()
                        && field
                            .node
                            .arguments
                            .iter()
                            .find(|(name, _)| name.node == arg.name)
                            .is_none()
                    {
                        ctx.report_error(vec![field.pos],
                             format!(
                                 r#"Field "{}" argument "{}" of type "{}" is required but not provided"#,
                                 field.node.name, arg.name, parent_type.name()
                             ));
                    }
                }
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    pub fn factory() -> ProvidedNonNullArguments {
        ProvidedNonNullArguments
    }

    #[test]
    fn ignores_unknown_arguments() {
        expect_passes_rule!(
            factory,
            r#"
          {
            dog {
              isHousetrained(unknownArgument: true)
            }
          }
        "#,
        );
    }

    #[test]
    fn arg_on_optional_arg() {
        expect_passes_rule!(
            factory,
            r#"
            {
              dog {
                isHousetrained(atOtherHomes: true)
              }
            }
        "#,
        );
    }

    #[test]
    fn no_arg_on_optional_arg() {
        expect_passes_rule!(
            factory,
            r#"
            {
              dog {
                isHousetrained
              }
            }
        "#,
        );
    }

    #[test]
    fn multiple_args() {
        expect_passes_rule!(
            factory,
            r#"
            {
              complicatedArgs {
                multipleReqs(req1: 1, req2: 2)
              }
            }
        "#,
        );
    }

    #[test]
    fn multiple_args_reverse_order() {
        expect_passes_rule!(
            factory,
            r#"
            {
              complicatedArgs {
                multipleReqs(req2: 2, req1: 1)
              }
            }
        "#,
        );
    }

    #[test]
    fn no_args_on_multiple_optional() {
        expect_passes_rule!(
            factory,
            r#"
            {
              complicatedArgs {
                multipleOpts
              }
            }
        "#,
        );
    }

    #[test]
    fn one_arg_on_multiple_optional() {
        expect_passes_rule!(
            factory,
            r#"
            {
              complicatedArgs {
                multipleOpts(opt1: 1)
              }
            }
        "#,
        );
    }

    #[test]
    fn second_arg_on_multiple_optional() {
        expect_passes_rule!(
            factory,
            r#"
            {
              complicatedArgs {
                multipleOpts(opt2: 1)
              }
            }
        "#,
        );
    }

    #[test]
    fn muliple_reqs_on_mixed_list() {
        expect_passes_rule!(
            factory,
            r#"
            {
              complicatedArgs {
                multipleOptAndReq(req1: 3, req2: 4)
              }
            }
        "#,
        );
    }

    #[test]
    fn multiple_reqs_and_one_opt_on_mixed_list() {
        expect_passes_rule!(
            factory,
            r#"
            {
              complicatedArgs {
                multipleOptAndReq(req1: 3, req2: 4, opt1: 5)
              }
            }
        "#,
        );
    }

    #[test]
    fn all_reqs_on_opts_on_mixed_list() {
        expect_passes_rule!(
            factory,
            r#"
            {
              complicatedArgs {
                multipleOptAndReq(req1: 3, req2: 4, opt1: 5, opt2: 6)
              }
            }
        "#,
        );
    }

    #[test]
    fn missing_one_non_nullable_argument() {
        expect_fails_rule!(
            factory,
            r#"
            {
              complicatedArgs {
                multipleReqs(req2: 2)
              }
            }
        "#,
        );
    }

    #[test]
    fn missing_multiple_non_nullable_arguments() {
        expect_fails_rule!(
            factory,
            r#"
            {
              complicatedArgs {
                multipleReqs
              }
            }
        "#,
        );
    }

    #[test]
    fn incorrect_value_and_missing_argument() {
        expect_fails_rule!(
            factory,
            r#"
            {
              complicatedArgs {
                multipleReqs(req1: "one")
              }
            }
        "#,
        );
    }

    #[test]
    fn ignores_unknown_directives() {
        expect_passes_rule!(
            factory,
            r#"
            {
              dog @unknown
            }
        "#,
        );
    }

    #[test]
    fn with_directives_of_valid_types() {
        expect_passes_rule!(
            factory,
            r#"
            {
              dog @include(if: true) {
                name
              }
              human @skip(if: false) {
                name
              }
            }
        "#,
        );
    }

    #[test]
    fn with_directive_with_missing_types() {
        expect_fails_rule!(
            factory,
            r#"
            {
              dog @include {
                name @skip
              }
            }
        "#,
        );
    }
}
