wow-mpq 0.6.4

High-performance parser for World of Warcraft MPQ archives with parallel processing support
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
//! Patch chain implementation for handling multiple MPQ archives with priority
//!
//! This module provides functionality for managing multiple MPQ archives in a chain,
//! where files in higher-priority archives override those in lower-priority ones.
//! This is essential for World of Warcraft's patching system.

use crate::{Archive, Error, FileEntry, Result};
use std::collections::HashMap;
use std::path::{Path, PathBuf};

/// A chain of MPQ archives with priority ordering
///
/// `PatchChain` manages multiple MPQ archives where files in higher-priority
/// archives override those in lower-priority ones. This mimics how World of Warcraft
/// handles its patch system, where patch-N.MPQ files override files in the base archives.
///
/// # Patch File Support
///
/// Starting with Cataclysm (4.x), WoW introduced binary patch files (PTCH format) that
/// contain binary diffs instead of complete files. `PatchChain` automatically detects
/// and applies these patches:
///
/// - Files with the `MPQ_FILE_PATCH_FILE` flag are automatically recognized
/// - Base file is located in lower-priority archives
/// - Patches are applied in order (lowest to highest priority)
/// - Both COPY (replacement) and BSD0 (binary diff) patches are supported
/// - MD5 verification ensures patch integrity
///
/// # Examples
///
/// ```no_run
/// use wow_mpq::PatchChain;
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut chain = PatchChain::new();
///
/// // Add base archive with lowest priority
/// chain.add_archive("Data/common.MPQ", 0)?;
///
/// // Add patches with increasing priority
/// chain.add_archive("Data/patch.MPQ", 100)?;
/// chain.add_archive("Data/patch-2.MPQ", 200)?;
/// chain.add_archive("Data/patch-3.MPQ", 300)?;
///
/// // Extract file - will use the highest priority version available
/// // If the file exists as a PTCH patch file, it will be automatically applied
/// let data = chain.read_file("Interface/Icons/INV_Misc_QuestionMark.blp")?;
/// # Ok(())
/// # }
/// ```
#[derive(Debug)]
pub struct PatchChain {
    /// Archives ordered by priority (highest first)
    archives: Vec<ChainEntry>,
    /// Cache of file locations for quick lookup
    file_map: HashMap<String, usize>,
}

#[derive(Debug)]
struct ChainEntry {
    /// The MPQ archive
    archive: Archive,
    /// Priority (higher numbers override lower)
    priority: i32,
    /// Path to the archive file
    path: PathBuf,
}

impl PatchChain {
    /// Create a new empty patch chain
    pub fn new() -> Self {
        Self {
            archives: Vec::new(),
            file_map: HashMap::new(),
        }
    }

    /// Add an archive to the chain with a specific priority
    ///
    /// Archives with higher priority values will override files in archives
    /// with lower priority values.
    ///
    /// # Parameters
    /// - `path`: Path to the MPQ archive
    /// - `priority`: Priority value (higher = overrides lower)
    ///
    /// # Typical priority values
    /// - 0: Base archives (common.MPQ, expansion.MPQ)
    /// - 100-999: Official patches (patch.MPQ, patch-2.MPQ, etc.)
    /// - 1000+: Custom patches or mods
    pub fn add_archive<P: AsRef<Path>>(&mut self, path: P, priority: i32) -> Result<()> {
        let path = path.as_ref();
        let archive = Archive::open(path)?;

        // Insert in sorted order (highest priority first)
        let entry = ChainEntry {
            archive,
            priority,
            path: path.to_path_buf(),
        };

        let insert_pos = self
            .archives
            .iter()
            .position(|e| e.priority < priority)
            .unwrap_or(self.archives.len());

        self.archives.insert(insert_pos, entry);

        // Rebuild file map
        self.rebuild_file_map()?;

        Ok(())
    }

    /// Remove an archive from the chain
    pub fn remove_archive<P: AsRef<Path>>(&mut self, path: P) -> Result<bool> {
        let path = path.as_ref();

        if let Some(pos) = self.archives.iter().position(|e| e.path == path) {
            self.archives.remove(pos);
            self.rebuild_file_map()?;
            Ok(true)
        } else {
            Ok(false)
        }
    }

    /// Clear all archives from the chain
    pub fn clear(&mut self) {
        self.archives.clear();
        self.file_map.clear();
    }

