//! BLAKE3 integrity verification for signal ledger checkpoints.
//!
//! Computes a hash over all serialized entry payloads before writing the
//! checkpoint meta, and verifies it on restore. A hash mismatch indicates
//! silent data corruption (bit rot, partial write, filesystem bug).

/// Compute a BLAKE3 hash over the concatenated checkpoint entry payloads.
///
/// Each value is length-prefixed to prevent ambiguous concatenation.
/// Returns a 32-byte BLAKE3 hash.
#[must_use]
pub fn hash_checkpoint_payload(entry_values: &[Vec<u8>]) -> [u8; 32] {
    let mut hasher = blake3::Hasher::new();
    for value in entry_values {
        // Length-prefix each value to prevent ambiguous concatenation.
        hasher.update(&(value.len() as u64).to_le_bytes());
        hasher.update(value);
    }
    *hasher.finalize().as_bytes()
}

/// Verify a checkpoint payload against its expected BLAKE3 hash.
///
/// Returns `true` if the hash matches or if `expected_hash` is all zeros
/// (V1 compatibility: no hash verification).
#[must_use]
pub fn verify_checkpoint_payload(entry_values: &[Vec<u8>], expected_hash: &[u8; 32]) -> bool {
    // V1 compatibility: all-zero hash means "no verification".
    if expected_hash == &[0u8; 32] {
        return true;
    }
    let actual = hash_checkpoint_payload(entry_values);
    actual == *expected_hash
}

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

    #[test]
    fn hash_verification_catches_corruption() {
        let values = vec![vec![1u8, 2, 3], vec![4, 5, 6]];
        let hash = hash_checkpoint_payload(&values);

        // Correct values verify.
        assert!(verify_checkpoint_payload(&values, &hash));

        // Corrupted values fail verification.
        let corrupt = vec![vec![1u8, 2, 99], vec![4, 5, 6]];
        assert!(!verify_checkpoint_payload(&corrupt, &hash));
    }

    #[test]
    fn zero_hash_skips_verification() {
        let values = vec![vec![1u8, 2, 3]];
        let zero_hash = [0u8; 32];
        assert!(verify_checkpoint_payload(&values, &zero_hash));
    }

    #[test]
    fn hash_is_order_dependent() {
        let a = vec![vec![1u8, 2], vec![3, 4]];
        let b = vec![vec![3u8, 4], vec![1, 2]];
        let hash_a = hash_checkpoint_payload(&a);
        let hash_b = hash_checkpoint_payload(&b);
        assert_ne!(hash_a, hash_b);
    }

    #[test]
    fn empty_payload_has_deterministic_hash() {
        let empty: Vec<Vec<u8>> = vec![];
        let hash1 = hash_checkpoint_payload(&empty);
        let hash2 = hash_checkpoint_payload(&empty);
        assert_eq!(hash1, hash2);
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod integrity_proptests {
    use proptest::prelude::*;

    use super::*;

    proptest! {
        #[test]
        fn hash_roundtrip(
            values in proptest::collection::vec(
                proptest::collection::vec(proptest::prelude::any::<u8>(), 0..100),
                0..50
            ),
        ) {
            let hash = hash_checkpoint_payload(&values);
            prop_assert!(verify_checkpoint_payload(&values, &hash));
        }
    }
}
