bl4_idb/
types.rs

1//! Shared types for the items database.
2//!
3//! These types are database-agnostic and used by all implementations.
4
5use serde::{Deserialize, Serialize};
6use sha2::{Digest, Sha256};
7use std::collections::HashMap;
8
9/// Verification status for items
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12#[derive(Default)]
13pub enum VerificationStatus {
14    #[default]
15    Unverified,
16    Decoded,
17    Screenshot,
18    Verified,
19}
20
21impl std::fmt::Display for VerificationStatus {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        match self {
24            Self::Unverified => write!(f, "unverified"),
25            Self::Decoded => write!(f, "decoded"),
26            Self::Screenshot => write!(f, "screenshot"),
27            Self::Verified => write!(f, "verified"),
28        }
29    }
30}
31
32impl std::str::FromStr for VerificationStatus {
33    type Err = ParseError;
34    fn from_str(s: &str) -> Result<Self, Self::Err> {
35        match s {
36            "unverified" => Ok(Self::Unverified),
37            "decoded" => Ok(Self::Decoded),
38            "screenshot" => Ok(Self::Screenshot),
39            "verified" => Ok(Self::Verified),
40            _ => Err(ParseError::InvalidVerificationStatus(s.to_string())),
41        }
42    }
43}
44
45/// Source of a field value
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)]
47#[serde(rename_all = "snake_case")]
48#[derive(Default)]
49pub enum ValueSource {
50    /// Value shown in the game UI (highest priority)
51    InGame = 3,
52    /// Value extracted by our decoder
53    #[default]
54    Decoder = 2,
55    /// Value from a community tool (with source_detail naming it)
56    CommunityTool = 1,
57}
58
59impl ValueSource {
60    /// Priority for sorting (higher = prefer)
61    pub fn priority(&self) -> u8 {
62        *self as u8
63    }
64}
65
66impl std::fmt::Display for ValueSource {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        match self {
69            Self::InGame => write!(f, "ingame"),
70            Self::Decoder => write!(f, "decoder"),
71            Self::CommunityTool => write!(f, "community_tool"),
72        }
73    }
74}
75
76impl std::str::FromStr for ValueSource {
77    type Err = ParseError;
78    fn from_str(s: &str) -> Result<Self, Self::Err> {
79        match s {
80            "ingame" | "in_game" => Ok(Self::InGame),
81            "decoder" => Ok(Self::Decoder),
82            "community_tool" | "community" => Ok(Self::CommunityTool),
83            _ => Err(ParseError::InvalidValueSource(s.to_string())),
84        }
85    }
86}
87
88/// Confidence level for a value
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)]
90#[serde(rename_all = "snake_case")]
91#[derive(Default)]
92pub enum Confidence {
93    /// Value has been verified (e.g., screenshot match)
94    Verified = 3,
95    /// Value is inferred but likely correct
96    #[default]
97    Inferred = 2,
98    /// Value is uncertain/experimental
99    Uncertain = 1,
100}
101
102impl Confidence {
103    /// Priority for sorting (higher = prefer)
104    pub fn priority(&self) -> u8 {
105        *self as u8
106    }
107}
108
109impl std::fmt::Display for Confidence {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        match self {
112            Self::Verified => write!(f, "verified"),
113            Self::Inferred => write!(f, "inferred"),
114            Self::Uncertain => write!(f, "uncertain"),
115        }
116    }
117}
118
119impl std::str::FromStr for Confidence {
120    type Err = ParseError;
121    fn from_str(s: &str) -> Result<Self, Self::Err> {
122        match s {
123            "verified" => Ok(Self::Verified),
124            "inferred" => Ok(Self::Inferred),
125            "uncertain" => Ok(Self::Uncertain),
126            _ => Err(ParseError::InvalidConfidence(s.to_string())),
127        }
128    }
129}
130
131/// Item fields that can have multi-source values
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
133#[serde(rename_all = "snake_case")]
134pub enum ItemField {
135    Name,
136    Prefix,
137    Manufacturer,
138    WeaponType,
139    ItemType,
140    Rarity,
141    Level,
142    Element,
143    Dps,
144    Damage,
145    Accuracy,
146    FireRate,
147    ReloadTime,
148    MagSize,
149    Value,
150    RedText,
151}
152
153impl ItemField {
154    /// All field variants
155    pub const ALL: &'static [ItemField] = &[
156        ItemField::Name,
157        ItemField::Prefix,
158        ItemField::Manufacturer,
159        ItemField::WeaponType,
160        ItemField::ItemType,
161        ItemField::Rarity,
162        ItemField::Level,
163        ItemField::Element,
164        ItemField::Dps,
165        ItemField::Damage,
166        ItemField::Accuracy,
167        ItemField::FireRate,
168        ItemField::ReloadTime,
169        ItemField::MagSize,
170        ItemField::Value,
171        ItemField::RedText,
172    ];
173
174    /// Display width for table formatting
175    pub fn display_width(&self) -> usize {
176        match self {
177            Self::Name => 20,
178            Self::Prefix => 15,
179            Self::Manufacturer => 12,
180            Self::WeaponType => 8,
181            Self::ItemType => 6,
182            Self::Rarity => 10,
183            Self::Level => 5,
184            Self::Element => 10,
185            Self::Dps => 6,
186            Self::Damage => 6,
187            Self::Accuracy => 8,
188            Self::FireRate => 10,
189            Self::ReloadTime => 11,
190            Self::MagSize => 8,
191            Self::Value => 8,
192            Self::RedText => 30,
193        }
194    }
195}
196
197impl std::fmt::Display for ItemField {
198    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199        match self {
200            Self::Name => write!(f, "name"),
201            Self::Prefix => write!(f, "prefix"),
202            Self::Manufacturer => write!(f, "manufacturer"),
203            Self::WeaponType => write!(f, "weapon_type"),
204            Self::ItemType => write!(f, "item_type"),
205            Self::Rarity => write!(f, "rarity"),
206            Self::Level => write!(f, "level"),
207            Self::Element => write!(f, "element"),
208            Self::Dps => write!(f, "dps"),
209            Self::Damage => write!(f, "damage"),
210            Self::Accuracy => write!(f, "accuracy"),
211            Self::FireRate => write!(f, "fire_rate"),
212            Self::ReloadTime => write!(f, "reload_time"),
213            Self::MagSize => write!(f, "mag_size"),
214            Self::Value => write!(f, "value"),
215            Self::RedText => write!(f, "red_text"),
216        }
217    }
218}
219
220impl std::str::FromStr for ItemField {
221    type Err = ParseError;
222    fn from_str(s: &str) -> Result<Self, Self::Err> {
223        match s {
224            "name" => Ok(Self::Name),
225            "prefix" => Ok(Self::Prefix),
226            "manufacturer" => Ok(Self::Manufacturer),
227            "weapon_type" => Ok(Self::WeaponType),
228            "item_type" => Ok(Self::ItemType),
229            "rarity" => Ok(Self::Rarity),
230            "level" => Ok(Self::Level),
231            "element" => Ok(Self::Element),
232            "dps" => Ok(Self::Dps),
233            "damage" => Ok(Self::Damage),
234            "accuracy" => Ok(Self::Accuracy),
235            "fire_rate" => Ok(Self::FireRate),
236            "reload_time" => Ok(Self::ReloadTime),
237            "mag_size" => Ok(Self::MagSize),
238            "value" => Ok(Self::Value),
239            "red_text" => Ok(Self::RedText),
240            _ => Err(ParseError::InvalidItemField(s.to_string())),
241        }
242    }
243}
244
245/// A field value with source attribution
246#[derive(Debug, Clone, Serialize, Deserialize)]
247pub struct ItemValue {
248    pub id: i64,
249    pub item_serial: String,
250    pub field: String,
251    pub value: String,
252    pub source: ValueSource,
253    pub source_detail: Option<String>,
254    pub confidence: Confidence,
255    pub created_at: String,
256}
257
258/// Item entry in the database (serial is the primary key)
259#[derive(Debug, Clone, Default, Serialize, Deserialize)]
260pub struct Item {
261    pub serial: String,
262    pub name: Option<String>,
263    pub prefix: Option<String>,
264    pub manufacturer: Option<String>,
265    pub weapon_type: Option<String>,
266    pub item_type: Option<String>,
267    pub rarity: Option<String>,
268    pub level: Option<i32>,
269    pub element: Option<String>,
270    pub dps: Option<i32>,
271    pub damage: Option<i32>,
272    pub accuracy: Option<i32>,
273    pub fire_rate: Option<f64>,
274    pub reload_time: Option<f64>,
275    pub mag_size: Option<i32>,
276    pub value: Option<i32>,
277    pub red_text: Option<String>,
278    pub notes: Option<String>,
279    pub verification_status: VerificationStatus,
280    pub verification_notes: Option<String>,
281    pub verified_at: Option<String>,
282    pub legal: bool,
283    pub source: Option<String>,
284    pub created_at: String,
285}
286
287/// Weapon part entry
288#[derive(Debug, Clone, Serialize, Deserialize)]
289pub struct ItemPart {
290    pub id: i64,
291    pub item_serial: String,
292    pub slot: String,
293    pub part_index: Option<i32>,
294    pub part_name: Option<String>,
295    pub manufacturer: Option<String>,
296    pub effect: Option<String>,
297    pub verified: bool,
298    pub verification_method: Option<String>,
299    pub verification_notes: Option<String>,
300    pub verified_at: Option<String>,
301}
302
303/// Image attachment entry
304#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct Attachment {
306    pub id: i64,
307    pub item_serial: String,
308    pub name: String,
309    pub mime_type: String,
310    /// View type: POPUP (item card), DETAIL (3D inspect), or OTHER
311    pub view: String,
312}
313
314/// Database statistics
315#[derive(Debug, Clone, Default, Serialize, Deserialize)]
316pub struct DbStats {
317    pub item_count: i64,
318    pub part_count: i64,
319    pub attachment_count: i64,
320    pub value_count: i64,
321}
322
323/// Migration statistics
324#[derive(Debug, Clone, Default, Serialize, Deserialize)]
325pub struct MigrationStats {
326    pub items_processed: usize,
327    pub values_migrated: usize,
328    pub values_skipped: usize,
329}
330
331/// Filter for listing items
332#[derive(Debug, Default, Clone, Serialize, Deserialize)]
333pub struct ItemFilter {
334    pub manufacturer: Option<String>,
335    pub weapon_type: Option<String>,
336    pub element: Option<String>,
337    pub rarity: Option<String>,
338    pub limit: Option<u32>,
339    pub offset: Option<u32>,
340}
341
342/// Update payload for items
343#[derive(Debug, Default, Clone, Serialize, Deserialize)]
344pub struct ItemUpdate {
345    pub name: Option<String>,
346    pub prefix: Option<String>,
347    pub manufacturer: Option<String>,
348    pub weapon_type: Option<String>,
349    pub rarity: Option<String>,
350    pub level: Option<i32>,
351    pub element: Option<String>,
352    pub dps: Option<i32>,
353    pub damage: Option<i32>,
354    pub accuracy: Option<i32>,
355    pub fire_rate: Option<f64>,
356    pub reload_time: Option<f64>,
357    pub mag_size: Option<i32>,
358    pub value: Option<i32>,
359    pub red_text: Option<String>,
360    pub notes: Option<String>,
361}
362
363/// Parse errors for string conversions
364#[derive(Debug, Clone, thiserror::Error)]
365pub enum ParseError {
366    #[error("Invalid verification status: {0}")]
367    InvalidVerificationStatus(String),
368    #[error("Invalid value source: {0}")]
369    InvalidValueSource(String),
370    #[error("Invalid confidence level: {0}")]
371    InvalidConfidence(String),
372    #[error("Invalid item field: {0}")]
373    InvalidItemField(String),
374}
375
376/// Helper to pick the best value from a collection based on source and confidence priority
377pub fn pick_best_value(values: impl IntoIterator<Item = ItemValue>) -> Option<ItemValue> {
378    values
379        .into_iter()
380        .max_by(|a, b| match a.source.priority().cmp(&b.source.priority()) {
381            std::cmp::Ordering::Equal => a.confidence.priority().cmp(&b.confidence.priority()),
382            other => other,
383        })
384}
385
386// =============================================================================
387// Source Hashing
388// =============================================================================
389
390/// Hash a source string with a salt for anonymous publishing.
391/// Returns a 12-character hex string.
392pub fn hash_source(source: &str, salt: &str) -> String {
393    let mut hasher = Sha256::new();
394    hasher.update(salt.as_bytes());
395    hasher.update(source.as_bytes());
396    let result = hasher.finalize();
397    hex::encode(&result[..6]) // 12 hex chars
398}
399
400/// Generate a random salt for source hashing (32 bytes, hex-encoded).
401pub fn generate_salt() -> String {
402    use rand::Rng;
403    let mut rng = rand::thread_rng();
404    let bytes: [u8; 32] = rng.gen();
405    hex::encode(bytes)
406}
407
408/// Try to find the original source name from a hash.
409/// Compares the hash against all known sources.
410pub fn lookup_source_hash<'a>(
411    hash: &str,
412    salt: &str,
413    known_sources: impl IntoIterator<Item = &'a str>,
414) -> Option<String> {
415    for source in known_sources {
416        if hash_source(source, salt) == hash {
417            return Some(source.to_string());
418        }
419    }
420    None
421}
422
423/// BL4 namespace UUID for generating deterministic UUIDv5 values.
424/// This is a fixed namespace for all BL4 items.
425pub const BL4_NAMESPACE: uuid::Uuid = uuid::uuid!("b14c0de4-0000-4000-8000-000000000001");
426
427/// Generate a deterministic UUIDv5 from a serial and hashed source.
428pub fn generate_item_uuid(serial: &str, hashed_source: &str) -> uuid::Uuid {
429    let name = format!("{}:{}", serial, hashed_source);
430    uuid::Uuid::new_v5(&BL4_NAMESPACE, name.as_bytes())
431}
432
433/// Generate a random UUIDv4 for items without a source.
434pub fn generate_random_uuid() -> uuid::Uuid {
435    uuid::Uuid::new_v4()
436}
437
438// =============================================================================
439// Value Selection
440// =============================================================================
441
442/// Group values by field and pick best for each
443pub fn best_values_by_field(
444    values: impl IntoIterator<Item = ItemValue>,
445) -> HashMap<String, String> {
446    let mut best_by_field: HashMap<String, ItemValue> = HashMap::new();
447
448    for value in values {
449        let dominated = best_by_field
450            .get(&value.field)
451            .map(
452                |existing| match value.source.priority().cmp(&existing.source.priority()) {
453                    std::cmp::Ordering::Greater => true,
454                    std::cmp::Ordering::Equal => {
455                        value.confidence.priority() > existing.confidence.priority()
456                    }
457                    std::cmp::Ordering::Less => false,
458                },
459            )
460            .unwrap_or(true);
461
462        if dominated {
463            best_by_field.insert(value.field.clone(), value);
464        }
465    }
466
467    best_by_field
468        .into_iter()
469        .map(|(k, v)| (k, v.value))
470        .collect()
471}
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476
477    #[test]
478    fn test_verification_status_parse() {
479        assert_eq!(
480            "unverified".parse::<VerificationStatus>().unwrap(),
481            VerificationStatus::Unverified
482        );
483        assert_eq!(
484            "decoded".parse::<VerificationStatus>().unwrap(),
485            VerificationStatus::Decoded
486        );
487        assert_eq!(
488            "screenshot".parse::<VerificationStatus>().unwrap(),
489            VerificationStatus::Screenshot
490        );
491        assert_eq!(
492            "verified".parse::<VerificationStatus>().unwrap(),
493            VerificationStatus::Verified
494        );
495        assert!("invalid".parse::<VerificationStatus>().is_err());
496    }
497
498    #[test]
499    fn test_verification_status_display() {
500        assert_eq!(VerificationStatus::Unverified.to_string(), "unverified");
501        assert_eq!(VerificationStatus::Decoded.to_string(), "decoded");
502        assert_eq!(VerificationStatus::Screenshot.to_string(), "screenshot");
503        assert_eq!(VerificationStatus::Verified.to_string(), "verified");
504    }
505
506    #[test]
507    fn test_value_source_parse() {
508        assert_eq!(
509            "ingame".parse::<ValueSource>().unwrap(),
510            ValueSource::InGame
511        );
512        assert_eq!(
513            "in_game".parse::<ValueSource>().unwrap(),
514            ValueSource::InGame
515        );
516        assert_eq!(
517            "decoder".parse::<ValueSource>().unwrap(),
518            ValueSource::Decoder
519        );
520        assert_eq!(
521            "community_tool".parse::<ValueSource>().unwrap(),
522            ValueSource::CommunityTool
523        );
524        assert_eq!(
525            "community".parse::<ValueSource>().unwrap(),
526            ValueSource::CommunityTool
527        );
528        assert!("invalid".parse::<ValueSource>().is_err());
529    }
530
531    #[test]
532    fn test_value_source_display() {
533        assert_eq!(ValueSource::InGame.to_string(), "ingame");
534        assert_eq!(ValueSource::Decoder.to_string(), "decoder");
535        assert_eq!(ValueSource::CommunityTool.to_string(), "community_tool");
536    }
537
538    #[test]
539    fn test_value_source_priority() {
540        assert!(ValueSource::InGame.priority() > ValueSource::Decoder.priority());
541        assert!(ValueSource::Decoder.priority() > ValueSource::CommunityTool.priority());
542    }
543
544    #[test]
545    fn test_confidence_parse() {
546        assert_eq!(
547            "verified".parse::<Confidence>().unwrap(),
548            Confidence::Verified
549        );
550        assert_eq!(
551            "inferred".parse::<Confidence>().unwrap(),
552            Confidence::Inferred
553        );
554        assert_eq!(
555            "uncertain".parse::<Confidence>().unwrap(),
556            Confidence::Uncertain
557        );
558        assert!("invalid".parse::<Confidence>().is_err());
559    }
560
561    #[test]
562    fn test_confidence_display() {
563        assert_eq!(Confidence::Verified.to_string(), "verified");
564        assert_eq!(Confidence::Inferred.to_string(), "inferred");
565        assert_eq!(Confidence::Uncertain.to_string(), "uncertain");
566    }
567
568    #[test]
569    fn test_confidence_priority() {
570        assert!(Confidence::Verified.priority() > Confidence::Inferred.priority());
571        assert!(Confidence::Inferred.priority() > Confidence::Uncertain.priority());
572    }
573
574    #[test]
575    fn test_item_field_parse() {
576        assert_eq!("name".parse::<ItemField>().unwrap(), ItemField::Name);
577        assert_eq!("prefix".parse::<ItemField>().unwrap(), ItemField::Prefix);
578        assert_eq!(
579            "manufacturer".parse::<ItemField>().unwrap(),
580            ItemField::Manufacturer
581        );
582        assert_eq!(
583            "weapon_type".parse::<ItemField>().unwrap(),
584            ItemField::WeaponType
585        );
586        assert_eq!(
587            "item_type".parse::<ItemField>().unwrap(),
588            ItemField::ItemType
589        );
590        assert_eq!("rarity".parse::<ItemField>().unwrap(), ItemField::Rarity);
591        assert_eq!("level".parse::<ItemField>().unwrap(), ItemField::Level);
592        assert_eq!("element".parse::<ItemField>().unwrap(), ItemField::Element);
593        assert!("invalid".parse::<ItemField>().is_err());
594    }
595
596    #[test]
597    fn test_item_field_display() {
598        assert_eq!(ItemField::Name.to_string(), "name");
599        assert_eq!(ItemField::WeaponType.to_string(), "weapon_type");
600    }
601
602    fn make_value(
603        field: &str,
604        value: &str,
605        source: ValueSource,
606        confidence: Confidence,
607    ) -> ItemValue {
608        ItemValue {
609            id: 0,
610            item_serial: String::new(),
611            field: field.to_string(),
612            value: value.to_string(),
613            source,
614            source_detail: None,
615            confidence,
616            created_at: String::new(),
617        }
618    }
619
620    #[test]
621    fn test_pick_best_value_by_source() {
622        let values = vec![
623            make_value(
624                "name",
625                "Community Name",
626                ValueSource::CommunityTool,
627                Confidence::Verified,
628            ),
629            make_value(
630                "name",
631                "Decoder Name",
632                ValueSource::Decoder,
633                Confidence::Verified,
634            ),
635            make_value(
636                "name",
637                "InGame Name",
638                ValueSource::InGame,
639                Confidence::Verified,
640            ),
641        ];
642        let best = pick_best_value(values).unwrap();
643        assert_eq!(best.value, "InGame Name");
644        assert_eq!(best.source, ValueSource::InGame);
645    }
646
647    #[test]
648    fn test_pick_best_value_by_confidence() {
649        let values = vec![
650            make_value(
651                "name",
652                "Uncertain",
653                ValueSource::Decoder,
654                Confidence::Uncertain,
655            ),
656            make_value(
657                "name",
658                "Verified",
659                ValueSource::Decoder,
660                Confidence::Verified,
661            ),
662            make_value(
663                "name",
664                "Inferred",
665                ValueSource::Decoder,
666                Confidence::Inferred,
667            ),
668        ];
669        let best = pick_best_value(values).unwrap();
670        assert_eq!(best.value, "Verified");
671        assert_eq!(best.confidence, Confidence::Verified);
672    }
673
674    #[test]
675    fn test_pick_best_value_source_over_confidence() {
676        // InGame with Uncertain should beat Decoder with Verified
677        let values = vec![
678            make_value(
679                "name",
680                "Decoder Verified",
681                ValueSource::Decoder,
682                Confidence::Verified,
683            ),
684            make_value(
685                "name",
686                "InGame Uncertain",
687                ValueSource::InGame,
688                Confidence::Uncertain,
689            ),
690        ];
691        let best = pick_best_value(values).unwrap();
692        assert_eq!(best.value, "InGame Uncertain");
693    }
694
695    #[test]
696    fn test_pick_best_value_empty() {
697        let values: Vec<ItemValue> = vec![];
698        assert!(pick_best_value(values).is_none());
699    }
700
701    #[test]
702    fn test_best_values_by_field() {
703        let values = vec![
704            make_value(
705                "name",
706                "Bad Name",
707                ValueSource::CommunityTool,
708                Confidence::Uncertain,
709            ),
710            make_value(
711                "name",
712                "Good Name",
713                ValueSource::InGame,
714                Confidence::Verified,
715            ),
716            make_value("level", "50", ValueSource::Decoder, Confidence::Inferred),
717            make_value("level", "51", ValueSource::InGame, Confidence::Verified),
718        ];
719        let best = best_values_by_field(values);
720        assert_eq!(best.get("name"), Some(&"Good Name".to_string()));
721        assert_eq!(best.get("level"), Some(&"51".to_string()));
722    }
723
724    #[test]
725    fn test_best_values_by_field_empty() {
726        let values: Vec<ItemValue> = vec![];
727        let best = best_values_by_field(values);
728        assert!(best.is_empty());
729    }
730
731    #[test]
732    fn test_hash_source() {
733        let salt = "test_salt_12345";
734        let source = "monokrome";
735
736        let hash1 = hash_source(source, salt);
737        let hash2 = hash_source(source, salt);
738
739        // Same input produces same hash
740        assert_eq!(hash1, hash2);
741        // Hash is 12 hex characters (6 bytes)
742        assert_eq!(hash1.len(), 12);
743        // All characters are hex
744        assert!(hash1.chars().all(|c| c.is_ascii_hexdigit()));
745
746        // Different sources produce different hashes
747        let hash3 = hash_source("other_source", salt);
748        assert_ne!(hash1, hash3);
749
750        // Different salts produce different hashes
751        let hash4 = hash_source(source, "different_salt");
752        assert_ne!(hash1, hash4);
753    }
754
755    #[test]
756    fn test_generate_salt() {
757        let salt1 = generate_salt();
758        let salt2 = generate_salt();
759
760        // Salt is 64 hex characters (32 bytes)
761        assert_eq!(salt1.len(), 64);
762        assert_eq!(salt2.len(), 64);
763
764        // All characters are hex
765        assert!(salt1.chars().all(|c| c.is_ascii_hexdigit()));
766        assert!(salt2.chars().all(|c| c.is_ascii_hexdigit()));
767
768        // Salts are different (very high probability)
769        assert_ne!(salt1, salt2);
770    }
771
772    #[test]
773    fn test_lookup_source_hash() {
774        let salt = "lookup_test_salt";
775        let sources = ["alice", "bob", "charlie"];
776
777        // Hash one of the sources
778        let alice_hash = hash_source("alice", salt);
779
780        // Should find it in the list
781        let found = lookup_source_hash(&alice_hash, salt, sources.iter().copied());
782        assert_eq!(found, Some("alice".to_string()));
783
784        // Should not find a non-existent hash
785        let fake_hash = "000000000000";
786        let not_found = lookup_source_hash(fake_hash, salt, sources.iter().copied());
787        assert!(not_found.is_none());
788    }
789
790    #[test]
791    fn test_generate_item_uuid() {
792        let serial = "@Ug12345678901234567890";
793        let hashed_source = "abc123def456";
794
795        let uuid1 = generate_item_uuid(serial, hashed_source);
796        let uuid2 = generate_item_uuid(serial, hashed_source);
797
798        // Same inputs produce same UUID (deterministic)
799        assert_eq!(uuid1, uuid2);
800
801        // UUID is version 5
802        assert_eq!(uuid1.get_version_num(), 5);
803
804        // Different inputs produce different UUIDs
805        let uuid3 = generate_item_uuid("different_serial", hashed_source);
806        assert_ne!(uuid1, uuid3);
807
808        let uuid4 = generate_item_uuid(serial, "different_hash");
809        assert_ne!(uuid1, uuid4);
810    }
811
812    #[test]
813    fn test_generate_random_uuid() {
814        let uuid1 = generate_random_uuid();
815        let uuid2 = generate_random_uuid();
816
817        // UUID is version 4
818        assert_eq!(uuid1.get_version_num(), 4);
819
820        // Random UUIDs are different (very high probability)
821        assert_ne!(uuid1, uuid2);
822    }
823
824    #[test]
825    fn test_item_field_display_width() {
826        // Test all fields have reasonable widths
827        for field in ItemField::ALL {
828            let width = field.display_width();
829            assert!(width > 0);
830            assert!(width <= 30);
831        }
832
833        // Specific widths
834        assert_eq!(ItemField::Name.display_width(), 20);
835        assert_eq!(ItemField::Level.display_width(), 5);
836        assert_eq!(ItemField::RedText.display_width(), 30);
837    }
838
839    #[test]
840    fn test_item_field_all_variants() {
841        // ALL should contain exactly 16 fields
842        assert_eq!(ItemField::ALL.len(), 16);
843
844        // All fields should be parseable and display correctly
845        for field in ItemField::ALL {
846            let as_string = field.to_string();
847            let parsed: ItemField = as_string.parse().unwrap();
848            assert_eq!(parsed, *field);
849        }
850    }
851}