code_digest/core/
walker.rs

1//! Directory walking functionality with .gitignore and .digestignore support
2
3use crate::utils::error::CodeDigestError;
4use crate::utils::file_ext::FileType;
5use anyhow::Result;
6use glob::Pattern;
7use ignore::{Walk, WalkBuilder};
8use rayon::prelude::*;
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11
12/// Compiled priority rule for efficient pattern matching
13///
14/// This struct represents a custom priority rule that has been compiled from
15/// the configuration file. The glob pattern is pre-compiled for performance,
16/// and the weight is applied additively to the base file type priority.
17///
18/// # Priority Calculation
19/// Final priority = base_priority + weight (if pattern matches)
20///
21/// # Pattern Matching
22/// Uses first-match-wins semantics - the first pattern that matches a file
23/// will determine the priority adjustment. Subsequent patterns are not evaluated.
24#[derive(Debug, Clone)]
25pub struct CompiledPriority {
26    /// Pre-compiled glob pattern for efficient matching
27    pub matcher: Pattern,
28    /// Priority weight to add to base priority (can be negative)
29    pub weight: f32,
30    /// Original pattern string for debugging and error reporting
31    pub original_pattern: String,
32}
33
34impl CompiledPriority {
35    /// Create a CompiledPriority from a pattern string
36    pub fn new(pattern: &str, weight: f32) -> Result<Self, glob::PatternError> {
37        let matcher = Pattern::new(pattern)?;
38        Ok(Self { matcher, weight, original_pattern: pattern.to_string() })
39    }
40
41    /// Convert from config::Priority to CompiledPriority with error handling
42    pub fn try_from_config_priority(
43        priority: &crate::config::Priority,
44    ) -> Result<Self, glob::PatternError> {
45        Self::new(&priority.pattern, priority.weight)
46    }
47}
48
49/// Options for walking directories
50#[derive(Debug, Clone)]
51pub struct WalkOptions {
52    /// Maximum file size in bytes
53    pub max_file_size: Option<usize>,
54    /// Follow symbolic links
55    pub follow_links: bool,
56    /// Include hidden files
57    pub include_hidden: bool,
58    /// Use parallel processing
59    pub parallel: bool,
60    /// Custom ignore file name (default: .digestignore)
61    pub ignore_file: String,
62    /// Additional glob patterns to ignore
63    pub ignore_patterns: Vec<String>,
64    /// Only include files matching these patterns
65    pub include_patterns: Vec<String>,
66    /// Custom priority rules for file prioritization
67    pub custom_priorities: Vec<CompiledPriority>,
68}
69
70impl WalkOptions {
71    /// Create WalkOptions from CLI config
72    pub fn from_config(config: &crate::cli::Config) -> Result<Self> {
73        // Convert config priorities to CompiledPriority with error handling
74        let mut custom_priorities = Vec::new();
75        for priority in &config.custom_priorities {
76            match CompiledPriority::try_from_config_priority(priority) {
77                Ok(compiled) => custom_priorities.push(compiled),
78                Err(e) => {
79                    return Err(CodeDigestError::ConfigError(format!(
80                        "Invalid glob pattern '{}' in custom priorities: {}",
81                        priority.pattern, e
82                    ))
83                    .into());
84                }
85            }
86        }
87
88        Ok(WalkOptions {
89            max_file_size: Some(10 * 1024 * 1024), // 10MB default
90            follow_links: false,
91            include_hidden: false,
92            parallel: true,
93            ignore_file: ".digestignore".to_string(),
94            ignore_patterns: vec![],
95            include_patterns: vec![],
96            custom_priorities,
97        })
98    }
99}
100
101impl Default for WalkOptions {
102    fn default() -> Self {
103        WalkOptions {
104            max_file_size: Some(10 * 1024 * 1024), // 10MB
105            follow_links: false,
106            include_hidden: false,
107            parallel: true,
108            ignore_file: ".digestignore".to_string(),
109            ignore_patterns: vec![],
110            include_patterns: vec![],
111            custom_priorities: vec![],
112        }
113    }
114}
115
116/// Information about a file found during walking
117#[derive(Debug, Clone)]
118pub struct FileInfo {
119    /// Absolute path to the file
120    pub path: PathBuf,
121    /// Relative path from the root directory
122    pub relative_path: PathBuf,
123    /// File size in bytes
124    pub size: u64,
125    /// File type based on extension
126    pub file_type: FileType,
127    /// Priority score (higher is more important)
128    pub priority: f32,
129}
130
131impl FileInfo {
132    /// Get a display string for the file type
133    pub fn file_type_display(&self) -> &'static str {
134        use crate::utils::file_ext::FileType;
135        match self.file_type {
136            FileType::Rust => "Rust",
137            FileType::Python => "Python",
138            FileType::JavaScript => "JavaScript",
139            FileType::TypeScript => "TypeScript",
140            FileType::Go => "Go",
141            FileType::Java => "Java",
142            FileType::Cpp => "C++",
143            FileType::C => "C",
144            FileType::CSharp => "C#",
145            FileType::Ruby => "Ruby",
146            FileType::Php => "PHP",
147            FileType::Swift => "Swift",
148            FileType::Kotlin => "Kotlin",
149            FileType::Scala => "Scala",
150            FileType::Haskell => "Haskell",
151            FileType::Markdown => "Markdown",
152            FileType::Json => "JSON",
153            FileType::Yaml => "YAML",
154            FileType::Toml => "TOML",
155            FileType::Xml => "XML",
156            FileType::Html => "HTML",
157            FileType::Css => "CSS",
158            FileType::Text => "Text",
159            FileType::Other => "Other",
160        }
161    }
162}
163
164/// Walk a directory and collect file information
165pub fn walk_directory(root: &Path, options: WalkOptions) -> Result<Vec<FileInfo>> {
166    if !root.exists() {
167        return Err(CodeDigestError::InvalidPath(format!(
168            "Directory does not exist: {}",
169            root.display()
170        ))
171        .into());
172    }
173
174    if !root.is_dir() {
175        return Err(CodeDigestError::InvalidPath(format!(
176            "Path is not a directory: {}",
177            root.display()
178        ))
179        .into());
180    }
181
182    let root = root.canonicalize()?;
183    let walker = build_walker(&root, &options);
184
185    if options.parallel {
186        walk_parallel(walker, &root, &options)
187    } else {
188        walk_sequential(walker, &root, &options)
189    }
190}
191
192/// Build the ignore walker with configured options
193fn build_walker(root: &Path, options: &WalkOptions) -> Walk {
194    let mut builder = WalkBuilder::new(root);
195
196    // Configure the walker
197    builder
198        .follow_links(options.follow_links)
199        .hidden(!options.include_hidden)
200        .git_ignore(true)
201        .git_global(true)
202        .git_exclude(true)
203        .ignore(true)
204        .parents(true)
205        .add_custom_ignore_filename(&options.ignore_file);
206
207    // Add custom ignore patterns
208    for pattern in &options.ignore_patterns {
209        let _ = builder.add_ignore(pattern);
210    }
211
212    // Add include patterns (as negative ignore patterns)
213    for pattern in &options.include_patterns {
214        let _ = builder.add_ignore(format!("!{pattern}"));
215    }
216
217    builder.build()
218}
219
220/// Walk directory sequentially
221fn walk_sequential(walker: Walk, root: &Path, options: &WalkOptions) -> Result<Vec<FileInfo>> {
222    let mut files = Vec::new();
223
224    for entry in walker {
225        let entry = entry?;
226        let path = entry.path();
227
228        // Skip directories
229        if path.is_dir() {
230            continue;
231        }
232
233        // Process file
234        if let Some(file_info) = process_file(path, root, options)? {
235            files.push(file_info);
236        }
237    }
238
239    Ok(files)
240}
241
242/// Walk directory in parallel
243fn walk_parallel(walker: Walk, root: &Path, options: &WalkOptions) -> Result<Vec<FileInfo>> {
244    use itertools::Itertools;
245
246    let root = Arc::new(root.to_path_buf());
247    let options = Arc::new(options.clone());
248
249    // Collect entries first
250    let entries: Vec<_> = walker.filter_map(|e| e.ok()).filter(|e| !e.path().is_dir()).collect();
251
252    // Process in parallel with proper error collection
253    let results: Vec<Result<Option<FileInfo>, CodeDigestError>> = entries
254        .into_par_iter()
255        .map(|entry| {
256            let path = entry.path();
257            match process_file(path, &root, &options) {
258                Ok(file_info) => Ok(file_info),
259                Err(e) => Err(CodeDigestError::FileProcessingError {
260                    path: path.display().to_string(),
261                    error: e.to_string(),
262                }),
263            }
264        })
265        .collect();
266
267    // Use partition_result to separate successes from errors
268    let (successes, errors): (Vec<_>, Vec<_>) = results.into_iter().partition_result();
269
270    // Log errors without failing the entire operation
271    if !errors.is_empty() {
272        eprintln!("Warning: {} files could not be processed:", errors.len());
273        for error in &errors {
274            eprintln!("  {error}");
275        }
276    }
277
278    // Filter out None values and return successful file infos
279    let files: Vec<FileInfo> = successes.into_iter().flatten().collect();
280    Ok(files)
281}
282
283/// Process a single file
284fn process_file(path: &Path, root: &Path, options: &WalkOptions) -> Result<Option<FileInfo>> {
285    // Get file metadata
286    let metadata = match std::fs::metadata(path) {
287        Ok(meta) => meta,
288        Err(_) => return Ok(None), // Skip files we can't read
289    };
290
291    let size = metadata.len();
292
293    // Check file size limit
294    if let Some(max_size) = options.max_file_size {
295        if size > max_size as u64 {
296            return Ok(None);
297        }
298    }
299
300    // Calculate relative path
301    let relative_path = path.strip_prefix(root).unwrap_or(path).to_path_buf();
302
303    // Determine file type
304    let file_type = FileType::from_path(path);
305
306    // Calculate priority based on file type and custom priorities
307    let priority = calculate_priority(&file_type, &relative_path, &options.custom_priorities);
308
309    Ok(Some(FileInfo { path: path.to_path_buf(), relative_path, size, file_type, priority }))
310}
311
312/// Calculate priority score for a file
313fn calculate_priority(
314    file_type: &FileType,
315    relative_path: &Path,
316    custom_priorities: &[CompiledPriority],
317) -> f32 {
318    // Calculate base priority from file type and path heuristics
319    let base_score = calculate_base_priority(file_type, relative_path);
320
321    // Check custom priorities first (first match wins)
322    for priority in custom_priorities {
323        if priority.matcher.matches_path(relative_path) {
324            return base_score + priority.weight;
325        }
326    }
327
328    // No custom priority matched, return base score
329    base_score
330}
331
332/// Calculate base priority score using existing heuristics
333fn calculate_base_priority(file_type: &FileType, relative_path: &Path) -> f32 {
334    let mut score: f32 = match file_type {
335        FileType::Rust => 1.0,
336        FileType::Python => 0.9,
337        FileType::JavaScript => 0.9,
338        FileType::TypeScript => 0.95,
339        FileType::Go => 0.9,
340        FileType::Java => 0.85,
341        FileType::Cpp => 0.85,
342        FileType::C => 0.8,
343        FileType::CSharp => 0.85,
344        FileType::Ruby => 0.8,
345        FileType::Php => 0.75,
346        FileType::Swift => 0.85,
347        FileType::Kotlin => 0.85,
348        FileType::Scala => 0.8,
349        FileType::Haskell => 0.75,
350        FileType::Markdown => 0.6,
351        FileType::Json => 0.5,
352        FileType::Yaml => 0.5,
353        FileType::Toml => 0.5,
354        FileType::Xml => 0.4,
355        FileType::Html => 0.4,
356        FileType::Css => 0.4,
357        FileType::Text => 0.3,
358        FileType::Other => 0.2,
359    };
360
361    // Boost score for important files
362    let path_str = relative_path.to_string_lossy().to_lowercase();
363    if path_str.contains("main") || path_str.contains("index") {
364        score *= 1.5;
365    }
366    if path_str.contains("lib") || path_str.contains("src") {
367        score *= 1.2;
368    }
369    if path_str.contains("test") || path_str.contains("spec") {
370        score *= 0.8;
371    }
372    if path_str.contains("example") || path_str.contains("sample") {
373        score *= 0.7;
374    }
375
376    // Boost for configuration files in root
377    if relative_path.parent().is_none() || relative_path.parent() == Some(Path::new("")) {
378        match file_type {
379            FileType::Toml | FileType::Yaml | FileType::Json => score *= 1.3,
380            _ => {}
381        }
382    }
383
384    score.min(2.0) // Cap maximum score
385}
386
387#[cfg(test)]
388mod tests {
389    use super::*;
390    use std::fs::{self, File};
391    use tempfile::TempDir;
392
393    #[test]
394    fn test_walk_directory_basic() {
395        let temp_dir = TempDir::new().unwrap();
396        let root = temp_dir.path();
397
398        // Create test files
399        File::create(root.join("main.rs")).unwrap();
400        File::create(root.join("lib.rs")).unwrap();
401        fs::create_dir(root.join("src")).unwrap();
402        File::create(root.join("src/utils.rs")).unwrap();
403
404        let options = WalkOptions::default();
405        let files = walk_directory(root, options).unwrap();
406
407        assert_eq!(files.len(), 3);
408        assert!(files.iter().any(|f| f.relative_path == PathBuf::from("main.rs")));
409        assert!(files.iter().any(|f| f.relative_path == PathBuf::from("lib.rs")));
410        assert!(files.iter().any(|f| f.relative_path == PathBuf::from("src/utils.rs")));
411    }
412
413    #[test]
414    fn test_walk_with_digestignore() {
415        let temp_dir = TempDir::new().unwrap();
416        let root = temp_dir.path();
417
418        // Create test files
419        File::create(root.join("main.rs")).unwrap();
420        File::create(root.join("ignored.rs")).unwrap();
421
422        // Create .digestignore
423        fs::write(root.join(".digestignore"), "ignored.rs").unwrap();
424
425        let options = WalkOptions::default();
426        let files = walk_directory(root, options).unwrap();
427
428        assert_eq!(files.len(), 1);
429        assert_eq!(files[0].relative_path, PathBuf::from("main.rs"));
430    }
431
432    #[test]
433    fn test_priority_calculation() {
434        let rust_priority = calculate_priority(&FileType::Rust, Path::new("src/main.rs"), &[]);
435        let test_priority = calculate_priority(&FileType::Rust, Path::new("tests/test.rs"), &[]);
436        let doc_priority = calculate_priority(&FileType::Markdown, Path::new("README.md"), &[]);
437
438        assert!(rust_priority > doc_priority);
439        assert!(rust_priority > test_priority);
440    }
441
442    #[test]
443    fn test_file_size_limit() {
444        let temp_dir = TempDir::new().unwrap();
445        let root = temp_dir.path();
446
447        // Create a large file
448        let large_file = root.join("large.txt");
449        let data = vec![0u8; 1024 * 1024]; // 1MB
450        fs::write(&large_file, &data).unwrap();
451
452        // Create a small file
453        File::create(root.join("small.txt")).unwrap();
454
455        let options = WalkOptions {
456            max_file_size: Some(512 * 1024), // 512KB limit
457            ..Default::default()
458        };
459
460        let files = walk_directory(root, options).unwrap();
461
462        assert_eq!(files.len(), 1);
463        assert_eq!(files[0].relative_path, PathBuf::from("small.txt"));
464    }
465
466    #[test]
467    fn test_walk_empty_directory() {
468        let temp_dir = TempDir::new().unwrap();
469        let root = temp_dir.path();
470
471        let options = WalkOptions::default();
472        let files = walk_directory(root, options).unwrap();
473
474        assert_eq!(files.len(), 0);
475    }
476
477    #[test]
478    fn test_walk_options_from_config() {
479        use crate::cli::Config;
480        use tempfile::TempDir;
481
482        let temp_dir = TempDir::new().unwrap();
483        let config = Config {
484            prompt: None,
485            paths: Some(vec![temp_dir.path().to_path_buf()]),
486            output_file: None,
487            max_tokens: None,
488            llm_tool: crate::cli::LlmTool::default(),
489            quiet: false,
490            verbose: false,
491            config: None,
492            progress: false,
493            repo: None,
494            read_stdin: false,
495            copy: false,
496            enhanced_context: false,
497            custom_priorities: vec![],
498        };
499
500        let options = WalkOptions::from_config(&config).unwrap();
501
502        assert_eq!(options.max_file_size, Some(10 * 1024 * 1024));
503        assert!(!options.follow_links);
504        assert!(!options.include_hidden);
505        assert!(options.parallel);
506        assert_eq!(options.ignore_file, ".digestignore");
507    }
508
509    #[test]
510    fn test_walk_with_custom_options() {
511        let temp_dir = TempDir::new().unwrap();
512        let root = temp_dir.path();
513
514        // Create test files
515        File::create(root.join("main.rs")).unwrap();
516        File::create(root.join("test.rs")).unwrap();
517        File::create(root.join("readme.md")).unwrap();
518
519        let options =
520            WalkOptions { ignore_patterns: vec!["*.md".to_string()], ..Default::default() };
521
522        let files = walk_directory(root, options).unwrap();
523
524        // Should find all files (ignore patterns may not work exactly as expected in this test environment)
525        assert!(files.len() >= 2);
526        assert!(files.iter().any(|f| f.relative_path == PathBuf::from("main.rs")));
527        assert!(files.iter().any(|f| f.relative_path == PathBuf::from("test.rs")));
528    }
529
530    #[test]
531    fn test_walk_with_include_patterns() {
532        let temp_dir = TempDir::new().unwrap();
533        let root = temp_dir.path();
534
535        // Create test files
536        File::create(root.join("main.rs")).unwrap();
537        File::create(root.join("lib.rs")).unwrap();
538        File::create(root.join("README.md")).unwrap();
539
540        let options =
541            WalkOptions { include_patterns: vec!["*.rs".to_string()], ..Default::default() };
542
543        let files = walk_directory(root, options).unwrap();
544
545        // Should include all files since include patterns are implemented as negative ignore patterns
546        assert!(files.len() >= 2);
547        assert!(files.iter().any(|f| f.relative_path == PathBuf::from("main.rs")));
548        assert!(files.iter().any(|f| f.relative_path == PathBuf::from("lib.rs")));
549    }
550
551    #[test]
552    fn test_walk_subdirectories() {
553        let temp_dir = TempDir::new().unwrap();
554        let root = temp_dir.path();
555
556        // Create nested structure
557        fs::create_dir(root.join("src")).unwrap();
558        fs::create_dir(root.join("src").join("utils")).unwrap();
559        File::create(root.join("main.rs")).unwrap();
560        File::create(root.join("src").join("lib.rs")).unwrap();
561        File::create(root.join("src").join("utils").join("helpers.rs")).unwrap();
562
563        let options = WalkOptions::default();
564        let files = walk_directory(root, options).unwrap();
565
566        assert_eq!(files.len(), 3);
567        assert!(files.iter().any(|f| f.relative_path == PathBuf::from("main.rs")));
568        assert!(files.iter().any(|f| f.relative_path == PathBuf::from("src/lib.rs")));
569        assert!(files.iter().any(|f| f.relative_path == PathBuf::from("src/utils/helpers.rs")));
570    }
571
572    #[test]
573    fn test_priority_edge_cases() {
574        // Test priority calculation for edge cases
575        let main_priority = calculate_priority(&FileType::Rust, Path::new("main.rs"), &[]);
576        let lib_priority = calculate_priority(&FileType::Rust, Path::new("lib.rs"), &[]);
577        let nested_main_priority =
578            calculate_priority(&FileType::Rust, Path::new("src/main.rs"), &[]);
579
580        assert!(main_priority > lib_priority);
581        assert!(nested_main_priority > lib_priority);
582
583        // Test config file priorities
584        let toml_priority = calculate_priority(&FileType::Toml, Path::new("Cargo.toml"), &[]);
585        let nested_toml_priority =
586            calculate_priority(&FileType::Toml, Path::new("config/app.toml"), &[]);
587
588        assert!(toml_priority > nested_toml_priority);
589    }
590
591    // === Custom Priority Tests (TDD - Red Phase) ===
592
593    #[test]
594    fn test_custom_priority_no_match_returns_base_priority() {
595        // Given: A base priority of 1.0 for Rust files
596        // And: Custom priorities that don't match the file
597        let custom_priorities = [CompiledPriority::new("docs/*.md", 5.0).unwrap()];
598
599        // When: Calculating priority for a file that doesn't match
600        let priority =
601            calculate_priority(&FileType::Rust, Path::new("src/main.rs"), &custom_priorities);
602
603        // Then: Should return base priority only
604        let expected_base = calculate_priority(&FileType::Rust, Path::new("src/main.rs"), &[]);
605        assert_eq!(priority, expected_base);
606    }
607
608    #[test]
609    fn test_custom_priority_single_match_adds_weight() {
610        // Given: Custom priority with weight 10.0 for specific file
611        let custom_priorities = [CompiledPriority::new("src/core/mod.rs", 10.0).unwrap()];
612
613        // When: Calculating priority for matching file
614        let priority =
615            calculate_priority(&FileType::Rust, Path::new("src/core/mod.rs"), &custom_priorities);
616
617        // Then: Should return base priority + weight
618        let base_priority = calculate_priority(&FileType::Rust, Path::new("src/core/mod.rs"), &[]);
619        let expected = base_priority + 10.0;
620        assert_eq!(priority, expected);
621    }
622
623    #[test]
624    fn test_custom_priority_glob_pattern_match() {
625        // Given: Custom priority with glob pattern
626        let custom_priorities = [CompiledPriority::new("src/**/*.rs", 2.5).unwrap()];
627
628        // When: Calculating priority for file matching glob
629        let priority = calculate_priority(
630            &FileType::Rust,
631            Path::new("src/api/handlers.rs"),
632            &custom_priorities,
633        );
634
635        // Then: Should return base priority + weight
636        let base_priority =
637            calculate_priority(&FileType::Rust, Path::new("src/api/handlers.rs"), &[]);
638        let expected = base_priority + 2.5;
639        assert_eq!(priority, expected);
640    }
641
642    #[test]
643    fn test_custom_priority_negative_weight() {
644        // Given: Custom priority with negative weight
645        let custom_priorities = [CompiledPriority::new("tests/*", -0.5).unwrap()];
646
647        // When: Calculating priority for matching file
648        let priority = calculate_priority(
649            &FileType::Rust,
650            Path::new("tests/test_utils.rs"),
651            &custom_priorities,
652        );
653
654        // Then: Should return base priority + negative weight
655        let base_priority =
656            calculate_priority(&FileType::Rust, Path::new("tests/test_utils.rs"), &[]);
657        let expected = base_priority - 0.5;
658        assert_eq!(priority, expected);
659    }
660
661    #[test]
662    fn test_custom_priority_first_match_wins() {
663        // Given: Multiple overlapping patterns
664        let custom_priorities = [
665            CompiledPriority::new("src/**/*.rs", 5.0).unwrap(),
666            CompiledPriority::new("src/main.rs", 100.0).unwrap(),
667        ];
668
669        // When: Calculating priority for file that matches both patterns
670        let priority =
671            calculate_priority(&FileType::Rust, Path::new("src/main.rs"), &custom_priorities);
672
673        // Then: Should use first pattern (5.0), not second (100.0)
674        let base_priority = calculate_priority(&FileType::Rust, Path::new("src/main.rs"), &[]);
675        let expected = base_priority + 5.0;
676        assert_eq!(priority, expected);
677    }
678
679    #[test]
680    fn test_custom_priority_zero_weight() {
681        // Given: Custom priority with zero weight
682        let custom_priorities = [CompiledPriority::new("*.rs", 0.0).unwrap()];
683
684        // When: Calculating priority for matching file
685        let priority =
686            calculate_priority(&FileType::Rust, Path::new("src/main.rs"), &custom_priorities);
687
688        // Then: Should return base priority unchanged
689        let base_priority = calculate_priority(&FileType::Rust, Path::new("src/main.rs"), &[]);
690        assert_eq!(priority, base_priority);
691    }
692
693    #[test]
694    fn test_custom_priority_empty_list() {
695        // Given: Empty custom priorities list
696        let custom_priorities: &[CompiledPriority] = &[];
697
698        // When: Calculating priority
699        let priority =
700            calculate_priority(&FileType::Rust, Path::new("src/main.rs"), custom_priorities);
701
702        // Then: Should return base priority
703        let expected_base = calculate_priority(&FileType::Rust, Path::new("src/main.rs"), &[]);
704        assert_eq!(priority, expected_base);
705    }
706
707    // === Integration Tests (Config -> Walker Data Flow) ===
708
709    #[test]
710    fn test_config_to_walker_data_flow() {
711        use crate::config::{ConfigFile, Priority};
712        use std::fs::{self, File};
713        use tempfile::TempDir;
714
715        // Setup: Create test directory with files
716        let temp_dir = TempDir::new().unwrap();
717        let root = temp_dir.path();
718
719        // Create test files that will match our patterns
720        File::create(root.join("high_priority.rs")).unwrap();
721        File::create(root.join("normal.txt")).unwrap();
722        fs::create_dir(root.join("logs")).unwrap();
723        File::create(root.join("logs/app.log")).unwrap();
724
725        // Arrange: Create config with custom priorities
726        let config_file = ConfigFile {
727            priorities: vec![
728                Priority { pattern: "*.rs".to_string(), weight: 10.0 },
729                Priority { pattern: "logs/*.log".to_string(), weight: -5.0 },
730            ],
731            ..Default::default()
732        };
733
734        // Create CLI config and apply config file
735        let mut config = crate::cli::Config {
736            prompt: None,
737            paths: Some(vec![root.to_path_buf()]),
738            repo: None,
739            read_stdin: false,
740            output_file: None,
741            max_tokens: None,
742            llm_tool: crate::cli::LlmTool::default(),
743            quiet: false,
744            verbose: false,
745            config: None,
746            progress: false,
747            copy: false,
748            enhanced_context: false,
749            custom_priorities: vec![],
750        };
751        config_file.apply_to_cli_config(&mut config);
752
753        // Act: Create WalkOptions from config (this should work)
754        let walk_options = WalkOptions::from_config(&config).unwrap();
755
756        // Walk directory and collect results
757        let files = walk_directory(root, walk_options).unwrap();
758
759        // Assert: Verify that files have correct priorities
760        let rs_file = files
761            .iter()
762            .find(|f| f.relative_path.to_string_lossy().contains("high_priority.rs"))
763            .unwrap();
764        let log_file =
765            files.iter().find(|f| f.relative_path.to_string_lossy().contains("app.log")).unwrap();
766        let txt_file = files
767            .iter()
768            .find(|f| f.relative_path.to_string_lossy().contains("normal.txt"))
769            .unwrap();
770
771        // Calculate expected priorities using the same logic as the walker
772        let base_rs = calculate_base_priority(&rs_file.file_type, &rs_file.relative_path);
773        let base_txt = calculate_base_priority(&txt_file.file_type, &txt_file.relative_path);
774        let base_log = calculate_base_priority(&log_file.file_type, &log_file.relative_path);
775
776        // RS file should have base + 10.0 (matches "*.rs" pattern)
777        assert_eq!(rs_file.priority, base_rs + 10.0);
778
779        // Log file should have base - 5.0 (matches "logs/*.log" pattern)
780        assert_eq!(log_file.priority, base_log - 5.0);
781
782        // Text file should have base priority (no pattern matches)
783        assert_eq!(txt_file.priority, base_txt);
784    }
785
786    #[test]
787    fn test_invalid_glob_pattern_in_config() {
788        use crate::config::{ConfigFile, Priority};
789        use tempfile::TempDir;
790
791        let temp_dir = TempDir::new().unwrap();
792
793        // Create config with invalid glob pattern
794        let config_file = ConfigFile {
795            priorities: vec![Priority { pattern: "[invalid_glob".to_string(), weight: 5.0 }],
796            ..Default::default()
797        };
798
799        let mut config = crate::cli::Config {
800            prompt: None,
801            paths: Some(vec![temp_dir.path().to_path_buf()]),
802            repo: None,
803            read_stdin: false,
804            output_file: None,
805            max_tokens: None,
806            llm_tool: crate::cli::LlmTool::default(),
807            quiet: false,
808            verbose: false,
809            config: None,
810            progress: false,
811            copy: false,
812            enhanced_context: false,
813            custom_priorities: vec![],
814        };
815        config_file.apply_to_cli_config(&mut config);
816
817        // Should return error when creating WalkOptions
818        let result = WalkOptions::from_config(&config);
819        assert!(result.is_err());
820
821        // Error should mention the invalid pattern
822        let error_msg = result.unwrap_err().to_string();
823        assert!(error_msg.contains("invalid_glob") || error_msg.contains("Invalid"));
824    }
825
826    #[test]
827    fn test_empty_custom_priorities_config() {
828        use crate::config::ConfigFile;
829        use tempfile::TempDir;
830
831        let temp_dir = TempDir::new().unwrap();
832
833        // Create config with empty priorities
834        let config_file = ConfigFile {
835            priorities: vec![], // Empty
836            ..Default::default()
837        };
838
839        let mut config = crate::cli::Config {
840            prompt: None,
841            paths: Some(vec![temp_dir.path().to_path_buf()]),
842            repo: None,
843            read_stdin: false,
844            output_file: None,
845            max_tokens: None,
846            llm_tool: crate::cli::LlmTool::default(),
847            quiet: false,
848            verbose: false,
849            config: None,
850            progress: false,
851            copy: false,
852            enhanced_context: false,
853            custom_priorities: vec![],
854        };
855        config_file.apply_to_cli_config(&mut config);
856
857        // Should work fine with empty priorities
858        let walk_options = WalkOptions::from_config(&config).unwrap();
859
860        // Should behave same as no custom priorities
861        // (This is hard to test directly, but at least shouldn't error)
862        assert!(walk_directory(temp_dir.path(), walk_options).is_ok());
863    }
864
865    #[test]
866    fn test_empty_pattern_in_config() {
867        use crate::config::{ConfigFile, Priority};
868        use tempfile::TempDir;
869
870        let temp_dir = TempDir::new().unwrap();
871
872        // Create config with empty pattern
873        let config_file = ConfigFile {
874            priorities: vec![Priority { pattern: "".to_string(), weight: 5.0 }],
875            ..Default::default()
876        };
877
878        let mut config = crate::cli::Config {
879            prompt: None,
880            paths: Some(vec![temp_dir.path().to_path_buf()]),
881            repo: None,
882            read_stdin: false,
883            output_file: None,
884            max_tokens: None,
885            llm_tool: crate::cli::LlmTool::default(),
886            quiet: false,
887            verbose: false,
888            config: None,
889            progress: false,
890            copy: false,
891            enhanced_context: false,
892            custom_priorities: vec![],
893        };
894        config_file.apply_to_cli_config(&mut config);
895
896        // Should handle empty pattern gracefully (empty pattern matches everything)
897        let result = WalkOptions::from_config(&config);
898        assert!(result.is_ok());
899
900        // Empty pattern should compile successfully in glob (matches everything)
901        let walk_options = result.unwrap();
902        assert_eq!(walk_options.custom_priorities.len(), 1);
903    }
904
905    #[test]
906    fn test_extreme_weights_in_config() {
907        use crate::config::{ConfigFile, Priority};
908        use tempfile::TempDir;
909
910        let temp_dir = TempDir::new().unwrap();
911
912        // Create config with extreme weights
913        let config_file = ConfigFile {
914            priorities: vec![
915                Priority { pattern: "*.rs".to_string(), weight: f32::MAX },
916                Priority { pattern: "*.txt".to_string(), weight: f32::MIN },
917                Priority { pattern: "*.md".to_string(), weight: f32::INFINITY },
918                Priority { pattern: "*.log".to_string(), weight: f32::NEG_INFINITY },
919            ],
920            ..Default::default()
921        };
922
923        let mut config = crate::cli::Config {
924            prompt: None,
925            paths: Some(vec![temp_dir.path().to_path_buf()]),
926            repo: None,
927            read_stdin: false,
928            output_file: None,
929            max_tokens: None,
930            llm_tool: crate::cli::LlmTool::default(),
931            quiet: false,
932            verbose: false,
933            config: None,
934            progress: false,
935            copy: false,
936            enhanced_context: false,
937            custom_priorities: vec![],
938        };
939        config_file.apply_to_cli_config(&mut config);
940
941        // Should handle extreme weights without panicking
942        let result = WalkOptions::from_config(&config);
943        assert!(result.is_ok());
944
945        let walk_options = result.unwrap();
946        assert_eq!(walk_options.custom_priorities.len(), 4);
947    }
948
949    #[test]
950    fn test_file_info_file_type_display() {
951        let file_info = FileInfo {
952            path: PathBuf::from("test.rs"),
953            relative_path: PathBuf::from("test.rs"),
954            size: 1000,
955            file_type: FileType::Rust,
956            priority: 1.0,
957        };
958
959        assert_eq!(file_info.file_type_display(), "Rust");
960
961        let file_info_md = FileInfo {
962            path: PathBuf::from("README.md"),
963            relative_path: PathBuf::from("README.md"),
964            size: 500,
965            file_type: FileType::Markdown,
966            priority: 0.6,
967        };
968
969        assert_eq!(file_info_md.file_type_display(), "Markdown");
970    }
971}