//! Building a JSON Schema validator.
//! The main idea is to create a tree from the input JSON Schema. This tree will contain
//! everything needed to perform such validation in runtime.
use std::collections::hash_map::Entry;

use crate::{
    error::{error, no_error, ErrorIterator},
    evaluation::{Annotations, ErrorDescription, Evaluation, EvaluationNode},
    node::SchemaNode,
    paths::{LazyLocation, Location, RefTracker},
    Draft, Json, NodeIdentity, SerdeJson, ValidationError, ValidationOptions,
};
use ahash::AHashMap;
use serde_json::Value;

// Re-export LazyEvaluationPath from paths module
pub(crate) use crate::paths::LazyEvaluationPath;

#[derive(Default)]
struct EvaluationPathCache {
    prefix_identifiers: AHashMap<(usize, usize), usize>,
    paths: AHashMap<(usize, usize), Location>,
}

impl EvaluationPathCache {
    fn evaluation_path(&mut self, tracker: &RefTracker<'_>, location: &Location) -> Location {
        let prefix_identifier = self.prefix_identifier(tracker);
        let key = (prefix_identifier, location.as_ptr());
        match self.paths.entry(key) {
            Entry::Occupied(entry) => entry.get().clone(),
            Entry::Vacant(entry) => {
                let path = tracker.evaluation_path_with_prefix(tracker.prefix(), location);
                entry.insert(path.clone());
                path
            }
        }
    }

    fn prefix_identifier(&mut self, tracker: &RefTracker<'_>) -> usize {
        if let Some(identifier) = tracker.cached_prefix_identifier() {
            return identifier;
        }
        let parent_identifier = tracker
            .parent()
            .map_or(0, |parent| self.prefix_identifier(parent));
        let key = (parent_identifier, tracker.suffix().as_ptr());
        let next_identifier = self.prefix_identifiers.len() + 1;
        let identifier = match self.prefix_identifiers.entry(key) {
            Entry::Occupied(entry) => *entry.get(),
            Entry::Vacant(entry) => {
                entry.insert(next_identifier);
                next_identifier
            }
        };
        tracker.cache_prefix_identifier(identifier);
        identifier
    }
}

/// Validation state for cycle detection and memoization.
#[derive(Default)]
pub struct ValidationContext {
    validating: Vec<(usize, NodeIdentity)>,
    /// Stack of (validators, instance) pairs currently collecting evaluated properties/items,
    /// used to break cycles from self-referential `$dynamicRef`/`$recursiveRef` under
    /// `unevaluatedProperties`/`unevaluatedItems`.
    marking: Vec<(usize, NodeIdentity)>,
    /// Lazy-initialized cache for recursive schema validation.
    is_valid_cache: Option<AHashMap<(usize, NodeIdentity), bool>>,
    /// Lazy-initialized cache for ECMA regex transformation results during format "regex" validation.
    ecma_regex_cache: Option<AHashMap<String, bool>>,
    evaluation_path_cache: Option<Box<EvaluationPathCache>>,
    evaluation_path_calls: u32,
}

/// Evaluation paths are only cached once this many have been built.
///
/// Small evaluations show little path reuse, so below this threshold the map costs more than it saves.
const EVALUATION_PATH_CACHE_THRESHOLD: u32 = 4096;

impl ValidationContext {
    pub(crate) fn new() -> Self {
        Self::default()
    }

    /// Returns `true` if cycle detected. `None` identity disables cycle tracking for this node.
    #[inline]
    pub(crate) fn enter(
        &mut self,
        node_id: usize,
        instance_identity: Option<NodeIdentity>,
    ) -> bool {
        let Some(identity) = instance_identity else {
            return false;
        };
        let key = (node_id, identity);
        if self.validating.contains(&key) {
            return true;
        }
        self.validating.push(key);
        false
    }

    #[inline]
    pub(crate) fn exit(&mut self, node_id: usize, instance_identity: Option<NodeIdentity>) {
        let Some(identity) = instance_identity else {
            return;
        };
        let popped = self.validating.pop();
        debug_assert_eq!(
            popped,
            Some((node_id, identity)),
            "ValidationContext::exit called out of order"
        );
    }

    /// Returns `true` if this `(validators, instance)` pair is already being marked (cycle).
    #[inline]
    pub(crate) fn enter_marking(
        &mut self,
        validators_id: usize,
        instance_identity: Option<NodeIdentity>,
    ) -> bool {
        let Some(identity) = instance_identity else {
            return false;
        };
        let key = (validators_id, identity);
        if self.marking.contains(&key) {
            return true;
        }
        self.marking.push(key);
        false
    }

    #[inline]
    pub(crate) fn exit_marking(&mut self) {
        self.marking.pop();
    }

    /// Containers only: the cache outlives them, and only they promise an identity no later node
    /// reuses.
    #[inline]
    pub(crate) fn get_cached_result(
        &self,
        node_id: usize,
        identity: Option<NodeIdentity>,
    ) -> Option<bool> {
        let cache = self.is_valid_cache.as_ref()?;
        cache.get(&(node_id, identity?)).copied()
    }

    /// Containers only: the cache outlives them, and only they promise an identity no later node
    /// reuses.
    #[inline]
    pub(crate) fn cache_result(
        &mut self,
        node_id: usize,
        identity: Option<NodeIdentity>,
        result: bool,
    ) {
        let Some(identity) = identity else {
            return;
        };
        self.is_valid_cache
            .get_or_insert_with(AHashMap::new)
            .insert((node_id, identity), result);
    }
    /// Evaluation path for `location` under `tracker`, cached across instance nodes.
    ///
    /// Keying on addresses is sound because none of them can be freed and reused mid-run:
    /// keyword locations and `$ref` suffixes live in the compiled schema, and cached prefixes
    /// are owned by `eval_prefix_cache`.
    pub(crate) fn evaluation_path(
        &mut self,
        tracker: &RefTracker<'_>,
        location: &Location,
    ) -> Location {
        self.evaluation_path_calls = self.evaluation_path_calls.saturating_add(1);
        if self.evaluation_path_calls < EVALUATION_PATH_CACHE_THRESHOLD {
            return tracker.evaluation_path_with_prefix(tracker.prefix(), location);
        }
        self.evaluation_path_cache
            .get_or_insert_with(Box::default)
            .evaluation_path(tracker, location)
    }

