//! 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()
}

#[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)");
    }
}
