//! Validator trait, slice/context types, and category discriminant.

use serde::{Deserialize, Serialize};

use crate::validation::verdict::Verdict;

/// Slice of authoritative state passed to a validator. Phase 1 carries
/// opaque JSON; typed query slices arrive when the query layer
/// (`ontoref-query`) is wired into the runtime in O3.
pub type Slice = serde_json::Value;

/// Bare-fn pointer signature for validators registered via inventory.
/// `#[onto_validator]` emits one entry per annotated function and stores
/// a pointer of this type in the registry.
pub type ValidatorFn = fn(&Slice, &ValidationCtx) -> Verdict;

/// Per-invocation context: which actor is invoking, under which op,
/// and any auxiliary metadata downstream impls may grow.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationCtx {
    /// Id of the actor invoking the operation.
    pub actor_id: String,
    /// Id of the operation whose validation plane is currently running.
    pub op_id: String,
}

impl ValidationCtx {
    /// Convenience constructor used by callers that don't yet have a
    /// session — primarily tests and the noop runtime path.
    pub fn synthetic(actor_id: impl Into<String>, op_id: impl Into<String>) -> Self {
        Self {
            actor_id: actor_id.into(),
            op_id: op_id.into(),
        }
    }
}

/// ADR-026 validator category. Structural validators check
/// well-formedness without state slice; Contextual validators require
/// a slice loaded from authoritative state.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ValidatorCategory {
    /// No slice required — pure check of inputs/effects shape.
    Structural,
    /// Slice required — check fit against current state.
    Contextual,
}

impl ValidatorCategory {
    /// Parse the category from the string discriminant used in the
    /// `#[onto_validator(category = "...")]` attribute.
    pub fn parse(s: &str) -> Option<Self> {
        match s {
            "Structural" => Some(Self::Structural),
            "Contextual" => Some(Self::Contextual),
            _ => None,
        }
    }
}

/// Object-safe validator API. Most concrete validators are bare fns
/// registered via [`crate::validation::ValidatorEntry`]; the trait is
/// also implementable directly for stateful validators (e.g. ones that
/// hold a precomputed lookup table).
pub trait Validator: Send + Sync {
    /// Evaluate `slice` under `ctx` and return a [`Verdict`].
    fn validate(&self, slice: &Slice, ctx: &ValidationCtx) -> Verdict;
}

impl<F> Validator for F
where
    F: Fn(&Slice, &ValidationCtx) -> Verdict + Send + Sync,
{
    fn validate(&self, slice: &Slice, ctx: &ValidationCtx) -> Verdict {
        (self)(slice, ctx)
    }
}

#[cfg(test)]
mod tests {
    use ontoref_derive::onto_validator;

    use super::*;
    use crate::validation::registry::ValidatorEntry;

    /// Dummy validator registered via the macro so the inventory iter
    /// has at least one entry to discover during the gate test.
    #[onto_validator(
        id = "dummy_always_accept",
        description = "Reference validator that accepts every input — exists to verify the \
                       registration path.",
        category = "Structural",
        predicate_ref = "dummy_always_accept"
    )]
    fn dummy_always_accept(_slice: &Slice, _ctx: &ValidationCtx) -> Verdict {
        Verdict::Accept
    }

    #[test]
    fn trait_compiles_for_fn_pointer() {
        let v: fn(&Slice, &ValidationCtx) -> Verdict = dummy_always_accept;
        let ctx = ValidationCtx::synthetic("test", "op:none");
        assert_eq!(v(&serde_json::json!({}), &ctx), Verdict::Accept);
    }

    #[test]
    fn category_parse_roundtrips() {
        assert_eq!(
            ValidatorCategory::parse("Structural"),
            Some(ValidatorCategory::Structural)
        );
        assert_eq!(
            ValidatorCategory::parse("Contextual"),
            Some(ValidatorCategory::Contextual)
        );
        assert_eq!(ValidatorCategory::parse("Bogus"), None);
    }

    #[test]
    fn verdict_is_accept_helper() {
        assert!(Verdict::Accept.is_accept());
        assert!(!Verdict::reject("nope", "validator-x").is_accept());
        assert!(!Verdict::Unknown.is_accept());
    }

    #[test]
    fn dummy_validator_discoverable_via_inventory() {
        let entries: Vec<&ValidatorEntry> = inventory::iter::<ValidatorEntry>().collect();
        assert!(
            entries.iter().any(|e| e.id == "dummy_always_accept"),
            "registered dummy must appear in inventory; got ids: {:?}",
            entries.iter().map(|e| e.id).collect::<Vec<_>>()
        );
    }

    #[test]
    fn dispatch_by_id_calls_dummy() {
        use crate::validation::dispatch;
        let ctx = ValidationCtx::synthetic("test", "op:none");
        let verdict = dispatch::dispatch("dummy_always_accept", &serde_json::json!({}), &ctx)
            .expect("dummy must be registered");
        assert_eq!(verdict, Verdict::Accept);
    }

    #[test]
    fn dispatch_unknown_id_returns_none() {
        use crate::validation::dispatch;
        let ctx = ValidationCtx::synthetic("test", "op:none");
        let result = dispatch::dispatch("not_registered", &serde_json::json!({}), &ctx);
        assert!(result.is_none(), "unknown id must return None");
    }
}