    /// Check if an ECMA regex pattern is valid.
    pub(crate) fn is_valid_ecma_regex(&mut self, pattern: &str) -> bool {
        if let Some(cache) = &self.ecma_regex_cache {
            if let Some(&result) = cache.get(pattern) {
                return result;
            }
        }
        let result = jsonschema_regex::to_rust_regex(pattern).is_ok();
        self.ecma_regex_cache
            .get_or_insert_with(AHashMap::new)
            .insert(pattern.to_owned(), result);
        result
    }
}

/// The Validate trait represents a predicate over some JSON value. Some validators are very simple
/// predicates such as "a value which is a string", whereas others may be much more complex,
/// consisting of several other validators composed together in various ways.
///
/// Much of the time all an application cares about is whether the predicate returns true or false,
/// in that case the `is_valid` function is sufficient. Sometimes applications will want more
/// detail about why a schema has failed, in which case the `validate` method can be used to
/// iterate over the errors produced by this validator. Finally, applications may be interested in
/// annotations produced by schemas over valid results, in this case the `evaluate` method can be used
/// to obtain this information.
///
/// If you are implementing `Validate` it is often sufficient to implement `validate` and
/// `is_valid`. `evaluate` is only necessary for validators which compose other validators. See the
/// documentation for `evaluate` for more information.
///
/// # Context Types
///
/// - `is_valid` takes `LightweightContext`: Only cycle detection, zero path tracking overhead.
/// - `validate`, `iter_errors`, `evaluate` take `ValidationContext`: Cycle detection + evaluation path tracking.
pub(crate) trait Validate<F: Json = SerdeJson>: Send + Sync {
    fn iter_errors<'i>(
        &self,
        instance: &F::Node<'i>,
        location: &LazyLocation,
        tracker: Option<&RefTracker>,
        ctx: &mut ValidationContext,
    ) -> ErrorIterator<'i> {
        match self.validate(instance, location, tracker, ctx) {
            Ok(()) => no_error(),
            Err(err) => error(err),
        }
    }

    fn is_valid(&self, instance: &F::Node<'_>, ctx: &mut ValidationContext) -> bool;

    fn validate<'i>(
        &self,
        instance: &F::Node<'i>,
        location: &LazyLocation,
        tracker: Option<&RefTracker>,
        ctx: &mut ValidationContext,
    ) -> Result<(), ValidationError<'i>>;

    fn evaluate(
        &self,
        instance: &F::Node<'_>,
        location: &LazyLocation,
        tracker: Option<&RefTracker>,
        ctx: &mut ValidationContext,
    ) -> EvaluationResult {
        let errors: Vec<ErrorDescription> = self
            .iter_errors(instance, location, tracker, ctx)
            .map(|e| ErrorDescription::from_validation_error(&e))
            .collect();
        if errors.is_empty() {
            EvaluationResult::valid_empty()
        } else {
            EvaluationResult::invalid_empty(errors)
        }
    }

    /// Returns the canonical location for this validator's schemaLocation output.
    ///
    /// Per JSON Schema spec, schemaLocation "MUST NOT include by-reference applicators
    /// such as `$ref` or `$dynamicRef`". For most validators, the keyword location is the
    /// canonical location, so this returns `None` by default.
    ///
    /// `RefValidator` and similar by-reference validators override this to return
    /// the target schema's canonical location (e.g., `/$defs/item` instead of
    /// `/properties/foo/$ref`).
    fn canonical_location(&self) -> Option<&Location> {
        None
    }
}

/// The result of evaluating a validator against an instance. This is a "partial" result because it does not include information about
/// where the error or annotation occurred.
#[derive(PartialEq)]
pub(crate) enum EvaluationResult {
    Valid {
        /// Annotations produced by this validator
        annotations: Option<Annotations>,
        /// Children evaluation nodes
        children: Vec<EvaluationNode>,
    },
    Invalid {
        /// Errors which caused this schema to be invalid
        errors: Vec<ErrorDescription>,
        /// Children evaluation nodes
        children: Vec<EvaluationNode>,
        /// Potential annotations that should be reported as dropped on failure
        annotations: Option<Annotations>,
    },
}

impl EvaluationResult {
    /// Create an empty `EvaluationResult` which is valid
    pub(crate) fn valid_empty() -> EvaluationResult {
        EvaluationResult::Valid {
            annotations: None,
            children: Vec::new(),
        }
    }

    /// Create an empty `EvaluationResult` which is invalid
    pub(crate) fn invalid_empty(errors: Vec<ErrorDescription>) -> EvaluationResult {
        EvaluationResult::Invalid {
            errors,
            children: Vec::new(),
            annotations: None,
        }
    }

    /// Set the annotation that will be returned for the current validator. If this
    /// `EvaluationResult` is invalid then this method does nothing
    pub(crate) fn annotate(&mut self, new_annotations: Annotations) {
        match self {
            Self::Valid { annotations, .. } | Self::Invalid { annotations, .. } => {
                *annotations = Some(new_annotations);
            }
        }
    }

    /// Set the error that will be returned for the current validator. If this
    /// `EvaluationResult` is valid then this method converts this application into
    /// `EvaluationResult::Invalid`
    pub(crate) fn mark_errored(&mut self, error: ErrorDescription) {
        match self {
            Self::Invalid { errors, .. } => errors.push(error),
            Self::Valid {
                annotations,
                children,
            } => {
                *self = Self::Invalid {
                    errors: vec![error],
                    children: std::mem::take(children),
                    annotations: annotations.take(),
                }
            }
        }
    }

