//! Shortid generation and validation.
//!
//! Per ADR-0005, the `shortid` user-facing type is a base58
//! random identifier of 10–12 characters with no ambiguous
//! glyphs (no `0`/`O`/`I`/`l`). The generator is small enough
//! to live in the DSL crate alongside the type definition.

use rand::RngExt;

/// Base58 alphabet — Bitcoin-style. 0 / O / I / l are excluded
/// because they are easily confused in print.
const ALPHABET: &[u8; 58] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";

const DEFAULT_LEN: usize = 10;

/// Length bounds accepted on user-supplied shortid values.
pub const MIN_LEN: usize = 10;
pub const MAX_LEN: usize = 12;

/// Generate a fresh shortid using the thread-local RNG.
#[must_use]
pub fn generate() -> String {
    generate_with_rng(&mut rand::rng())
}

/// Generate a shortid from a caller-supplied RNG.
///
/// Lets `seed --seed <n>` produce **reproducible** shortid values
/// (ADR-0048 D4) by threading its seeded RNG through, while the default
/// [`generate`] keeps its thread-RNG behaviour for ordinary inserts.
#[must_use]
pub fn generate_with_rng<R: RngExt + ?Sized>(rng: &mut R) -> String {
    let mut out = String::with_capacity(DEFAULT_LEN);
    for _ in 0..DEFAULT_LEN {
        let idx = rng.random_range(0..ALPHABET.len());
        out.push(ALPHABET[idx] as char);
    }
    out
}

/// Validate a user-supplied shortid value.
pub fn validate(value: &str) -> Result<(), String> {
    if value.len() < MIN_LEN || value.len() > MAX_LEN {
        return Err(format!(
            "shortid must be {MIN_LEN}–{MAX_LEN} characters; got {} character(s)",
            value.chars().count()
        ));
    }
    for c in value.chars() {
        if !ALPHABET.contains(&(c as u8)) {
            return Err(format!(
                "shortid contains '{c}', which is not in the base58 alphabet \
                 (no 0, O, I, or l; ASCII letters and digits otherwise)"
            ));
        }
    }
    Ok(())
}

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

    #[test]
    fn generated_ids_have_default_length() {
        let id = generate();
        assert_eq!(id.len(), DEFAULT_LEN);
    }

    #[test]
    fn generated_ids_use_only_base58_alphabet() {
        for _ in 0..100 {
            let id = generate();
            for c in id.chars() {
                assert!(
                    ALPHABET.contains(&(c as u8)),
                    "char {c:?} not in base58 alphabet"
                );
            }
        }
    }

    #[test]
    fn generated_ids_are_not_all_identical() {
        // Probabilistically extremely unlikely with a good RNG;
        // catches a wholly broken generator (constant output).
        let a = generate();
        let b = generate();
        let c = generate();
        assert!(
            a != b || b != c,
            "all three generated ids were identical: {a}, {b}, {c}"
        );
    }

    #[test]
    fn validate_accepts_well_formed_values() {
        assert!(validate("23456789Ab").is_ok()); // 10 chars
        assert!(validate("23456789AbCD").is_ok()); // 12 chars
    }

    #[test]
    fn validate_rejects_too_short_or_too_long() {
        let err = validate("short").unwrap_err();
        assert!(err.contains("characters"));
        let err = validate("waytoolongafornow").unwrap_err();
        assert!(err.contains("characters"));
    }

    #[test]
    fn validate_rejects_ambiguous_glyphs() {
        for bad in ["0aaaaaaaaa", "Oaaaaaaaaa", "Iaaaaaaaaa", "laaaaaaaaa"] {
            let err = validate(bad).unwrap_err();
            assert!(err.contains("base58"), "for {bad}: {err}");
        }
    }
}