    /// Get the number of archives in the chain
    pub fn archive_count(&self) -> usize {
        self.archives.len()
    }

    /// Read a file from the chain
    ///
    /// Returns the file from the highest-priority archive that contains it.
    /// If the file is a patch file, this method will automatically:
    /// 1. Find the base file in lower-priority archives
    /// 2. Apply all patches in priority order
    /// 3. Return the fully patched result
    pub fn read_file(&mut self, filename: &str) -> Result<Vec<u8>> {
        // Normalize filename and convert to uppercase for case-insensitive lookup
        // This matches MPQ hashing behavior which is always case-insensitive
        let lookup_key = crate::path::normalize_mpq_path(filename).to_uppercase();

        if let Some(&archive_idx) = self.file_map.get(&lookup_key) {
            // Check if this is a patch file by examining the file info
            let file_info = self.archives[archive_idx]
                .archive
                .find_file(filename)?
                .ok_or_else(|| Error::FileNotFound(filename.to_string()))?;

            if file_info.is_patch_file() {
                // This is a patch file - need to find base and apply patches
                self.read_patched_file(filename, archive_idx)
            } else {
                // Regular file - read normally
                self.archives[archive_idx].archive.read_file(filename)
            }
        } else {
            Err(Error::FileNotFound(filename.to_string()))
        }
    }

    /// Read a patch file and apply it to the base file
    ///
    /// This method handles the patch chain resolution:
    /// 1. Find the base file (non-patch version) in lower-priority archives
    /// 2. Read all patches for this file in priority order
    /// 3. Apply patches sequentially to produce the final result
    fn read_patched_file(&mut self, filename: &str, _patch_idx: usize) -> Result<Vec<u8>> {
        use crate::patch::{PatchFile, apply_patch};

        // Step 1: Find the base file (search lower priority archives)
        let mut base_data: Option<Vec<u8>> = None;
        let mut patches = Vec::new();

        // Collect all versions of this file in priority order (highest first)
        for (idx, entry) in self.archives.iter_mut().enumerate() {
            if let Ok(Some(file_info)) = entry.archive.find_file(filename) {
                if file_info.is_patch_file() {
                    // This is a patch - read it raw (bypass the read_file check)
                    match entry.archive.read_patch_file_raw(filename) {
                        Ok(patch_data) => {
                            // Parse the patch
                            match PatchFile::parse(&patch_data) {
                                Ok(patch) => patches.push((idx, patch)),
                                Err(e) => {
                                    log::warn!(
                                        "Failed to parse patch file '{}' in archive {} (priority {}): {}",
                                        filename,
                                        entry.path.display(),
                                        entry.priority,
                                        e
                                    );
                                }
                            }
                        }
                        Err(e) => {
                            log::warn!(
                                "Failed to read patch file '{}' in archive {} (priority {}): {}",
                                filename,
                                entry.path.display(),
                                entry.priority,
                                e
                            );
                        }
                    }
                } else if base_data.is_none() {
                    // This is a regular file - use as base if we haven't found one yet
                    match entry.archive.read_file(filename) {
                        Ok(data) => {
                            log::debug!(
                                "Found base file '{}' in archive {} (priority {})",
                                filename,
                                entry.path.display(),
                                entry.priority
                            );
                            base_data = Some(data);
                        }
                        Err(e) => {
                            log::warn!(
                                "Failed to read base file '{}' in archive {} (priority {}): {}",
                                filename,
                                entry.path.display(),
                                entry.priority,
                                e
                            );
                        }
                    }
                }
            }
        }

        // Step 2: Verify we have a base file
        let mut current_data = base_data.ok_or_else(|| {
            Error::FileNotFound(format!(
                "No base file found for patch file '{filename}' in patch chain"
            ))
        })?;

        // Step 3: Apply patches in reverse priority order (lowest to highest)
        // This ensures patches are applied in the correct sequence
        patches.reverse();
        for (idx, patch) in patches {
            log::debug!(
                "Applying patch '{}' from archive {} (priority {})",
                filename,
                self.archives[idx].path.display(),
                self.archives[idx].priority
            );

            current_data = apply_patch(&patch, &current_data)?;
        }

        Ok(current_data)
    }

    /// Check if a file exists in the chain
    pub fn contains_file(&self, filename: &str) -> bool {
        let lookup_key = crate::path::normalize_mpq_path(filename).to_uppercase();
        self.file_map.contains_key(&lookup_key)
    }