    pub(crate) fn from_children(children: Vec<EvaluationNode>) -> EvaluationResult {
        if children.iter().any(|node| !node.valid) {
            EvaluationResult::Invalid {
                errors: Vec::new(),
                children,
                annotations: None,
            }
        } else {
            EvaluationResult::Valid {
                annotations: None,
                children,
            }
        }
    }
}

impl From<EvaluationNode> for EvaluationResult {
    fn from(node: EvaluationNode) -> Self {
        if node.valid {
            EvaluationResult::Valid {
                annotations: None,
                children: vec![node],
            }
        } else {
            EvaluationResult::Invalid {
                errors: Vec::new(),
                children: vec![node],
                annotations: None,
            }
        }
    }
}

/// A compiled JSON Schema validator.
///
/// This structure represents a JSON Schema that has been parsed and compiled into
/// an efficient internal representation for validation. It contains the root node
/// of the schema tree and the configuration options used during compilation.
pub struct Validator<F: Json = SerdeJson> {
    pub(crate) root: SchemaNode<F>,
    pub(crate) draft: Draft,
}

impl<F: Json> Clone for Validator<F> {
    fn clone(&self) -> Self {
        Self {
            root: self.root.clone(),
            draft: self.draft,
        }
    }
}

impl<F: Json> std::fmt::Debug for Validator<F> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Validator")
            .field("root", &self.root)
            .field("draft", &self.draft)
            .finish()
    }
}

impl Validator<SerdeJson> {
    /// Create a default [`ValidationOptions`] for configuring JSON Schema validation.
    ///
    /// Use this to set the draft version and other validation parameters.
    ///
    /// # Example
    ///
    /// ```rust
    /// # use jsonschema::Draft;
    /// # let schema = serde_json::json!({});
    /// let validator = jsonschema::options()
    ///     .with_draft(Draft::Draft7)
    ///     .build(&schema);
    /// ```
    #[must_use]
    pub fn options<'i>() -> ValidationOptions<'i> {
        ValidationOptions::default()
    }
    /// Create a default [`ValidationOptions`] configured for async validation.
    ///
    /// Use this to set the draft version and other validation parameters when working
    /// with schemas that require async reference resolution.
    ///
    /// # Example
    ///
    /// ```rust
    /// # use serde_json::json;
    /// # use jsonschema::Draft;
    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
    /// let schema = json!({
    ///     "$ref": "https://example.com/schema.json"
    /// });
    ///
    /// let validator = jsonschema::async_options()
    ///     .with_draft(Draft::Draft202012)
    ///     .build(&schema)
    ///     .await?;
    /// # Ok(())
    /// # }
    /// ```
    ///
    /// For sync validation, use [`options()`](crate::options()) instead.
    #[cfg(feature = "resolve-async")]
    #[must_use]
    pub fn async_options<'i>(
    ) -> ValidationOptions<'i, std::sync::Arc<dyn referencing::AsyncRetrieve>> {
        ValidationOptions::default()
    }
    /// Create a validator using the default options.
    ///
    /// # Errors
    ///
    /// Returns an error if the supplied `schema` is invalid for the selected draft or references cannot be resolved.
    pub fn new(schema: &Value) -> Result<Validator, ValidationError<'static>> {
        Self::options().build(schema)
    }
    /// Create a validator using the default async options.
    #[cfg(feature = "resolve-async")]
    ///
    /// # Errors
    ///
    /// Returns an error if the supplied `schema` is invalid for the selected draft or references cannot be resolved.
    pub async fn async_new(schema: &Value) -> Result<Validator, ValidationError<'static>> {
        Self::async_options().build(schema).await
    }
}

// `F::Node` is a borrow handle (`&Value` for `serde_json`); taking it by value keeps the serde
// call ergonomics (`&Value`, not `&&Value`) across every representation.
#[allow(clippy::needless_pass_by_value)]
impl<F: Json> Validator<F> {
    /// Validate `instance` against `schema` and return the first error if any.
    ///
    /// # Errors
    ///
    /// Returns the first [`ValidationError`] describing why `instance` does not satisfy the schema.
    #[inline]
    pub fn validate<'i>(&self, instance: F::Node<'i>) -> Result<(), ValidationError<'i>> {
        let mut ctx = ValidationContext::new();
        self.root
            .validate(&instance, &LazyLocation::new(), None, &mut ctx)
    }
    /// Run validation against `instance` and return an iterator over [`ValidationError`] in the error case.
    #[inline]
    #[must_use]
    pub fn iter_errors<'i>(&'i self, instance: F::Node<'i>) -> ErrorIterator<'i> {
        let mut ctx = ValidationContext::new();
        self.root
            .iter_errors(&instance, &LazyLocation::new(), None, &mut ctx)
    }
    /// Run validation against `instance` but return a boolean result instead of an iterator.
    /// It is useful for cases, where it is important to only know the fact if the data is valid or not.
    /// This approach is much faster, than [`Validator::validate`].
    #[must_use]
    #[inline]
    pub fn is_valid(&self, instance: F::Node<'_>) -> bool {
        let mut ctx = ValidationContext::new();
        self.root.is_valid(&instance, &mut ctx)
    }
    /// Evaluate the schema and expose structured output formats.
    #[must_use]
    #[inline]
    pub fn evaluate(&self, instance: F::Node<'_>) -> Evaluation {
        let mut ctx = ValidationContext::new();
        let root = self
            .root
            .evaluate_instance(&instance, &LazyLocation::new(), None, &mut ctx);
        Evaluation::new(root)
    }
    /// The [`Draft`] which was used to build this validator.
    #[must_use]
    pub fn draft(&self) -> Draft {
        self.draft
    }
}

