//! Argon2id password hashing.
//!
//! `hash_password` returns a PHC-encoded string suitable for storage in
//! `users.password_hash`. `verify_password` checks a candidate against a
//! stored hash. Default Argon2 params are argon2id with the crate's
//! recommended cost (m=19456 KiB, t=2, p=1) — OWASP-aligned.

use argon2::password_hash::rand_core::OsRng;
use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
use argon2::Argon2;

use crate::error::{AppError, AppResult};

pub fn hash_password(plain: &str) -> AppResult<String> {
    let salt = SaltString::generate(&mut OsRng);
    Argon2::default()
        .hash_password(plain.as_bytes(), &salt)
        .map(|h| h.to_string())
        .map_err(|e| AppError::Other(anyhow::anyhow!("password hash failed: {e}")))
}

pub fn verify_password(plain: &str, phc: &str) -> bool {
    let Ok(hash) = PasswordHash::new(phc) else {
        return false;
    };
    Argon2::default()
        .verify_password(plain.as_bytes(), &hash)
        .is_ok()
}

/// Async wrapper around [`hash_password`] that offloads the CPU- and
/// memory-heavy Argon2 work to the blocking pool. Async handlers must call
/// this rather than the sync primitive: at ~15-50 ms per hash, running it
/// inline stalls every other task sharing the runtime worker thread.
pub async fn hash_password_async(plain: String) -> AppResult<String> {
    tokio::task::spawn_blocking(move || hash_password(&plain))
        .await
        .map_err(|e| AppError::Other(anyhow::anyhow!("password hash task join: {e}")))?
}

/// Async wrapper around [`verify_password`]. A join failure (blocking pool
/// gone at shutdown, panic) resolves to `false` — the caller only ever uses
/// the result as a boolean gate, and denying auth is the safe default.
pub async fn verify_password_async(plain: String, phc: String) -> bool {
    tokio::task::spawn_blocking(move || verify_password(&plain, &phc))
        .await
        .unwrap_or(false)
}

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

    #[test]
    fn hash_then_verify_roundtrip() {
        let phc = hash_password("correct horse battery staple").unwrap();
        assert!(phc.starts_with("$argon2id$"));
        assert!(verify_password("correct horse battery staple", &phc));
    }

    #[test]
    fn verify_rejects_wrong_password() {
        let phc = hash_password("hunter2").unwrap();
        assert!(!verify_password("hunter3", &phc));
    }

    #[test]
    fn verify_rejects_malformed_hash() {
        assert!(!verify_password("anything", "not a real phc string"));
    }

    #[test]
    fn hashes_are_salted() {
        let a = hash_password("same").unwrap();
        let b = hash_password("same").unwrap();
        assert_ne!(a, b, "two hashes of the same password must differ (salt)");
    }

    #[tokio::test]
    async fn async_hash_then_verify_roundtrip() {
        let phc = hash_password_async("correct horse battery staple".to_string())
            .await
            .unwrap();
        assert!(phc.starts_with("$argon2id$"));
        assert!(verify_password_async("correct horse battery staple".to_string(), phc).await);
    }

    #[tokio::test]
    async fn async_verify_rejects_wrong_password() {
        let phc = hash_password_async("hunter2".to_string()).await.unwrap();
        assert!(!verify_password_async("hunter3".to_string(), phc).await);
    }

    #[tokio::test]
    async fn async_verify_rejects_malformed_hash() {
        assert!(!verify_password_async("anything".to_string(), "not a real phc".to_string()).await);
    }
}