    /// Find which archive contains a file
    ///
    /// Returns the path to the archive containing the file, or None if not found.
    pub fn find_file_archive(&self, filename: &str) -> Option<&Path> {
        let lookup_key = crate::path::normalize_mpq_path(filename).to_uppercase();
        self.file_map
            .get(&lookup_key)
            .map(|&idx| self.archives[idx].path.as_path())
    }

    /// List all files in the chain
    ///
    /// Returns a deduplicated list of all files across all archives,
    /// with file information from the highest-priority archive for each file.
    pub fn list(&mut self) -> Result<Vec<FileEntry>> {
        let mut seen = HashMap::new();
        let mut result = Vec::new();

        // Process archives in priority order (highest first)
        for (idx, entry) in self.archives.iter_mut().enumerate() {
            match entry.archive.list() {
                Ok(files) => {
                    for file in files {
                        // Only add if we haven't seen this file yet
                        if !seen.contains_key(&file.name) {
                            seen.insert(file.name.clone(), idx);
                            result.push(file);
                        }
                    }
                }
                Err(_) => {
                    // Try list_all if no listfile
                    if let Ok(files) = entry.archive.list_all() {
                        for file in files {
                            if !seen.contains_key(&file.name) {
                                seen.insert(file.name.clone(), idx);
                                result.push(file);
                            }
                        }
                    }
                }
            }
        }

        // Sort by name for consistent output
        result.sort_by(|a, b| a.name.cmp(&b.name));

        Ok(result)
    }

    /// Get information about all archives in the chain
    pub fn get_chain_info(&mut self) -> Vec<ChainInfo> {
        self.archives
            .iter_mut()
            .filter_map(|entry| {
                entry.archive.get_info().ok().map(|info| ChainInfo {
                    path: entry.path.clone(),
                    priority: entry.priority,
                    file_count: info.file_count,
                    archive_size: info.file_size,
                    format_version: info.format_version,
                })
            })
            .collect()
    }

    /// Rebuild the internal file map
    fn rebuild_file_map(&mut self) -> Result<()> {
        self.file_map.clear();

        // Process archives in priority order (highest first)
        for (idx, entry) in self.archives.iter_mut().enumerate() {
            // Try to get file list
            let files = match entry.archive.list() {
                Ok(files) => files,
                Err(_) => {
                    // Try list_all if no listfile
                    match entry.archive.list_all() {
                        Ok(files) => files,
                        Err(_) => continue, // Skip this archive
                    }
                }
            };

            // Add files to map (only if not already present from higher priority)
            // MPQ hashing is case-insensitive, so normalize keys to uppercase
            for file in files {
                let normalized_key = crate::path::normalize_mpq_path(&file.name).to_uppercase();
                self.file_map.entry(normalized_key).or_insert(idx);
            }
        }

        Ok(())
    }

    /// Extract multiple files efficiently
    ///
    /// This method extracts multiple files in a single pass, which can be more
    /// efficient than calling `read_file` multiple times.
    pub fn extract_files(&mut self, filenames: &[&str]) -> Vec<(String, Result<Vec<u8>>)> {
        filenames
            .iter()
            .map(|&filename| {
                let result = self.read_file(filename);
                (filename.to_string(), result)
            })
            .collect()
    }

    /// Get a reference to a specific archive by path
    pub fn get_archive<P: AsRef<Path>>(&self, path: P) -> Option<&Archive> {
        let path = path.as_ref();
        self.archives
            .iter()
            .find(|e| e.path == path)
            .map(|e| &e.archive)
    }

    /// Get archive priority by path
    pub fn get_priority<P: AsRef<Path>>(&self, path: P) -> Option<i32> {
        let path = path.as_ref();
        self.archives
            .iter()
            .find(|e| e.path == path)
            .map(|e| e.priority)
    }

    /// Load multiple archives in parallel and build a patch chain
    ///
    /// This method loads all archives concurrently using rayon, then builds
    /// the patch chain with proper priority ordering. This is significantly
    /// faster than loading archives sequentially.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use wow_mpq::PatchChain;
    ///
    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// let archives = vec![
    ///     ("Data/common.MPQ", 0),
    ///     ("Data/patch.MPQ", 100),
    ///     ("Data/patch-2.MPQ", 200),
    ///     ("Data/patch-3.MPQ", 300),
    /// ];
    ///
    /// let chain = PatchChain::from_archives_parallel(archives)?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn from_archives_parallel<P: AsRef<Path> + Sync>(archives: Vec<(P, i32)>) -> Result<Self> {
        use rayon::prelude::*;