/// A map of compiled JSON Schema validators keyed by URI-fragment JSON pointers.
///
/// Built via [`ValidationOptions::build_map`] or the [`validator_map_for`](crate::validator_map_for)
/// convenience function.
///
/// Each key is a URI-fragment JSON pointer (e.g. `"#"`, `"#/$defs/User"`).
/// The root schema is always present under the key `"#"`.
pub struct ValidatorMap<F: Json = SerdeJson> {
    pub(crate) validators: AHashMap<String, Validator<F>>,
}

impl<F: Json> std::fmt::Debug for ValidatorMap<F> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("ValidatorMap")
            .field("validators", &self.validators)
            .finish()
    }
}

impl<F: Json> ValidatorMap<F> {
    /// Returns the validator for the given URI-fragment pointer, or `None` if not found.
    #[must_use]
    pub fn get(&self, pointer: &str) -> Option<&Validator<F>> {
        self.validators.get(pointer)
    }

    /// Returns `true` if the map contains a validator for the given pointer.
    #[must_use]
    pub fn contains_key(&self, pointer: &str) -> bool {
        self.validators.contains_key(pointer)
    }

    /// Returns an iterator over all URI-fragment pointers in the map.
    pub fn keys(&self) -> impl Iterator<Item = &str> {
        self.validators.keys().map(String::as_str)
    }

    /// Returns the number of compiled subschema validators in the map.
    #[must_use]
    pub fn len(&self) -> usize {
        self.validators.len()
    }

    /// Returns `true` if the map contains no validators.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.validators.is_empty()
    }
}

impl<F: Json> std::ops::Index<&str> for ValidatorMap<F> {
    type Output = Validator<F>;

    /// # Panics
    ///
    /// Panics if the pointer is not found in the map.
    fn index(&self, pointer: &str) -> &Validator<F> {
        self.validators
            .get(pointer)
            .unwrap_or_else(|| panic!("JSON pointer '{pointer}' not found in ValidatorMap"))
    }
}

#[cfg(test)]
mod tests {
    use crate::{
        error::ValidationError, keywords::custom::Keyword, paths::Location, Validator, ValidatorMap,
    };
    use fancy_regex::Regex;
    use num_cmp::NumCmp;
    use serde_json::{json, Map, Value};
    use std::sync::LazyLock;

    #[cfg(not(target_arch = "wasm32"))]
    fn load(path: &str, idx: usize) -> Value {
        use std::{fs::File, io::Read, path::Path};
        let path = Path::new(path);
        let mut file = File::open(path).unwrap();
        let mut content = String::new();
        file.read_to_string(&mut content).ok().unwrap();
        let data: Value = serde_json::from_str(&content).unwrap();
        let case = &data.as_array().unwrap()[idx];
        case.get("schema").unwrap().clone()
    }

    /// Large enough that early paths are built directly and later ones come from the cache.
    #[test]
    fn evaluation_paths_match_across_cache_threshold() {
        const ELEMENTS: usize = 5000;

        let schema = json!({
            "type": "array",
            "items": {"$ref": "#/$defs/entry"},
            "$defs": {
                "entry": {
                    "type": "object",
                    "properties": {"value": {"$ref": "#/$defs/leaf"}}
                },
                "leaf": {"type": "integer"}
            }
        });
        let validator = crate::validator_for(&schema).expect("Valid schema");
        let instance = Value::Array((0..ELEMENTS).map(|_| json!({"value": "no"})).collect());

        let evaluation = validator.evaluate(&instance);
        let list = serde_json::to_value(evaluation.list()).expect("Serializable");
        let failures: Vec<_> = list["details"]
            .as_array()
            .expect("`details` is an array")
            .iter()
            .filter(|unit| unit.get("errors").is_some())
            .collect();

        assert_eq!(failures.len(), ELEMENTS);
        for (idx, unit) in failures.iter().enumerate() {
            assert_eq!(
                unit["evaluationPath"],
                "/items/$ref/properties/value/$ref/type"
            );
            assert_eq!(unit["instanceLocation"], format!("/{idx}/value"));
            assert_eq!(unit["schemaLocation"], "/$defs/leaf/type");
        }
    }

    #[test]
    fn only_keyword() {
        // When only one keyword is specified
        let schema = json!({"type": "string"});
        let validator = crate::validator_for(&schema).unwrap();
        let value1 = json!("AB");
        let value2 = json!(1);
        // And only this validator
        assert_eq!(validator.root.validators().len(), 1);
        assert!(validator.validate(&value1).is_ok());
        assert!(validator.validate(&value2).is_err());
    }

    #[test]
    #[cfg(not(target_arch = "wasm32"))]
    fn validate_ref() {
        let schema = load("tests/suite/tests/draft7/ref.json", 1);
        let value = json!({"bar": 3});
        let validator = crate::validator_for(&schema).unwrap();
        assert!(validator.validate(&value).is_ok());
        let value = json!({"bar": true});
        assert!(validator.validate(&value).is_err());
    }

    #[test]
    fn wrong_schema_type() {
        let schema = json!([1]);
        let validator = crate::validator_for(&schema);
        assert!(validator.is_err());
    }

