waddling_errors_hash/
wdp.rs

1//! WDP (Waddling Diagnostic Protocol) Conformant Hash Implementation
2//!
3//! This module provides hash functions that are fully conformant with the
4//! WDP specification (Parts 5 and 7).
5//!
6//! ## WDP Part 5: Compact IDs
7//!
8//! Error codes are hashed using xxHash3 with the seed `0x000031762D706477`:
9//! - Input is normalized (trimmed and uppercased)
10//! - Output is 5 Base62 characters
11//!
12//! ## WDP Part 7: Namespaces
13//!
14//! Namespaces are hashed using xxHash3 with a different seed `0x762D736E2D706477`:
15//! - This prevents collisions between namespace and code hashes
16//! - Combined format: `namespace_hash-code_hash` (e.g., `h4tYw2-81E9g`)
17//!
18//! ## Example
19//!
20//! ```
21//! use waddling_errors_hash::wdp::{
22//!     compute_wdp_hash,
23//!     compute_wdp_namespace_hash,
24//!     compute_wdp_full_id,
25//! };
26//!
27//! // Hash an error code (WDP Part 5)
28//! let hash = compute_wdp_hash("E.Auth.Token.001");
29//! assert_eq!(hash.len(), 5);
30//!
31//! // Hash a namespace (WDP Part 7)
32//! let ns_hash = compute_wdp_namespace_hash("auth_lib");
33//! assert_eq!(ns_hash.len(), 5);
34//!
35//! // Combined identifier (namespace_hash-code_hash)
36//! let full_id = compute_wdp_full_id("auth_lib", "E.Auth.Token.001");
37//! assert_eq!(full_id.len(), 11); // 5 + 1 + 5
38//! assert!(full_id.contains('-'));
39//! ```
40
41#[cfg(not(feature = "std"))]
42extern crate alloc;
43
44#[cfg(feature = "std")]
45use std::string::String;
46
47#[cfg(not(feature = "std"))]
48use alloc::string::String;
49
50use crate::base62::to_base62;
51
52// =============================================================================
53// WDP Seed Constants (NORMATIVE)
54// =============================================================================
55
56/// WDP v1 seed for error code hashes (Part 5: Compact IDs)
57///
58/// Seed string: `"wdp-v1"`
59/// UTF-8 bytes: `[0x77, 0x64, 0x70, 0x2D, 0x76, 0x31]`
60/// Zero-padded to 8 bytes: `[0x77, 0x64, 0x70, 0x2D, 0x76, 0x31, 0x00, 0x00]`
61/// Little-endian u64: `0x000031762D706477`
62///
63/// This value is specified in WDP Part 5, Section 4.5.1.
64pub const WDP_CODE_SEED: u64 = 0x000031762D706477;
65
66/// WDP v1 seed for namespace hashes (Part 7: Namespaces)
67///
68/// Seed string: `"wdp-ns-v1"`
69/// UTF-8 bytes: `[0x77, 0x64, 0x70, 0x2D, 0x6E, 0x73, 0x2D, 0x76, 0x31]`
70/// First 8 bytes: `[0x77, 0x64, 0x70, 0x2D, 0x6E, 0x73, 0x2D, 0x76]`
71/// Little-endian u64: `0x762D736E2D706477`
72///
73/// This value is specified in WDP Part 7, Section 4.2.
74pub const WDP_NAMESPACE_SEED: u64 = 0x762D736E2D706477;
75
76// =============================================================================
77// Input Normalization (WDP Part 5, Section 3)
78// =============================================================================
79
80/// Normalize input for WDP hash computation
81///
82/// Per WDP Part 5, Section 3.3:
83/// 1. Trim leading and trailing whitespace
84/// 2. Convert to uppercase
85///
86/// This ensures case-insensitive lookups and typo resilience:
87/// - `E.Auth.Token.001` → `E.AUTH.TOKEN.001`
88/// - `e.auth.token.001` → `E.AUTH.TOKEN.001`
89/// - `  E.Auth.Token.001  ` → `E.AUTH.TOKEN.001`
90///
91/// All produce the same hash.
92#[inline]
93pub fn normalize_wdp_input(input: &str) -> String {
94    input.trim().to_uppercase()
95}
96
97// =============================================================================
98// WDP Part 5: Compact ID Hash Functions
99// =============================================================================
100
101/// Compute a WDP-conformant hash for an error code (Part 5: Compact IDs)
102///
103/// This function implements the WDP Part 5 specification:
104/// 1. Normalize input (trim + uppercase)
105/// 2. Hash with xxHash3 using seed `0x000031762D706477`
106/// 3. Encode as 5-character Base62
107///
108/// # Example
109///
110/// ```
111/// use waddling_errors_hash::wdp::compute_wdp_hash;
112///
113/// // All of these produce the same hash due to normalization:
114/// let hash1 = compute_wdp_hash("E.Auth.Token.001");
115/// let hash2 = compute_wdp_hash("E.AUTH.TOKEN.001");
116/// let hash3 = compute_wdp_hash("e.auth.token.001");
117/// let hash4 = compute_wdp_hash("  E.Auth.Token.001  ");
118///
119/// assert_eq!(hash1, hash2);
120/// assert_eq!(hash2, hash3);
121/// assert_eq!(hash3, hash4);
122/// assert_eq!(hash1.len(), 5);
123/// ```
124///
125/// # WDP Conformance
126///
127/// This function is fully conformant with WDP Part 5: Compact IDs.
128pub fn compute_wdp_hash(input: &str) -> String {
129    use xxhash_rust::xxh3::xxh3_64_with_seed;
130
131    // Step 1: Normalize (trim + uppercase)
132    let normalized = normalize_wdp_input(input);
133
134    // Step 2: Hash with WDP seed
135    let hash = xxh3_64_with_seed(normalized.as_bytes(), WDP_CODE_SEED);
136
137    // Step 3: Convert to 5-char Base62
138    hash_to_base62(hash)
139}
140
141/// Compute WDP hash without normalization (for pre-normalized input)
142///
143/// Use this when you know the input is already normalized (uppercase, trimmed).
144/// This avoids the allocation from `to_uppercase()`.
145///
146/// # Safety Note
147///
148/// If the input is not properly normalized, the hash will differ from
149/// `compute_wdp_hash()` and may not match other WDP implementations.
150#[inline]
151pub fn compute_wdp_hash_normalized(normalized_input: &str) -> String {
152    use xxhash_rust::xxh3::xxh3_64_with_seed;
153
154    let hash = xxh3_64_with_seed(normalized_input.as_bytes(), WDP_CODE_SEED);
155    hash_to_base62(hash)
156}
157
158// =============================================================================
159// WDP Part 7: Namespace Hash Functions
160// =============================================================================
161
162/// Compute a WDP-conformant hash for a namespace (Part 7: Namespaces)
163///
164/// This function implements the WDP Part 7 specification:
165/// 1. Hash namespace with xxHash3 using seed `0x762D736E2D706477`
166/// 2. Encode as 5-character Base62
167///
168/// Note: Namespaces are NOT normalized (they should already be lowercase snake_case).
169///
170/// # Example
171///
172/// ```
173/// use waddling_errors_hash::wdp::compute_wdp_namespace_hash;
174///
175/// let hash = compute_wdp_namespace_hash("auth_lib");
176/// assert_eq!(hash.len(), 5);
177/// ```
178///
179/// # WDP Conformance
180///
181/// This function is fully conformant with WDP Part 7: Namespaces.
182pub fn compute_wdp_namespace_hash(namespace: &str) -> String {
183    use xxhash_rust::xxh3::xxh3_64_with_seed;
184
185    // Namespace uses different seed to prevent collisions with code hashes
186    let hash = xxh3_64_with_seed(namespace.as_bytes(), WDP_NAMESPACE_SEED);
187    hash_to_base62(hash)
188}
189
190// =============================================================================
191// Combined Identifiers
192// =============================================================================
193
194/// Compute a full WDP identifier: `namespace_hash-code_hash`
195///
196/// Per WDP Part 7, Section 5.2, the combined format is:
197/// - `namespace_hash`: 5 Base62 characters
198/// - separator: single hyphen `-`
199/// - `code_hash`: 5 Base62 characters
200/// - Total: 11 characters
201///
202/// # Example
203///
204/// ```
205/// use waddling_errors_hash::wdp::compute_wdp_full_id;
206///
207/// let full_id = compute_wdp_full_id("auth_lib", "E.Auth.Token.001");
208/// assert_eq!(full_id.len(), 11);
209/// assert!(full_id.contains('-'));
210///
211/// // Format: "h4tYw2-81E9g" (namespace_hash-code_hash)
212/// let parts: Vec<&str> = full_id.split('-').collect();
213/// assert_eq!(parts.len(), 2);
214/// assert_eq!(parts[0].len(), 5); // namespace hash
215/// assert_eq!(parts[1].len(), 5); // code hash
216/// ```
217///
218/// # WDP Conformance
219///
220/// This function is fully conformant with WDP Part 5 and Part 7.
221pub fn compute_wdp_full_id(namespace: &str, code: &str) -> String {
222    let ns_hash = compute_wdp_namespace_hash(namespace);
223    let code_hash = compute_wdp_hash(code);
224
225    #[cfg(feature = "std")]
226    {
227        format!("{}-{}", ns_hash, code_hash)
228    }
229
230    #[cfg(not(feature = "std"))]
231    {
232        use alloc::format;
233        format!("{}-{}", ns_hash, code_hash)
234    }
235}
236
237/// Parse a WDP full identifier into namespace hash and code hash
238///
239/// Returns `Some((namespace_hash, code_hash))` if valid, `None` otherwise.
240///
241/// # Example
242///
243/// ```
244/// use waddling_errors_hash::wdp::parse_wdp_full_id;
245///
246/// // With namespace
247/// let result = parse_wdp_full_id("h4tYw-81E9g");
248/// assert_eq!(result, Some(("h4tYw", "81E9g")));
249///
250/// // Without namespace (legacy 5-char format)
251/// let result = parse_wdp_full_id("81E9g");
252/// assert_eq!(result, Some(("", "81E9g")));
253///
254/// // Invalid
255/// let result = parse_wdp_full_id("invalid");
256/// assert_eq!(result, None);
257/// ```
258pub fn parse_wdp_full_id(full_id: &str) -> Option<(&str, &str)> {
259    if full_id.len() == 5 && full_id.chars().all(|c| c.is_ascii_alphanumeric()) {
260        // Legacy format: just code hash
261        Some(("", full_id))
262    } else if full_id.len() == 11 {
263        // Full format: namespace_hash-code_hash (no allocation needed)
264        // Expected format: XXXXX-XXXXX where X is alphanumeric
265        let bytes = full_id.as_bytes();
266        if bytes[5] == b'-'
267            && full_id[..5].chars().all(|c| c.is_ascii_alphanumeric())
268            && full_id[6..].chars().all(|c| c.is_ascii_alphanumeric())
269        {
270            Some((&full_id[..5], &full_id[6..]))
271        } else {
272            None
273        }
274    } else {
275        None
276    }
277}
278
279// =============================================================================
280// Verification Functions
281// =============================================================================
282
283/// Verify that a hash matches the input code (WDP-conformant)
284///
285/// # Example
286///
287/// ```
288/// use waddling_errors_hash::wdp::{compute_wdp_hash, verify_wdp_hash};
289///
290/// let code = "E.Auth.Token.001";
291/// let hash = compute_wdp_hash(code);
292///
293/// assert!(verify_wdp_hash(code, &hash));
294/// assert!(verify_wdp_hash("e.auth.token.001", &hash)); // Case-insensitive!
295/// assert!(!verify_wdp_hash("E.Auth.Token.002", &hash));
296/// ```
297pub fn verify_wdp_hash(input: &str, hash: &str) -> bool {
298    compute_wdp_hash(input) == hash
299}
300
301/// Verify that a namespace hash matches the input namespace
302///
303/// # Example
304///
305/// ```
306/// use waddling_errors_hash::wdp::{compute_wdp_namespace_hash, verify_wdp_namespace_hash};
307///
308/// let namespace = "auth_lib";
309/// let hash = compute_wdp_namespace_hash(namespace);
310///
311/// assert!(verify_wdp_namespace_hash(namespace, &hash));
312/// assert!(!verify_wdp_namespace_hash("other_lib", &hash));
313/// ```
314pub fn verify_wdp_namespace_hash(namespace: &str, hash: &str) -> bool {
315    compute_wdp_namespace_hash(namespace) == hash
316}
317
318// =============================================================================
319// Compile-Time (const) Hash Functions
320// =============================================================================
321
322/// Base62 character set for const evaluation
323const BASE62_CHARS_CONST: &[u8; 62] =
324    b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
325
326/// Convert a u64 hash to a 5-character Base62 array (const-compatible)
327///
328/// This is a const fn version of hash_to_base62 that returns a fixed-size array
329/// instead of a String, making it suitable for compile-time evaluation.
330#[inline]
331pub const fn const_hash_to_base62(hash: u64) -> [u8; 5] {
332    // Take first 5 bytes (40 bits) of the hash
333    let bytes: [u8; 5] = [
334        (hash >> 32) as u8,
335        (hash >> 24) as u8,
336        (hash >> 16) as u8,
337        (hash >> 8) as u8,
338        hash as u8,
339    ];
340
341    // Combine into a 40-bit number
342    let mut num: u64 = 0;
343    num = (num << 8) | (bytes[0] as u64);
344    num = (num << 8) | (bytes[1] as u64);
345    num = (num << 8) | (bytes[2] as u64);
346    num = (num << 8) | (bytes[3] as u64);
347    num = (num << 8) | (bytes[4] as u64);
348
349    // Convert to base62
350    let mut result = [0u8; 5];
351    let mut n = num;
352
353    // Manual unrolling since we can't use for loops in const fn on mutable state easily
354    result[4] = BASE62_CHARS_CONST[(n % 62) as usize];
355    n /= 62;
356    result[3] = BASE62_CHARS_CONST[(n % 62) as usize];
357    n /= 62;
358    result[2] = BASE62_CHARS_CONST[(n % 62) as usize];
359    n /= 62;
360    result[1] = BASE62_CHARS_CONST[(n % 62) as usize];
361    n /= 62;
362    result[0] = BASE62_CHARS_CONST[(n % 62) as usize];
363
364    result
365}
366
367/// Compute WDP hash at compile time from a byte slice
368///
369/// This function uses const_xxh3 for compile-time hash computation.
370/// The input should be pre-normalized (uppercase ASCII bytes).
371///
372/// # Example
373///
374/// ```
375/// use waddling_errors_hash::wdp::{const_wdp_hash_bytes, const_hash_to_base62};
376///
377/// const HASH: u64 = const_wdp_hash_bytes(b"E.AUTH.TOKEN.001");
378/// const BASE62: [u8; 5] = const_hash_to_base62(HASH);
379/// ```
380#[inline]
381pub const fn const_wdp_hash_bytes(input: &[u8]) -> u64 {
382    use xxhash_rust::const_xxh3::xxh3_64_with_seed;
383    xxh3_64_with_seed(input, WDP_CODE_SEED)
384}
385
386/// Compute a WDP-conformant hash at compile time
387///
388/// This is a const fn that computes the hash from pre-normalized uppercase bytes.
389/// Returns a 5-byte array that can be converted to a &str.
390///
391/// # Example
392///
393/// ```
394/// use waddling_errors_hash::wdp::const_wdp_hash;
395///
396/// const HASH_BYTES: [u8; 5] = const_wdp_hash(b"E.AUTH.TOKEN.001");
397/// // Convert to str at compile time if needed
398/// ```
399#[inline]
400pub const fn const_wdp_hash(normalized_uppercase_input: &[u8]) -> [u8; 5] {
401    let hash = const_wdp_hash_bytes(normalized_uppercase_input);
402    const_hash_to_base62(hash)
403}
404
405/// Format a sequence number as 3-digit zero-padded string bytes
406///
407/// Returns a 3-byte array with the formatted number.
408/// Used for building WDP-conformant error codes at compile time.
409///
410/// # Example
411///
412/// ```
413/// use waddling_errors_hash::wdp::const_format_sequence;
414///
415/// assert_eq!(&const_format_sequence(1), b"001");
416/// assert_eq!(&const_format_sequence(42), b"042");
417/// assert_eq!(&const_format_sequence(999), b"999");
418/// ```
419#[inline]
420pub const fn const_format_sequence(seq_num: u16) -> [u8; 3] {
421    let d0 = (seq_num / 100) % 10;
422    let d1 = (seq_num / 10) % 10;
423    let d2 = seq_num % 10;
424
425    [b'0' + d0 as u8, b'0' + d1 as u8, b'0' + d2 as u8]
426}
427
428/// Compute WDP hash from error code parts at compile time
429///
430/// Takes the four parts of an error code (severity, component, primary, sequence)
431/// and computes the WDP-conformant hash. All string inputs must be uppercase.
432///
433/// # Arguments
434///
435/// * `severity` - Severity character as byte (e.g., b'E', b'W', b'C')
436/// * `component` - Component value as uppercase bytes (e.g., b"AUTH")
437/// * `primary` - Primary value as uppercase bytes (e.g., b"TOKEN")
438/// * `seq_num` - Sequence number (1-999)
439///
440/// # Example
441///
442/// ```
443/// use waddling_errors_hash::wdp::const_wdp_hash_from_parts;
444///
445/// // Hash for "E.AUTH.TOKEN.001"
446/// const HASH: [u8; 5] = const_wdp_hash_from_parts(b'E', b"AUTH", b"TOKEN", 1);
447/// ```
448#[inline]
449pub const fn const_wdp_hash_from_parts(
450    severity: u8,
451    component: &[u8],
452    primary: &[u8],
453    seq_num: u16,
454) -> [u8; 5] {
455    // Maximum expected length: 1 (severity) + 1 (.) + 16 (component) + 1 (.) + 16 (primary) + 1 (.) + 3 (seq) = 39
456    // Using 64 bytes for safety
457    let mut buffer = [0u8; 64];
458    let mut pos = 0;
459
460    // Severity
461    buffer[pos] = severity;
462    pos += 1;
463
464    // Dot
465    buffer[pos] = b'.';
466    pos += 1;
467
468    // Component (uppercase)
469    let mut i = 0;
470    while i < component.len() {
471        buffer[pos] = to_uppercase_byte(component[i]);
472        pos += 1;
473        i += 1;
474    }
475
476    // Dot
477    buffer[pos] = b'.';
478    pos += 1;
479
480    // Primary (uppercase)
481    i = 0;
482    while i < primary.len() {
483        buffer[pos] = to_uppercase_byte(primary[i]);
484        pos += 1;
485        i += 1;
486    }
487
488    // Dot
489    buffer[pos] = b'.';
490    pos += 1;
491
492    // Sequence number (3-digit zero-padded)
493    let seq_bytes = const_format_sequence(seq_num);
494    buffer[pos] = seq_bytes[0];
495    buffer[pos + 1] = seq_bytes[1];
496    buffer[pos + 2] = seq_bytes[2];
497    pos += 3;
498
499    // Compute hash on the constructed code string
500    const_wdp_hash(slice_to_len(&buffer, pos))
501}
502
503/// Convert a byte to uppercase (const-compatible)
504#[inline]
505const fn to_uppercase_byte(b: u8) -> u8 {
506    if b >= b'a' && b <= b'z' { b - 32 } else { b }
507}
508
509/// Create a slice of specific length from an array (const-compatible workaround)
510///
511/// Note: In const context, we can't return a slice directly from an array slice,
512/// so we use the hash function that takes the full buffer and length hint.
513#[inline]
514const fn slice_to_len(buffer: &[u8; 64], len: usize) -> &[u8] {
515    // Enforce bounds to avoid UB in unsafe slice creation
516    let capped_len = if len > buffer.len() {
517        buffer.len()
518    } else {
519        len
520    };
521    let ptr = buffer.as_ptr();
522    // Safety: capped_len is guaranteed <= buffer.len() by the check above
523    unsafe { core::slice::from_raw_parts(ptr, capped_len) }
524}
525
526// =============================================================================
527// Internal Helpers
528// =============================================================================
529
530/// Convert xxHash3 output to 5-character Base62
531///
532/// Takes the first 40 bits (5 bytes) of the hash and encodes as Base62.
533#[inline]
534fn hash_to_base62(hash: u64) -> String {
535    let bytes = [
536        (hash >> 32) as u8,
537        (hash >> 24) as u8,
538        (hash >> 16) as u8,
539        (hash >> 8) as u8,
540        hash as u8,
541    ];
542    to_base62(&bytes)
543}
544
545// =============================================================================
546// Tests
547// =============================================================================
548
549#[cfg(test)]
550mod tests {
551    use super::*;
552
553    #[test]
554    fn test_wdp_code_seed_value() {
555        // Verify the seed constant matches the calculation
556        let seed_bytes = b"wdp-v1";
557        let mut padded = [0u8; 8];
558        padded[..6].copy_from_slice(seed_bytes);
559        let calculated = u64::from_le_bytes(padded);
560        assert_eq!(calculated, WDP_CODE_SEED);
561        assert_eq!(WDP_CODE_SEED, 0x000031762D706477);
562    }
563
564    #[test]
565    fn test_wdp_namespace_seed_value() {
566        // Verify the namespace seed constant matches the calculation
567        let seed_bytes = b"wdp-ns-v1";
568        let mut padded = [0u8; 8];
569        padded[..8].copy_from_slice(&seed_bytes[..8]);
570        let calculated = u64::from_le_bytes(padded);
571        assert_eq!(calculated, WDP_NAMESPACE_SEED);
572        assert_eq!(WDP_NAMESPACE_SEED, 0x762D736E2D706477);
573    }
574
575    #[test]
576    fn test_normalize_wdp_input() {
577        assert_eq!(normalize_wdp_input("E.Auth.Token.001"), "E.AUTH.TOKEN.001");
578        assert_eq!(normalize_wdp_input("e.auth.token.001"), "E.AUTH.TOKEN.001");
579        assert_eq!(
580            normalize_wdp_input("  E.Auth.Token.001  "),
581            "E.AUTH.TOKEN.001"
582        );
583        assert_eq!(normalize_wdp_input("E.AUTH.TOKEN.001"), "E.AUTH.TOKEN.001");
584    }
585
586    #[test]
587    fn test_compute_wdp_hash_length() {
588        let hash = compute_wdp_hash("E.Auth.Token.001");
589        assert_eq!(hash.len(), 5);
590        assert!(hash.chars().all(|c| c.is_ascii_alphanumeric()));
591    }
592
593    #[test]
594    fn test_compute_wdp_hash_deterministic() {
595        let hash1 = compute_wdp_hash("E.Auth.Token.001");
596        let hash2 = compute_wdp_hash("E.Auth.Token.001");
597        assert_eq!(hash1, hash2);
598    }
599
600    #[test]
601    fn test_compute_wdp_hash_case_insensitive() {
602        let hash1 = compute_wdp_hash("E.Auth.Token.001");
603        let hash2 = compute_wdp_hash("E.AUTH.TOKEN.001");
604        let hash3 = compute_wdp_hash("e.auth.token.001");
605        let hash4 = compute_wdp_hash("E.auth.Token.001");
606
607        assert_eq!(hash1, hash2);
608        assert_eq!(hash2, hash3);
609        assert_eq!(hash3, hash4);
610    }
611
612    #[test]
613    fn test_compute_wdp_hash_whitespace_insensitive() {
614        let hash1 = compute_wdp_hash("E.Auth.Token.001");
615        let hash2 = compute_wdp_hash("  E.Auth.Token.001  ");
616        let hash3 = compute_wdp_hash("\tE.Auth.Token.001\n");
617
618        assert_eq!(hash1, hash2);
619        assert_eq!(hash2, hash3);
620    }
621
622    #[test]
623    fn test_different_codes_produce_different_hashes() {
624        let hash1 = compute_wdp_hash("E.Auth.Token.001");
625        let hash2 = compute_wdp_hash("E.Auth.Token.002");
626        assert_ne!(hash1, hash2);
627    }
628
629    #[test]
630    fn test_compute_wdp_namespace_hash_length() {
631        let hash = compute_wdp_namespace_hash("auth_lib");
632        assert_eq!(hash.len(), 5);
633        assert!(hash.chars().all(|c| c.is_ascii_alphanumeric()));
634    }
635
636    #[test]
637    fn test_namespace_and_code_seeds_differ() {
638        // Same input, different seeds should produce different hashes
639        let input = "test";
640        use xxhash_rust::xxh3::xxh3_64_with_seed;
641
642        let code_hash = xxh3_64_with_seed(input.as_bytes(), WDP_CODE_SEED);
643        let ns_hash = xxh3_64_with_seed(input.as_bytes(), WDP_NAMESPACE_SEED);
644
645        assert_ne!(code_hash, ns_hash);
646    }
647
648    #[test]
649    fn test_compute_wdp_full_id_format() {
650        let full_id = compute_wdp_full_id("auth_lib", "E.Auth.Token.001");
651
652        assert_eq!(full_id.len(), 11);
653        assert!(full_id.contains('-'));
654
655        let parts: Vec<&str> = full_id.split('-').collect();
656        assert_eq!(parts.len(), 2);
657        assert_eq!(parts[0].len(), 5);
658        assert_eq!(parts[1].len(), 5);
659    }
660
661    #[test]
662    fn test_compute_wdp_full_id_components() {
663        let namespace = "auth_lib";
664        let code = "E.Auth.Token.001";
665
666        let ns_hash = compute_wdp_namespace_hash(namespace);
667        let code_hash = compute_wdp_hash(code);
668        let full_id = compute_wdp_full_id(namespace, code);
669
670        let expected = format!("{}-{}", ns_hash, code_hash);
671        assert_eq!(full_id, expected);
672    }
673
674    #[test]
675    fn test_parse_wdp_full_id() {
676        // Full format
677        let result = parse_wdp_full_id("h4tYw-81E9g");
678        assert_eq!(result, Some(("h4tYw", "81E9g")));
679
680        // Legacy format (5-char)
681        let result = parse_wdp_full_id("81E9g");
682        assert_eq!(result, Some(("", "81E9g")));
683
684        // Invalid formats
685        assert_eq!(parse_wdp_full_id(""), None);
686        assert_eq!(parse_wdp_full_id("123"), None);
687        assert_eq!(parse_wdp_full_id("invalid-format-here"), None);
688        assert_eq!(parse_wdp_full_id("h4tYw--81E9g"), None);
689    }
690
691    #[test]
692    fn test_verify_wdp_hash() {
693        let code = "E.Auth.Token.001";
694        let hash = compute_wdp_hash(code);
695
696        assert!(verify_wdp_hash(code, &hash));
697        assert!(verify_wdp_hash("e.auth.token.001", &hash)); // Case-insensitive
698        assert!(!verify_wdp_hash("E.Auth.Token.002", &hash));
699    }
700
701    #[test]
702    fn test_verify_wdp_namespace_hash() {
703        let namespace = "auth_lib";
704        let hash = compute_wdp_namespace_hash(namespace);
705
706        assert!(verify_wdp_namespace_hash(namespace, &hash));
707        assert!(!verify_wdp_namespace_hash("other_lib", &hash));
708    }
709}