        // Load all archives in parallel
        let loaded_archives: Result<Vec<_>> = archives
            .par_iter()
            .map(|(path, priority)| {
                let path_ref = path.as_ref();
                Archive::open(path_ref).map(|archive| ChainEntry {
                    archive,
                    priority: *priority,
                    path: path_ref.to_path_buf(),
                })
            })
            .collect();

        let mut loaded_archives = loaded_archives?;

        // Sort by priority (highest first)
        loaded_archives.sort_by(|a, b| b.priority.cmp(&a.priority));

        let mut chain = Self {
            archives: loaded_archives,
            file_map: HashMap::new(),
        };

        // Build the file map
        chain.rebuild_file_map()?;

        Ok(chain)
    }

    /// Add multiple archives to the chain in parallel
    ///
    /// This method loads multiple archives concurrently and adds them to the
    /// existing chain with their specified priorities.
    pub fn add_archives_parallel<P: AsRef<Path> + Sync>(
        &mut self,
        archives: Vec<(P, i32)>,
    ) -> Result<()> {
        use rayon::prelude::*;

        // Load new archives in parallel
        let new_archives: Result<Vec<_>> = archives
            .par_iter()
            .map(|(path, priority)| {
                let path_ref = path.as_ref();
                Archive::open(path_ref).map(|archive| ChainEntry {
                    archive,
                    priority: *priority,
                    path: path_ref.to_path_buf(),
                })
            })
            .collect();

        let new_archives = new_archives?;

        // Add each new archive at the correct position
        for entry in new_archives {
            let insert_pos = self
                .archives
                .iter()
                .position(|e| e.priority < entry.priority)
                .unwrap_or(self.archives.len());

            self.archives.insert(insert_pos, entry);
        }

        // Rebuild file map
        self.rebuild_file_map()?;

        Ok(())
    }

    /// Update the priority of an existing archive
    pub fn set_priority<P: AsRef<Path>>(&mut self, path: P, new_priority: i32) -> Result<()> {
        let path = path.as_ref();

        // Find and remove the archive
        let archive_idx = self
            .archives
            .iter()
            .position(|e| e.path == path)
            .ok_or_else(|| Error::InvalidFormat("Archive not found in chain".to_string()))?;

        let mut entry = self.archives.remove(archive_idx);
        entry.priority = new_priority;

        // Re-insert at correct position
        let insert_pos = self
            .archives
            .iter()
            .position(|e| e.priority < new_priority)
            .unwrap_or(self.archives.len());

        self.archives.insert(insert_pos, entry);

        // Rebuild file map
        self.rebuild_file_map()?;

        Ok(())
    }
}

impl Default for PatchChain {
    fn default() -> Self {
        Self::new()
    }
}

/// Information about an archive in the chain
#[derive(Debug, Clone)]
pub struct ChainInfo {
    /// Path to the archive
    pub path: PathBuf,
    /// Priority in the chain
    pub priority: i32,
    /// Number of files in the archive
    pub file_count: usize,
    /// Total size of the archive
    pub archive_size: u64,
    /// MPQ format version
    pub format_version: crate::FormatVersion,
}

#[cfg(test)]
mod tests;

#[cfg(test)]
mod integration_tests {
    use super::*;
    use crate::{ArchiveBuilder, ListfileOption};
    use tempfile::TempDir;

    fn create_test_archive(dir: &Path, name: &str, files: &[(&str, &[u8])]) -> PathBuf {
        let path = dir.join(name);
        let mut builder = ArchiveBuilder::new().listfile_option(ListfileOption::Generate);

        for (filename, data) in files {
            builder = builder.add_file_data(data.to_vec(), filename);
        }

        builder.build(&path).unwrap();
        path
    }