    #[test]
    fn multiple_errors() {
        let schema = json!({"minProperties": 2, "propertyNames": {"minLength": 3}});
        let value = json!({"a": 3});
        let validator = crate::validator_for(&schema).unwrap();
        let errors: Vec<_> = validator.iter_errors(&value).collect();
        assert_eq!(errors.len(), 2);
        assert_eq!(
            errors[0].to_string(),
            r#"{"a":3} has less than 2 properties"#
        );
        assert_eq!(errors[1].to_string(), r#""a" is shorter than 3 characters"#);
    }

    #[test]
    fn custom_keyword_definition() {
        // Define a custom validator that verifies the object's keys consist of
        // only ASCII representable characters.
        // NOTE: This could be done with `propertyNames` + `pattern` but will be slower due to
        // regex usage.
        struct CustomObjectValidator;
        impl<'i> Keyword<'i> for CustomObjectValidator {
            fn validate(&self, instance: &'i Value) -> Result<(), ValidationError<'i>> {
                for key in instance.as_object().unwrap().keys() {
                    if !key.is_ascii() {
                        return Err(ValidationError::custom("Key is not ASCII"));
                    }
                }
                Ok(())
            }

            fn is_valid(&self, instance: &'i Value) -> bool {
                for (key, _value) in instance.as_object().unwrap() {
                    if !key.is_ascii() {
                        return false;
                    }
                }
                true
            }
        }

        fn custom_object_type_factory<'a>(
            _: &'a Map<String, Value>,
            schema: &'a Value,
            _path: Location,
        ) -> Result<Box<dyn for<'i> Keyword<'i>>, ValidationError<'a>> {
            const EXPECTED: &str = "ascii-keys";
            if schema.as_str() == Some(EXPECTED) {
                Ok(Box::new(CustomObjectValidator))
            } else {
                Err(ValidationError::schema(format!(
                    "Expected '{EXPECTED}', got {schema}"
                )))
            }
        }

        // Define a JSON schema that enforces the top level object has ASCII keys and has at least 1 property
        let schema =
            json!({ "custom-object-type": "ascii-keys", "type": "object", "minProperties": 1 });
        let validator = crate::options()
            .with_keyword("custom-object-type", custom_object_type_factory)
            .build(&schema)
            .unwrap();

        // Verify schema validation detects object with too few properties
        let instance = json!({});
        assert!(validator.validate(&instance).is_err());
        assert!(!validator.is_valid(&instance));

        // Verify validator succeeds on a valid custom-object-type
        let instance = json!({ "a" : 1 });
        assert!(validator.validate(&instance).is_ok());
        assert!(validator.is_valid(&instance));

        // Verify validator detects invalid custom-object-type
        let instance = json!({ "å" : 1 });
        let error = validator.validate(&instance).expect_err("Should fail");
        assert_eq!(error.to_string(), "Key is not ASCII");
        assert!(!validator.is_valid(&instance));
    }

    #[test]
    fn custom_keyword_iter_errors() {
        // A custom keyword that reports every non-ASCII key at once via `iter_errors`.
        struct NonAsciiKeys;
        impl<'i> Keyword<'i> for NonAsciiKeys {
            fn validate(&self, instance: &'i Value) -> Result<(), ValidationError<'i>> {
                self.iter_errors(instance).next().map_or(Ok(()), Err)
            }

            fn is_valid(&self, instance: &'i Value) -> bool {
                instance
                    .as_object()
                    .is_none_or(|obj| obj.keys().all(|key| key.is_ascii()))
            }

            fn iter_errors(
                &self,
                instance: &'i Value,
            ) -> Box<dyn Iterator<Item = ValidationError<'i>> + 'i> {
                let errors: Vec<_> = instance
                    .as_object()
                    .into_iter()
                    .flat_map(|obj| obj.keys())
                    .filter(|key| !key.is_ascii())
                    .map(|key| ValidationError::custom(format!("Key {key:?} is not ASCII")))
                    .collect();
                Box::new(errors.into_iter())
            }
        }

        fn factory<'a>(
            _: &'a Map<String, Value>,
            _: &'a Value,
            _: Location,
        ) -> Result<Box<dyn for<'i> Keyword<'i>>, ValidationError<'a>> {
            Ok(Box::new(NonAsciiKeys))
        }

        let schema = json!({ "ascii-keys": true });
        let validator = crate::options()
            .with_keyword("ascii-keys", factory)
            .build(&schema)
            .unwrap();

        let instance = json!({ "å": 1, "ø": 2, "ok": 3 });
        let errors: Vec<_> = validator.iter_errors(&instance).collect();
        assert_eq!(errors.len(), 2);
        assert_eq!(errors[0].to_string(), r#"Key "å" is not ASCII"#);
        assert_eq!(errors[1].to_string(), r#"Key "ø" is not ASCII"#);

        // `validate` yields the first error; `is_valid` short-circuits.
        assert!(!validator.is_valid(&instance));
        assert_eq!(
            validator.validate(&instance).unwrap_err().to_string(),
            r#"Key "å" is not ASCII"#
        );

        let valid = json!({ "ok": 1 });
        assert!(validator.is_valid(&valid));
        assert!(validator.validate(&valid).is_ok());
    }

    #[test]
    fn custom_keyword_iter_errors_default() {
        // A keyword that does NOT override `iter_errors`, exercising the default
        // that yields at most one error from `validate`.
        struct NonEmpty;
        impl<'i> Keyword<'i> for NonEmpty {
            fn validate(&self, instance: &'i Value) -> Result<(), ValidationError<'i>> {
                if self.is_valid(instance) {
                    Ok(())
                } else {
                    Err(ValidationError::custom("must not be empty"))
                }
            }

            fn is_valid(&self, instance: &'i Value) -> bool {
                instance.as_object().is_none_or(|obj| !obj.is_empty())
            }
        }

        fn factory<'a>(
            _: &'a Map<String, Value>,
            _: &'a Value,
            _: Location,
        ) -> Result<Box<dyn for<'i> Keyword<'i>>, ValidationError<'a>> {
            Ok(Box::new(NonEmpty))
        }

        let validator = crate::options()
            .with_keyword("non-empty", factory)
            .build(&json!({ "non-empty": true }))
            .unwrap();

        let empty = json!({});
        let errors: Vec<_> = validator.iter_errors(&empty).collect();
        assert_eq!(errors.len(), 1);
        assert_eq!(errors[0].to_string(), "must not be empty");

        let non_empty = json!({ "a": 1 });
        assert_eq!(validator.iter_errors(&non_empty).count(), 0);
    }

