use std::{
    hash::{Hash as _, Hasher as _},
    sync::Arc,
};

use xxhash_rust::xxh3::Xxh3;

use super::{
    rules::{ValidationRule, ValidationVisitors},
    utils::{ValidationError, ValidationErrorContext},
};

use crate::{
    ast::{visit_document, OperationVisitorContext},
    static_graphql::{query, schema},
};

#[derive(Clone)]
pub struct ValidationPlan {
    pub rules: Vec<Arc<Box<dyn ValidationRule>>>,
    pub hash: u64,
}

#[inline]
fn calculate_hash(rules: &[Arc<Box<dyn ValidationRule>>]) -> u64 {
    let mut hasher = Xxh3::new();
    for rule in rules {
        rule.error_code().hash(&mut hasher);
    }
    hasher.finish()
}

impl ValidationPlan {
    pub fn new() -> Self {
        let rules = vec![];
        Self {
            hash: calculate_hash(&rules),
            rules,
        }
    }

    pub fn from(rules: Vec<Box<dyn ValidationRule>>) -> Self {
        let rules = rules
            .into_iter()
            .map(|rule| rule.into())
            .collect::<Vec<Arc<Box<dyn ValidationRule>>>>();

        Self {
            hash: calculate_hash(&rules),
            rules,
        }
    }

    pub fn add_rule(&mut self, rule: Box<dyn ValidationRule>) {
        self.rules.push(Arc::new(rule));
        self.recalculate_hash();
    }

    fn recalculate_hash(&mut self) {
        self.hash = calculate_hash(&self.rules);
    }
}

impl Default for ValidationPlan {
    fn default() -> Self {
        Self::new()
    }
}

pub fn validate<'a>(
    schema: &'a schema::Document,
    operation: &'a query::Document,
    validation_plan: &'a ValidationPlan,
) -> Vec<ValidationError> {
    let mut error_collector = ValidationErrorContext::new();
    let mut validation_context = OperationVisitorContext::new(operation, schema);

    let mut visitor_instances = Vec::with_capacity(validation_plan.rules.len());
    for rule in &validation_plan.rules {
        visitor_instances.push(rule.visitor());
    }
    let mut visitors = ValidationVisitors::new(visitor_instances);

    if !visitors.is_empty() {
        visit_document(
            &mut visitors,
            operation,
            &mut validation_context,
            &mut error_collector,
        );
    }

    error_collector.errors
}

#[test]
fn cyclic_fragment_should_never_loop() {
    use crate::validation::rules::default_rules_validation_plan;
    use crate::validation::test_utils::*;

    let mut default_plan = default_rules_validation_plan();
    let errors = test_operation_with_schema(
        "
        {
          dog {
            nickname
            ...bark
            ...parents
          }
        }

        fragment bark on Dog {
          barkVolume
          ...parents
        }

        fragment parents on Dog {
          mother {
            ...bark
          }
        }

    ",
        TEST_SCHEMA,
        &mut default_plan,
    );

    let messages = get_messages(&errors);
    assert_eq!(errors[0].error_code, "NoFragmentsCycle");
    assert_eq!(messages.len(), 1);
    assert_eq!(
        messages,
        vec!["Cannot spread fragment \"bark\" within itself via \"parents\"."]
    )
}

#[test]
fn simple_self_reference_fragment_should_not_loop() {
    use crate::validation::rules::default_rules_validation_plan;
    use crate::validation::test_utils::*;

    let mut default_plan = default_rules_validation_plan();
    let errors = test_operation_with_schema(
        "
        query dog {
          dog {
            ...DogFields
          }
        }

        fragment DogFields on Dog {
          mother {
            ...DogFields
          }
          father {
            ...DogFields
          }
        }
    ",
        TEST_SCHEMA,
        &mut default_plan,
    );

    let messages = get_messages(&errors);
    assert_eq!(messages.len(), 2);
    assert_eq!(
        messages,
        vec![
            "Cannot spread fragment \"DogFields\" within itself.",
            "Cannot spread fragment \"DogFields\" within itself."
        ]
    )
}

#[test]
fn fragment_loop_through_multiple_frags() {
    use crate::validation::rules::default_rules_validation_plan;
    use crate::validation::test_utils::*;

    let mut default_plan = default_rules_validation_plan();
    let errors = test_operation_with_schema(
        "
        query dog {
          dog {
            ...DogFields1
          }
        }

        fragment DogFields1 on Dog {
          barks
          ...DogFields2
        }

        fragment DogFields2 on Dog {
          barkVolume
          ...DogFields3
        }

        fragment DogFields3 on Dog {
          name
          ...DogFields1
        }
    ",
        TEST_SCHEMA,
        &mut default_plan,
    );

    let messages = get_messages(&errors);
    assert_eq!(messages.len(), 1);
    assert_eq!(
        messages,
        vec![
      "Cannot spread fragment \"DogFields1\" within itself via \"DogFields2\", \"DogFields3\"."
    ]
    )
}