    #[test]
    fn test_patch_chain_priority() {
        let temp = TempDir::new().unwrap();

        // Create base archive
        let base_files: Vec<(&str, &[u8])> = vec![
            ("file1.txt", b"base file1"),
            ("file2.txt", b"base file2"),
            ("file3.txt", b"base file3"),
        ];
        let base_path = create_test_archive(temp.path(), "base.mpq", &base_files);

        // Create patch archive that overrides file2
        let patch_files: Vec<(&str, &[u8])> =
            vec![("file2.txt", b"patched file2"), ("file4.txt", b"new file4")];
        let patch_path = create_test_archive(temp.path(), "patch.mpq", &patch_files);

        // Build chain
        let mut chain = PatchChain::new();
        chain.add_archive(&base_path, 0).unwrap();
        chain.add_archive(&patch_path, 100).unwrap();

        // Test file priority
        assert_eq!(chain.read_file("file1.txt").unwrap(), b"base file1");
        assert_eq!(chain.read_file("file2.txt").unwrap(), b"patched file2"); // Override
        assert_eq!(chain.read_file("file3.txt").unwrap(), b"base file3");
        assert_eq!(chain.read_file("file4.txt").unwrap(), b"new file4");
    }

    #[test]
    fn test_patch_chain_listing() {
        let temp = TempDir::new().unwrap();

        // Create archives
        let base_files: Vec<(&str, &[u8])> = vec![("file1.txt", b"data1"), ("file2.txt", b"data2")];
        let patch_files: Vec<(&str, &[u8])> =
            vec![("file2.txt", b"patch2"), ("file3.txt", b"data3")];

        let base_path = create_test_archive(temp.path(), "base.mpq", &base_files);
        let patch_path = create_test_archive(temp.path(), "patch.mpq", &patch_files);

        // Build chain
        let mut chain = PatchChain::new();
        chain.add_archive(&base_path, 0).unwrap();
        chain.add_archive(&patch_path, 100).unwrap();

        // List should show all unique files
        let files = chain.list().unwrap();

        // Filter out the listfile
        let files: Vec<_> = files
            .into_iter()
            .filter(|f| f.name != "(listfile)")
            .collect();

        assert_eq!(files.len(), 3);

        let names: Vec<_> = files.iter().map(|f| f.name.as_str()).collect();
        assert!(names.contains(&"file1.txt"));
        assert!(names.contains(&"file2.txt"));
        assert!(names.contains(&"file3.txt"));
    }

    #[test]
    fn test_find_file_archive() {
        let temp = TempDir::new().unwrap();

        let base_files: Vec<(&str, &[u8])> = vec![("file1.txt", b"data")];
        let patch_files: Vec<(&str, &[u8])> = vec![("file2.txt", b"data")];

        let base_path = create_test_archive(temp.path(), "base.mpq", &base_files);
        let patch_path = create_test_archive(temp.path(), "patch.mpq", &patch_files);

        let mut chain = PatchChain::new();
        chain.add_archive(&base_path, 0).unwrap();
        chain.add_archive(&patch_path, 100).unwrap();

        assert_eq!(
            chain.find_file_archive("file1.txt"),
            Some(base_path.as_path())
        );
        assert_eq!(
            chain.find_file_archive("file2.txt"),
            Some(patch_path.as_path())
        );
        assert_eq!(chain.find_file_archive("nonexistent.txt"), None);
    }

    #[test]
    fn test_remove_archive() {
        let temp = TempDir::new().unwrap();

        let files: Vec<(&str, &[u8])> = vec![("file.txt", b"data")];
        let path = create_test_archive(temp.path(), "test.mpq", &files);

        let mut chain = PatchChain::new();
        chain.add_archive(&path, 0).unwrap();

        assert!(chain.contains_file("file.txt"));
        assert!(chain.remove_archive(&path).unwrap());
        assert!(!chain.contains_file("file.txt"));
        assert!(!chain.remove_archive(&path).unwrap()); // Already removed
    }

    #[test]
    fn test_priority_reordering() {
        let temp = TempDir::new().unwrap();

        let files: Vec<(&str, &[u8])> = vec![("file.txt", b"data")];
        let path1 = create_test_archive(temp.path(), "test1.mpq", &files);
        let path2 = create_test_archive(temp.path(), "test2.mpq", &files);

        let mut chain = PatchChain::new();
        chain.add_archive(&path1, 100).unwrap();
        chain.add_archive(&path2, 50).unwrap();

        // path1 should be first (higher priority)
        assert_eq!(chain.archives[0].priority, 100);

        // Update priority
        chain.set_priority(&path2, 150).unwrap();

        // Now path2 should be first
        assert_eq!(chain.archives[0].priority, 150);
    }