    #[test]
    fn custom_format_and_override_keyword() {
        // Check that a string has some number of digits followed by a dot followed by exactly 2 digits.
        fn currency_format_checker(s: &str) -> bool {
            static CURRENCY_RE: LazyLock<Regex> = LazyLock::new(|| {
                Regex::new("^(0|([1-9]+[0-9]*))(\\.[0-9]{2})$").expect("Invalid regex")
            });
            CURRENCY_RE.is_match(s).expect("Invalid regex")
        }
        // A custom keyword validator that overrides "minimum"
        // so that "minimum" may apply to "currency"-formatted strings as well.
        struct CustomMinimumValidator {
            limit: f64,
            with_currency_format: bool,
        }

        impl<'i> Keyword<'i> for CustomMinimumValidator {
            fn validate(&self, instance: &'i Value) -> Result<(), ValidationError<'i>> {
                if self.is_valid(instance) {
                    Ok(())
                } else {
                    Err(ValidationError::custom(format!(
                        "value is less than the minimum of {}",
                        self.limit
                    )))
                }
            }

            fn is_valid(&self, instance: &'i Value) -> bool {
                match instance {
                    // Numeric comparison should happen just like original behavior
                    Value::Number(instance) => {
                        if let Some(item) = instance.as_u64() {
                            !NumCmp::num_lt(item, self.limit)
                        } else if let Some(item) = instance.as_i64() {
                            !NumCmp::num_lt(item, self.limit)
                        } else {
                            let item = instance.as_f64().expect("Always valid");
                            !NumCmp::num_lt(item, self.limit)
                        }
                    }
                    // String comparison should cast currency-formatted
                    Value::String(instance)
                        if self.with_currency_format && currency_format_checker(instance) =>
                    {
                        // all preconditions for minimum applying are met
                        let value = instance
                            .parse::<f64>()
                            .expect("format validated by regex checker");
                        !NumCmp::num_lt(value, self.limit)
                    }
                    // In all other cases, the "minimum" keyword should not apply
                    _ => true,
                }
            }
        }

        // Build a validator that overrides the standard `minimum` keyword
        fn custom_minimum_factory<'a>(
            parent: &'a Map<String, Value>,
            schema: &'a Value,
            _path: Location,
        ) -> Result<Box<dyn for<'i> Keyword<'i>>, ValidationError<'a>> {
            let limit = if let Value::Number(limit) = schema {
                limit.as_f64().expect("Always valid")
            } else {
                return Err(ValidationError::schema("minimum must be a number"));
            };
            let with_currency_format = parent
                .get("format")
                .is_some_and(|format| format == "currency");
            Ok(Box::new(CustomMinimumValidator {
                limit,
                with_currency_format,
            }))
        }

        // Schema includes both the custom format and the overridden keyword
        let schema = json!({ "minimum": 2, "type": "string", "format": "currency" });
        let validator = crate::options()
            .with_format("currency", currency_format_checker)
            .with_keyword("minimum", custom_minimum_factory)
            .with_keyword("minimum-2", custom_minimum_factory)
            .should_validate_formats(true)
            .build(&schema)
            .expect("Invalid schema");

        // Control: verify schema validation rejects non-string types
        let instance = json!(15);
        assert!(validator.validate(&instance).is_err());
        assert!(!validator.is_valid(&instance));

        // Control: verify validator rejects ill-formatted strings
        let instance = json!("not a currency");
        assert!(validator.validate(&instance).is_err());
        assert!(!validator.is_valid(&instance));

        // Verify validator allows properly formatted strings that conform to custom keyword
        let instance = json!("3.00");
        assert!(validator.validate(&instance).is_ok());
        assert!(validator.is_valid(&instance));

        // Verify validator rejects properly formatted strings that do not conform to custom keyword
        let instance = json!("1.99");
        assert!(validator.validate(&instance).is_err());
        assert!(!validator.is_valid(&instance));

        // Define another schema that applies "minimum" to an integer to ensure original behavior
        let schema = json!({ "minimum": 2, "type": "integer" });
        let validator = crate::options()
            .with_format("currency", currency_format_checker)
            .with_keyword("minimum", custom_minimum_factory)
            .build(&schema)
            .expect("Invalid schema");

        // Verify schema allows integers greater than 2
        let instance = json!(3);
        assert!(validator.validate(&instance).is_ok());
        assert!(validator.is_valid(&instance));

        // Verify schema rejects integers less than 2
        let instance = json!(1);
        assert!(validator.validate(&instance).is_err());
        assert!(!validator.is_valid(&instance));

