//! rok-validate — request validation for the rok ecosystem.
//!
//! # Quick start
//!
//! ```rust,ignore
//! use rok_validate::{Valid, Validate};
//! use serde::Deserialize;
//!
//! #[derive(Deserialize, Validate)]
//! pub struct CreateUserRequest {
//!     #[validate(required, email)]
//!     pub email: String,
//!
//!     #[validate(required, min = 8, max = 128)]
//!     pub password: String,
//!
//!     #[validate(required, same = "password")]
//!     pub password_confirmation: String,
//!
//!     #[validate(optional, max = 100)]
//!     pub name: Option<String>,
//! }
//!
//! // In your Axum handler — body is guaranteed valid:
//! pub async fn store(Valid(body): Valid<CreateUserRequest>) -> impl IntoResponse {
//!     // body.email, body.password etc. are all validated
//! }
//! ```
//!
//! # Supported rules
//!
//! | Attribute | Checks |
//! |---|---|
//! | `required` | non-None and non-empty |
//! | `optional` | skip remaining rules if absent |
//! | `email` | contains `@`, domain has `.` |
//! | `url` | starts with `http://` or `https://` |
//! | `numeric` | string parses as a finite number |
//! | `min = N` | string: ≥ N chars; number: ≥ N |
//! | `max = N` | string: ≤ N chars; number: ≤ N |
//! | `same = "field"` | must equal another field |

pub mod errors;
pub mod rules;
pub mod sort;

pub use errors::ValidationErrors;
pub use sort::SortParam;

// Re-export the derive macros so users only need `use rok_validate::Validate;`.
pub use rok_validate_macros::{Filterable, Sortable, Validate};

/// Axum `Valid<T>` extractor. Enable with `features = ["axum"]`.
#[cfg(feature = "axum")]
pub mod extractor;

#[cfg(feature = "axum")]
pub use extractor::{
    FormRejection, FormRequest, QueryValidationRejection, Valid, ValidRejection, ValidatedForm,
    ValidatedQuery,
};

// ── Validate trait ────────────────────────────────────────────────────────────

/// Synchronous validation.  Implement via `#[derive(Validate)]`.
///
/// ```rust,ignore
/// use rok_validate::{Validate, ValidationErrors};
///
/// pub struct MyRequest { pub name: String }
///
/// impl Validate for MyRequest {
///     fn validate(&self) -> Result<(), ValidationErrors> {
///         let mut errors = ValidationErrors::new();
///         if self.name.is_empty() {
///             errors.add("name", "The name field is required.");
///         }
///         if errors.is_empty() { Ok(()) } else { Err(errors) }
///     }
/// }
/// ```
pub trait Validate {
    fn validate(&self) -> Result<(), ValidationErrors>;
}

// ── Tests (unit tests that don't use derive macros) ───────────────────────────

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

    // ── ValidationErrors::prefix / merge ──────────────────────────────────────

    #[test]
    fn prefix_prepends_key() {
        let mut e = ValidationErrors::new();
        e.add("zip", "required");
        let prefixed = e.prefix("address");
        let inner = prefixed.into_inner();
        assert!(inner.contains_key("address.zip"));
        assert!(!inner.contains_key("zip"));
    }

    #[test]
    fn merge_combines_errors() {
        let mut a = ValidationErrors::new();
        a.add("email", "invalid");
        let mut b = ValidationErrors::new();
        b.add("name", "required");
        b.add("email", "too long");
        a.merge(b);
        let inner = a.into_inner();
        assert_eq!(inner["email"].len(), 2);
        assert!(inner.contains_key("name"));
    }

    // ── SortParam ─────────────────────────────────────────────────────────────

    #[test]
    fn sort_param_asc_desc() {
        let sp = SortParam::parse("-created_at,email", &["created_at", "email", "name"]);
        assert_eq!(
            sp.0,
            vec![
                ("created_at".to_string(), false),
                ("email".to_string(), true),
            ]
        );
    }

    #[test]
    fn sort_param_drops_unknown() {
        let sp = SortParam::parse("-created_at,injected_col", &["created_at", "email"]);
        assert_eq!(sp.0.len(), 1);
        assert_eq!(sp.0[0].0, "created_at");
    }

    #[test]
    fn sort_param_empty_allowlist_allows_all() {
        let sp = SortParam::parse("a,b,c", &[]);
        assert_eq!(sp.0.len(), 3);
    }

    #[test]
    fn sort_param_empty_string() {
        let sp = SortParam::parse("", &[]);
        assert!(sp.0.is_empty());
    }

    #[test]
    fn sort_param_single_desc() {
        let sp = SortParam::parse("-name", &["name"]);
        assert_eq!(sp.0, vec![("name".to_string(), false)]);
    }

    #[test]
    fn sort_param_deserialize() {
        use serde::Deserialize;
        #[derive(Deserialize)]
        struct Q {
            sort: SortParam,
        }
        let q: Q = serde_json::from_str(r#"{"sort":"-created_at,email"}"#).unwrap();
        assert_eq!(q.sort.0[0], ("created_at".to_string(), false));
        assert_eq!(q.sort.0[1], ("email".to_string(), true));
    }
}