    #[test]
    fn test_parallel_patch_chain_loading() {
        let temp = TempDir::new().unwrap();

        // Create multiple test archives
        let mut archive_paths = Vec::new();
        for i in 0..5 {
            let common_content = format!("Common content v{i}");
            let unique_name = format!("unique_{i}.txt");
            let unique_content = format!("Unique to archive {i}");
            let files: Vec<(&str, &[u8])> = vec![
                ("common.txt", common_content.as_bytes()),
                (&unique_name, unique_content.as_bytes()),
            ];
            let path = create_test_archive(temp.path(), &format!("archive_{i}.mpq"), &files);
            archive_paths.push((path, i * 100)); // Increasing priorities
        }

        // Load sequentially and measure time
        let start = std::time::Instant::now();
        let mut chain_seq = PatchChain::new();
        for (path, priority) in &archive_paths {
            chain_seq.add_archive(path, *priority).unwrap();
        }
        let seq_duration = start.elapsed();

        // Load in parallel
        let start = std::time::Instant::now();
        let mut chain_par = PatchChain::from_archives_parallel(archive_paths.clone()).unwrap();
        let par_duration = start.elapsed();

        // Verify both chains have the same content
        assert_eq!(
            chain_seq.list().unwrap().len(),
            chain_par.list().unwrap().len()
        );

        // Verify priority ordering (highest priority archive should win)
        let common_content = chain_par.read_file("common.txt").unwrap();
        assert_eq!(common_content, b"Common content v4"); // Archive 4 has highest priority (400)

        // Verify all unique files are accessible
        for i in 0..5 {
            let unique_file = format!("unique_{i}.txt");
            let content = chain_par.read_file(&unique_file).unwrap();
            assert_eq!(content, format!("Unique to archive {i}").as_bytes());
        }

        // Parallel should be faster (or at least not significantly slower)
        println!("Sequential loading: {seq_duration:?}");
        println!("Parallel loading: {par_duration:?}");
    }

    #[test]
    fn test_add_archives_parallel() {
        let temp = TempDir::new().unwrap();

        // Create initial archive
        let base_files: Vec<(&str, &[u8])> = vec![("base.txt", b"base content")];
        let base_path = create_test_archive(temp.path(), "base.mpq", &base_files);

        // Create chain with base archive
        let mut chain = PatchChain::new();
        chain.add_archive(&base_path, 0).unwrap();

        // Create multiple patch archives
        let mut patch_archives = Vec::new();
        for i in 1..=3 {
            let patch_name = format!("patch_{i}.txt");
            let patch_content = format!("Patch {i} content");
            let common_content = format!("Common from patch {i}");
            let files: Vec<(&str, &[u8])> = vec![
                (&patch_name, patch_content.as_bytes()),
                ("common.txt", common_content.as_bytes()),
            ];
            let path = create_test_archive(temp.path(), &format!("patch_{i}.mpq"), &files);
            patch_archives.push((path, i * 100));
        }

        // Add patches in parallel
        chain.add_archives_parallel(patch_archives).unwrap();

        // Verify all files are accessible
        assert_eq!(chain.read_file("base.txt").unwrap(), b"base content");
        assert_eq!(chain.read_file("patch_1.txt").unwrap(), b"Patch 1 content");
        assert_eq!(chain.read_file("patch_2.txt").unwrap(), b"Patch 2 content");
        assert_eq!(chain.read_file("patch_3.txt").unwrap(), b"Patch 3 content");

        // Verify priority (patch 3 has highest priority)
        assert_eq!(
            chain.read_file("common.txt").unwrap(),
            b"Common from patch 3"
        );

        // Verify chain info
        let info = chain.get_chain_info();
        assert_eq!(info.len(), 4); // base + 3 patches
    }

    #[test]
    fn test_parallel_loading_with_invalid_archive() {
        let temp = TempDir::new().unwrap();

        // Create some valid archives
        let mut archives = Vec::new();
        for i in 0..2 {
            let file_name = format!("file_{i}.txt");
            let files: Vec<(&str, &[u8])> = vec![(&file_name, b"content")];
            let path = create_test_archive(temp.path(), &format!("valid_{i}.mpq"), &files);
            archives.push((path, i * 100));
        }

        // Add a non-existent archive
        archives.push((temp.path().join("nonexistent.mpq"), 200));

        // Try to load in parallel - should fail
        let result = PatchChain::from_archives_parallel(archives);
        assert!(result.is_err());
    }
}