        // Invalid `minimum` value - meta-schema validation catches this first
        let schema = json!({ "minimum": "foo" });
        let error = crate::options()
            .with_keyword("minimum", custom_minimum_factory)
            .build(&schema)
            .expect_err("Should fail");
        // The meta-schema validates before our factory runs, so we get a type error
        assert_eq!(error.to_string(), "\"foo\" is not of type \"number\"");
    }

    #[test]
    fn custom_keyword_validation_error_paths() {
        struct AlwaysFailValidator;
        impl<'i> Keyword<'i> for AlwaysFailValidator {
            fn validate(&self, _instance: &'i Value) -> Result<(), ValidationError<'i>> {
                Err(ValidationError::custom("always fails"))
            }
            fn is_valid(&self, _instance: &'i Value) -> bool {
                false
            }
        }

        fn always_fail_factory<'a>(
            _: &'a Map<String, Value>,
            _: &'a Value,
            _: Location,
        ) -> Result<Box<dyn for<'i> Keyword<'i>>, ValidationError<'a>> {
            Ok(Box::new(AlwaysFailValidator))
        }

        let schema = json!({
            "properties": {
                "name": { "alwaysFail": true }
            }
        });
        let validator = crate::options()
            .with_keyword("alwaysFail", always_fail_factory)
            .build(&schema)
            .expect("Valid schema");

        let instance = json!({"name": "test"});
        let error = validator.validate(&instance).expect_err("Should fail");
        assert_eq!(error.instance_path().as_str(), "/name");
        assert_eq!(error.schema_path().as_str(), "/properties/name/alwaysFail");
        assert_eq!(
            error.evaluation_path().as_str(),
            "/properties/name/alwaysFail"
        );
        assert_eq!(error.kind().keyword(), "alwaysFail");
    }

    #[test]
    fn custom_keyword_factory_error_schema_path() {
        fn failing_factory<'a>(
            _: &'a Map<String, Value>,
            _: &'a Value,
            _: Location,
        ) -> Result<Box<dyn for<'i> Keyword<'i>>, ValidationError<'a>> {
            Err(ValidationError::schema("invalid schema value"))
        }

        let schema = json!({
            "properties": {
                "field": { "myKeyword": "bad-value" }
            }
        });
        let error = crate::options()
            .with_keyword("myKeyword", failing_factory)
            .build(&schema)
            .expect_err("Should fail");
        assert_eq!(error.to_string(), "invalid schema value");
        assert_eq!(error.schema_path().as_str(), "/properties/field/myKeyword");
    }

    #[test]
    fn test_validator_is_send_and_sync() {
        fn assert_send_sync<T: Send + Sync>() {}
        assert_send_sync::<Validator>();
    }

    #[test]
    fn test_validator_map_is_send_and_sync() {
        fn assert_send_sync<T: Send + Sync>() {}
        assert_send_sync::<ValidatorMap>();
    }

    #[test]
    fn test_validator_clone() {
        let schema = json!({"type": "string", "minLength": 3});
        let validator = crate::validator_for(&schema).expect("Valid schema");

        // Clone the validator
        let cloned = validator.clone();

        // Both validators should work independently
        assert!(validator.is_valid(&json!("hello")));
        assert!(!validator.is_valid(&json!("hi")));

        assert!(cloned.is_valid(&json!("hello")));
        assert!(!cloned.is_valid(&json!("hi")));

        // Verify they validate the same way
        assert_eq!(
            validator.is_valid(&json!("test")),
            cloned.is_valid(&json!("test"))
        );
    }

    #[test]
    fn ref_with_required_multiple_missing_clones_deferred_eval_path() {
        // Tests the Deferred branch of LazyEvaluationPath::clone()
        // When $ref is involved and multiple required properties are missing,
        // the eval_path is cloned for each error
        let schema = json!({
            "$defs": {
                "Person": {
                    "type": "object",
                    "required": ["name", "age", "email"]
                }
            },
            "$ref": "#/$defs/Person"
        });
        let validator = crate::validator_for(&schema).expect("Valid schema");
        let instance = json!({});

        let errors: Vec<_> = validator.iter_errors(&instance).collect();

        // Should have 3 errors for the 3 missing required properties
        assert_eq!(errors.len(), 3);

        // All errors should have evaluation paths that include the $ref traversal
        for error in &errors {
            assert_eq!(error.schema_path().as_str(), "/$defs/Person/required");
            assert_eq!(error.evaluation_path().as_str(), "/$ref/required");
        }
    }

    #[test]
    fn test_validator_map_get_existing() {
        let schema = json!({
            "$defs": {
                "User": {"type": "object", "required": ["name"]}
            }
        });
        let map = crate::validator_map_for(&schema).unwrap();
        let v = map.get("#/$defs/User").unwrap();
        assert!(v.is_valid(&json!({"name": "Alice"})));
        assert!(!v.is_valid(&json!({})));
    }

    #[test]
    fn test_validator_map_root_always_present() {
        let schema = json!({"type": "string"});
        let map = crate::validator_map_for(&schema).unwrap();
        assert!(map.get("#").unwrap().is_valid(&json!("hello")));
        assert!(!map.get("#").unwrap().is_valid(&json!(42)));
    }

    #[test]
    fn test_validator_map_get_missing_returns_none() {
        let schema = json!({"type": "object"});
        let map = crate::validator_map_for(&schema).unwrap();
        assert!(map.get("#/nonexistent").is_none());
    }

    #[test]
    fn test_validator_map_contains_key() {
        let schema = json!({"$defs": {"Foo": {"type": "string"}}});
        let map = crate::validator_map_for(&schema).unwrap();
        assert!(map.contains_key("#/$defs/Foo"));
        assert!(!map.contains_key("#/missing"));
    }

    #[test]
    fn test_validator_map_keys_includes_root_and_defs() {
        let schema = json!({
            "type": "object",
            "$defs": {"Foo": {"type": "string"}}
        });
        let map = crate::validator_map_for(&schema).unwrap();
        let keys: std::collections::HashSet<&str> = map.keys().collect();
        assert!(keys.contains("#"));
        assert!(keys.contains("#/$defs/Foo"));
    }

    #[test]
    fn test_validator_map_index() {
        let schema = json!({"$defs": {"Bar": {"type": "integer"}}});
        let map = crate::validator_map_for(&schema).unwrap();
        assert!(map["#/$defs/Bar"].is_valid(&json!(42)));
        assert!(!map["#/$defs/Bar"].is_valid(&json!("nope")));
    }

    #[test]
    #[should_panic(expected = "JSON pointer '#/nonexistent' not found in ValidatorMap")]
    fn test_validator_map_index_panics_on_missing() {
        let schema = json!({"type": "object"});
        let map = crate::validator_map_for(&schema).unwrap();
        let _ = &map["#/nonexistent"];
    }

    #[test]
    fn test_validator_map_via_options() {
        let schema = json!({"$defs": {"S": {"type": "string"}}});
        let map = crate::options().build_map(&schema).unwrap();
        assert!(map.get("#/$defs/S").unwrap().is_valid(&json!("hello")));
    }

    #[cfg(all(feature = "resolve-async", not(target_family = "wasm")))]
    #[tokio::test]
    async fn test_async_validator_map_for() {
        let schema = json!({"$defs": {"T": {"type": "string"}}});
        let map = crate::async_validator_map_for(&schema).await.unwrap();
        assert!(map["#/$defs/T"].is_valid(&json!("hello")));
        assert!(!map["#/$defs/T"].is_valid(&json!(42)));
    }

    #[cfg(all(feature = "resolve-async", not(target_family = "wasm")))]
    #[tokio::test]
    async fn test_async_build_map_via_options() {
        let schema = json!({"$defs": {"N": {"type": "integer"}}});
        let map = crate::async_options().build_map(&schema).await.unwrap();
        assert!(map["#/$defs/N"].is_valid(&json!(7)));
        assert!(!map["#/$defs/N"].is_valid(&json!("not an int")));
    }

    #[test]
    fn test_validator_map_len() {
        let schema = json!({
            "$defs": {
                "A": {"type": "string"},
                "B": {"type": "integer"}
            }
        });
        let map = crate::validator_map_for(&schema).unwrap();
        assert!(map.len() >= 3); // root + A + B
    }

    #[test]
    fn test_validator_map_is_empty_false() {
        let schema = json!({"type": "string"});
        let map = crate::validator_map_for(&schema).unwrap();
        assert!(!map.is_empty());
    }

    #[test]
    fn test_validator_map_nested_pointer() {
        let schema = json!({
            "properties": {
                "name": {"type": "string", "minLength": 2}
            }
        });
        let map = crate::validator_map_for(&schema).unwrap();
        assert!(map["#/properties/name"].is_valid(&json!("Al")));
        assert!(!map["#/properties/name"].is_valid(&json!("A")));
    }

    #[test]
    fn test_validator_map_with_registry() {
        let address_schema = json!({
            "type": "object",
            "properties": {
                "city": {"type": "string"},
                "zip": {"type": "string"}
            },
            "required": ["city"]
        });
        let registry = crate::Registry::new()
            .add("https://example.com/address.json", &address_schema)
            .expect("valid resource")
            .prepare()
            .expect("registry build failed");

        let schema = json!({
            "$defs": {
                "Address": {"$ref": "https://example.com/address.json"}
            }
        });
        let map = crate::options()
            .with_registry(&registry)
            .build_map(&schema)
            .unwrap();

        let v = map.get("#/$defs/Address").unwrap();
        assert!(v.is_valid(&json!({"city": "NYC", "zip": "10001"})));
        assert!(!v.is_valid(&json!({"zip": "10001"}))); // missing required "city"
    }

    #[test]
    fn test_validator_map_with_registry_multiple_refs() {
        let user_schema = json!({
            "type": "object",
            "properties": {"name": {"type": "string"}},
            "required": ["name"]
        });
        let role_schema = json!({
            "type": "string",
            "enum": ["admin", "user", "guest"]
        });
        let registry = crate::Registry::new()
            .add("https://example.com/user.json", &user_schema)
            .expect("valid resource")
            .add("https://example.com/role.json", &role_schema)
            .expect("valid resource")
            .prepare()
            .expect("registry build failed");

        let schema = json!({
            "$defs": {
                "User": {"$ref": "https://example.com/user.json"},
                "Role": {"$ref": "https://example.com/role.json"}
            }
        });
        let map = crate::options()
            .with_registry(&registry)
            .build_map(&schema)
            .unwrap();

        assert!(map["#/$defs/User"].is_valid(&json!({"name": "Alice"})));
        assert!(!map["#/$defs/User"].is_valid(&json!({})));
        assert!(map["#/$defs/Role"].is_valid(&json!("admin")));
        assert!(!map["#/$defs/Role"].is_valid(&json!("superuser")));
    }

    #[cfg(all(feature = "resolve-async", not(target_family = "wasm")))]
    #[tokio::test]
    async fn test_async_validator_map_with_registry() {
        let address_schema = json!({
            "type": "object",
            "properties": {"city": {"type": "string"}},
            "required": ["city"]
        });
        let registry = crate::Registry::new()
            .add("https://example.com/address.json", &address_schema)
            .expect("valid resource")
            .prepare()
            .expect("registry build failed");

        let schema = json!({
            "$defs": {
                "Address": {"$ref": "https://example.com/address.json"}
            }
        });
        let map = crate::async_options()
            .with_registry(&registry)
            .build_map(&schema)
            .await
            .unwrap();

        let v = map.get("#/$defs/Address").unwrap();
        assert!(v.is_valid(&json!({"city": "NYC"})));
        assert!(!v.is_valid(&json!({})));
    }

    #[cfg(all(feature = "resolve-async", not(target_family = "wasm")))]
    #[tokio::test]
    async fn test_async_validator_map_with_registry_multiple_refs() {
        let user_schema = json!({
            "type": "object",
            "properties": {"name": {"type": "string"}},
            "required": ["name"]
        });
        let role_schema = json!({
            "type": "string",
            "enum": ["admin", "user", "guest"]
        });
        let registry = crate::Registry::new()
            .add("https://example.com/user.json", &user_schema)
            .expect("valid resource")
            .add("https://example.com/role.json", &role_schema)
            .expect("valid resource")
            .prepare()
            .expect("registry build failed");

        let schema = json!({
            "$defs": {
                "User": {"$ref": "https://example.com/user.json"},
                "Role": {"$ref": "https://example.com/role.json"}
            }
        });
        let map = crate::async_options()
            .with_registry(&registry)
            .build_map(&schema)
            .await
            .unwrap();

        assert!(map["#/$defs/User"].is_valid(&json!({"name": "Alice"})));
        assert!(!map["#/$defs/User"].is_valid(&json!({})));
        assert!(map["#/$defs/Role"].is_valid(&json!("admin")));
        assert!(!map["#/$defs/Role"].is_valid(&json!("superuser")));
    }
}
