intent_engine/
project.rs

1use crate::db::{create_pool, run_migrations};
2use crate::error::{IntentError, Result};
3use serde::{Deserialize, Serialize};
4use sqlx::SqlitePool;
5use std::path::PathBuf;
6
7const INTENT_DIR: &str = ".intent-engine";
8const DB_FILE: &str = "project.db";
9
10/// Project root markers in priority order (highest priority first)
11/// These are used to identify the root directory of a project
12const PROJECT_ROOT_MARKERS: &[&str] = &[
13    ".git",           // Git (highest priority)
14    ".hg",            // Mercurial
15    "package.json",   // Node.js
16    "Cargo.toml",     // Rust
17    "pyproject.toml", // Python (PEP 518)
18    "go.mod",         // Go Modules
19    "pom.xml",        // Maven (Java)
20    "build.gradle",   // Gradle (Java/Kotlin)
21];
22
23#[derive(Debug)]
24pub struct ProjectContext {
25    pub root: PathBuf,
26    pub db_path: PathBuf,
27    pub pool: SqlitePool,
28}
29
30/// Information about directory traversal for database location
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct DirectoryTraversalInfo {
33    pub path: String,
34    pub has_intent_engine: bool,
35    pub is_selected: bool,
36}
37
38/// Detailed information about database path resolution
39#[derive(Debug, Serialize, Deserialize)]
40pub struct DatabasePathInfo {
41    pub current_working_directory: String,
42    pub env_var_set: bool,
43    pub env_var_path: Option<String>,
44    pub env_var_valid: Option<bool>,
45    pub directories_checked: Vec<DirectoryTraversalInfo>,
46    pub home_directory: Option<String>,
47    pub home_has_intent_engine: bool,
48    pub final_database_path: Option<String>,
49    pub resolution_method: Option<String>,
50}
51
52impl ProjectContext {
53    /// Collect detailed information about database path resolution for diagnostics
54    ///
55    /// This function traces through all the steps of finding the database location,
56    /// showing which directories were checked and why a particular location was chosen.
57    pub fn get_database_path_info() -> DatabasePathInfo {
58        let cwd = std::env::current_dir()
59            .ok()
60            .map(|p| p.display().to_string())
61            .unwrap_or_else(|| "<unable to determine>".to_string());
62
63        let mut info = DatabasePathInfo {
64            current_working_directory: cwd.clone(),
65            env_var_set: false,
66            env_var_path: None,
67            env_var_valid: None,
68            directories_checked: Vec::new(),
69            home_directory: None,
70            home_has_intent_engine: false,
71            final_database_path: None,
72            resolution_method: None,
73        };
74
75        // Check strategy 1: Environment variable
76        if let Ok(env_path) = std::env::var("INTENT_ENGINE_PROJECT_DIR") {
77            info.env_var_set = true;
78            info.env_var_path = Some(env_path.clone());
79
80            let path = PathBuf::from(&env_path);
81            let intent_dir = path.join(INTENT_DIR);
82            let has_intent_engine = intent_dir.exists() && intent_dir.is_dir();
83            info.env_var_valid = Some(has_intent_engine);
84
85            if has_intent_engine {
86                let db_path = intent_dir.join(DB_FILE);
87                info.final_database_path = Some(db_path.display().to_string());
88                info.resolution_method =
89                    Some("Environment Variable (INTENT_ENGINE_PROJECT_DIR)".to_string());
90                return info;
91            }
92        }
93
94        // Check strategy 2: Upward directory traversal
95        if let Ok(mut current) = std::env::current_dir() {
96            loop {
97                let intent_dir = current.join(INTENT_DIR);
98                let has_intent_engine = intent_dir.exists() && intent_dir.is_dir();
99
100                let is_selected = has_intent_engine && info.final_database_path.is_none();
101
102                info.directories_checked.push(DirectoryTraversalInfo {
103                    path: current.display().to_string(),
104                    has_intent_engine,
105                    is_selected,
106                });
107
108                if has_intent_engine && info.final_database_path.is_none() {
109                    let db_path = intent_dir.join(DB_FILE);
110                    info.final_database_path = Some(db_path.display().to_string());
111                    info.resolution_method = Some("Upward Directory Traversal".to_string());
112                    // Continue traversal to show all directories checked
113                }
114
115                if !current.pop() {
116                    break;
117                }
118            }
119        }
120
121        // Check strategy 3: Home directory
122        #[cfg(not(target_os = "windows"))]
123        let home_path = std::env::var("HOME").ok().map(PathBuf::from);
124
125        #[cfg(target_os = "windows")]
126        let home_path = std::env::var("HOME")
127            .ok()
128            .map(PathBuf::from)
129            .or_else(|| std::env::var("USERPROFILE").ok().map(PathBuf::from));
130
131        if let Some(home) = home_path {
132            info.home_directory = Some(home.display().to_string());
133            let intent_dir = home.join(INTENT_DIR);
134            info.home_has_intent_engine = intent_dir.exists() && intent_dir.is_dir();
135
136            if info.home_has_intent_engine && info.final_database_path.is_none() {
137                let db_path = intent_dir.join(DB_FILE);
138                info.final_database_path = Some(db_path.display().to_string());
139                info.resolution_method = Some("Home Directory Fallback".to_string());
140            }
141        }
142
143        info
144    }
145
146    /// Find the project root by searching upwards for .intent-engine directory
147    ///
148    /// Search strategy (in priority order):
149    /// 1. Check INTENT_ENGINE_PROJECT_DIR environment variable
150    /// 2. Search upwards from current directory for .intent-engine/, but:
151    ///    - Stop at project boundary (defined by PROJECT_ROOT_MARKERS)
152    ///    - Do NOT cross into parent projects to prevent database mixing
153    /// 3. Check user's home directory for .intent-engine/
154    ///
155    /// **Important**: This function now respects project boundaries to prevent
156    /// nested projects from accidentally using parent project databases.
157    pub fn find_project_root() -> Option<PathBuf> {
158        // Strategy 1: Check environment variable (highest priority)
159        if let Ok(env_path) = std::env::var("INTENT_ENGINE_PROJECT_DIR") {
160            let path = PathBuf::from(env_path);
161            let intent_dir = path.join(INTENT_DIR);
162            if intent_dir.exists() && intent_dir.is_dir() {
163                eprintln!(
164                    "✓ Using project from INTENT_ENGINE_PROJECT_DIR: {}",
165                    path.display()
166                );
167                return Some(path);
168            } else {
169                eprintln!(
170                    "⚠ INTENT_ENGINE_PROJECT_DIR set but no .intent-engine found: {}",
171                    path.display()
172                );
173            }
174        }
175
176        // Strategy 2: Search upwards from current directory
177        // BUT respect project boundaries (don't cross into parent projects)
178        if let Ok(current_dir) = std::env::current_dir() {
179            let start_dir = current_dir.clone();
180
181            // First, find the boundary of the current project (if any)
182            // This is the directory that contains a project marker
183            let project_boundary = Self::infer_project_root();
184
185            let mut current = start_dir.clone();
186            loop {
187                let intent_dir = current.join(INTENT_DIR);
188                if intent_dir.exists() && intent_dir.is_dir() {
189                    // Found .intent-engine directory
190
191                    // Check if we're within or at the project boundary
192                    // If there's a project boundary and we've crossed it, don't use this .intent-engine
193                    if let Some(ref boundary) = project_boundary {
194                        // Check if the found .intent-engine is within our project boundary
195                        // (current path should be equal to or a child of boundary)
196                        if !current.starts_with(boundary) && current != *boundary {
197                            // We've crossed the project boundary into a parent project
198                            // Do NOT use this .intent-engine
199                            break;
200                        }
201                    }
202
203                    if current != start_dir {
204                        eprintln!("✓ Found project: {}", current.display());
205                    }
206                    return Some(current);
207                }
208
209                // Check if we've reached the project boundary
210                // If so, stop searching (don't go into parent projects)
211                if let Some(ref boundary) = project_boundary {
212                    if current == *boundary {
213                        // We've reached the boundary without finding .intent-engine
214                        // Stop here and return None (will trigger initialization)
215                        break;
216                    }
217                }
218
219                if !current.pop() {
220                    break;
221                }
222            }
223        }
224
225        // Strategy 3: Check user's home directory (fallback)
226        if let Ok(home) = std::env::var("HOME") {
227            let home_path = PathBuf::from(home);
228            let intent_dir = home_path.join(INTENT_DIR);
229            if intent_dir.exists() && intent_dir.is_dir() {
230                eprintln!("✓ Using home project: {}", home_path.display());
231                return Some(home_path);
232            }
233        }
234
235        // Windows: also check USERPROFILE
236        #[cfg(target_os = "windows")]
237        if let Ok(userprofile) = std::env::var("USERPROFILE") {
238            let home_path = PathBuf::from(userprofile);
239            let intent_dir = home_path.join(INTENT_DIR);
240            if intent_dir.exists() && intent_dir.is_dir() {
241                eprintln!("✓ Using home project: {}", home_path.display());
242                return Some(home_path);
243            }
244        }
245
246        None
247    }
248
249    /// Infer the project root directory based on common project markers
250    ///
251    /// This function implements a smart algorithm to find the project root:
252    /// 1. Start from current directory and traverse upwards
253    /// 2. Check each directory for project markers (in priority order)
254    /// 3. Return the first directory that contains any marker
255    /// 4. If no marker found, return None (fallback to CWD handled by caller)
256    fn infer_project_root() -> Option<PathBuf> {
257        let cwd = std::env::current_dir().ok()?;
258        let mut current = cwd.clone();
259
260        loop {
261            // Check if any marker exists in current directory
262            for marker in PROJECT_ROOT_MARKERS {
263                let marker_path = current.join(marker);
264                if marker_path.exists() {
265                    return Some(current);
266                }
267            }
268
269            // Try to move up to parent directory
270            if !current.pop() {
271                // Reached filesystem root without finding any marker
272                break;
273            }
274        }
275
276        None
277    }
278
279    /// Initialize a new Intent-Engine project using smart root inference
280    ///
281    /// This function implements the smart lazy initialization algorithm:
282    /// 1. Try to infer project root based on common markers
283    /// 2. If inference succeeds, initialize in the inferred root
284    /// 3. If inference fails, fallback to CWD and print warning to stderr
285    pub async fn initialize_project() -> Result<Self> {
286        let cwd = std::env::current_dir()?;
287
288        // Try to infer the project root
289        let root = match Self::infer_project_root() {
290            Some(inferred_root) => {
291                // Successfully inferred project root
292                inferred_root
293            },
294            None => {
295                // Fallback: use current working directory
296                // Print warning to stderr
297                eprintln!(
298                    "Warning: Could not determine a project root based on common markers (e.g., .git, package.json).\n\
299                     Initialized Intent-Engine in the current directory '{}'.\n\
300                     For predictable behavior, it's recommended to initialize from a directory containing a root marker.",
301                    cwd.display()
302                );
303                cwd
304            },
305        };
306
307        let intent_dir = root.join(INTENT_DIR);
308        let db_path = intent_dir.join(DB_FILE);
309
310        // Create .intent-engine directory if it doesn't exist
311        if !intent_dir.exists() {
312            std::fs::create_dir_all(&intent_dir)?;
313        }
314
315        // Create database connection
316        let pool = create_pool(&db_path).await?;
317
318        // Run migrations
319        run_migrations(&pool).await?;
320
321        Ok(ProjectContext {
322            root,
323            db_path,
324            pool,
325        })
326    }
327
328    /// Load an existing project context
329    pub async fn load() -> Result<Self> {
330        let root = Self::find_project_root().ok_or(IntentError::NotAProject)?;
331        let db_path = root.join(INTENT_DIR).join(DB_FILE);
332
333        let pool = create_pool(&db_path).await?;
334
335        Ok(ProjectContext {
336            root,
337            db_path,
338            pool,
339        })
340    }
341
342    /// Load project context, initializing if necessary (for write commands)
343    pub async fn load_or_init() -> Result<Self> {
344        match Self::load().await {
345            Ok(ctx) => Ok(ctx),
346            Err(IntentError::NotAProject) => Self::initialize_project().await,
347            Err(e) => Err(e),
348        }
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355
356    // Note: Tests that modify the current directory are intentionally limited
357    // because they can interfere with other tests running in parallel.
358    // These functionalities are thoroughly tested by integration tests.
359
360    #[test]
361    fn test_constants() {
362        assert_eq!(INTENT_DIR, ".intent-engine");
363        assert_eq!(DB_FILE, "project.db");
364    }
365
366    #[test]
367    fn test_project_context_debug() {
368        // Just verify that ProjectContext implements Debug
369        // We can't easily create one without side effects in a unit test
370        let _type_check = |ctx: ProjectContext| {
371            let _ = format!("{:?}", ctx);
372        };
373    }
374
375    #[test]
376    fn test_project_root_markers_list() {
377        // Verify that the markers list contains expected markers
378        assert!(PROJECT_ROOT_MARKERS.contains(&".git"));
379        assert!(PROJECT_ROOT_MARKERS.contains(&"Cargo.toml"));
380        assert!(PROJECT_ROOT_MARKERS.contains(&"package.json"));
381    }
382
383    #[test]
384    fn test_project_root_markers_priority() {
385        // Verify that .git has highest priority (comes first)
386        assert_eq!(PROJECT_ROOT_MARKERS[0], ".git");
387    }
388
389    /// Test infer_project_root in an isolated environment
390    /// Note: This test creates a temporary directory structure but doesn't change CWD
391    #[test]
392    fn test_infer_project_root_with_git() {
393        // This test is limited because we can't easily change CWD in unit tests
394        // The actual behavior is tested in integration tests
395        // Here we just verify the marker list is correct
396        assert!(PROJECT_ROOT_MARKERS.contains(&".git"));
397    }
398
399    /// Test that markers list includes all major project types
400    #[test]
401    fn test_all_major_project_types_covered() {
402        let markers = PROJECT_ROOT_MARKERS;
403
404        // Git version control
405        assert!(markers.contains(&".git"));
406        assert!(markers.contains(&".hg"));
407
408        // Programming languages
409        assert!(markers.contains(&"Cargo.toml")); // Rust
410        assert!(markers.contains(&"package.json")); // Node.js
411        assert!(markers.contains(&"pyproject.toml")); // Python
412        assert!(markers.contains(&"go.mod")); // Go
413        assert!(markers.contains(&"pom.xml")); // Java (Maven)
414        assert!(markers.contains(&"build.gradle")); // Java/Kotlin (Gradle)
415    }
416
417    /// Test DirectoryTraversalInfo structure
418    #[test]
419    fn test_directory_traversal_info_creation() {
420        let info = DirectoryTraversalInfo {
421            path: "/test/path".to_string(),
422            has_intent_engine: true,
423            is_selected: false,
424        };
425
426        assert_eq!(info.path, "/test/path");
427        assert!(info.has_intent_engine);
428        assert!(!info.is_selected);
429    }
430
431    /// Test DirectoryTraversalInfo Clone trait
432    #[test]
433    fn test_directory_traversal_info_clone() {
434        let info = DirectoryTraversalInfo {
435            path: "/test/path".to_string(),
436            has_intent_engine: true,
437            is_selected: true,
438        };
439
440        let cloned = info.clone();
441        assert_eq!(cloned.path, info.path);
442        assert_eq!(cloned.has_intent_engine, info.has_intent_engine);
443        assert_eq!(cloned.is_selected, info.is_selected);
444    }
445
446    /// Test DirectoryTraversalInfo Debug trait
447    #[test]
448    fn test_directory_traversal_info_debug() {
449        let info = DirectoryTraversalInfo {
450            path: "/test/path".to_string(),
451            has_intent_engine: false,
452            is_selected: true,
453        };
454
455        let debug_str = format!("{:?}", info);
456        assert!(debug_str.contains("DirectoryTraversalInfo"));
457        assert!(debug_str.contains("/test/path"));
458    }
459
460    /// Test DirectoryTraversalInfo serialization
461    #[test]
462    fn test_directory_traversal_info_serialization() {
463        let info = DirectoryTraversalInfo {
464            path: "/test/path".to_string(),
465            has_intent_engine: true,
466            is_selected: false,
467        };
468
469        let json = serde_json::to_string(&info).unwrap();
470        assert!(json.contains("path"));
471        assert!(json.contains("has_intent_engine"));
472        assert!(json.contains("is_selected"));
473        assert!(json.contains("/test/path"));
474    }
475
476    /// Test DirectoryTraversalInfo deserialization
477    #[test]
478    fn test_directory_traversal_info_deserialization() {
479        let json = r#"{"path":"/test/path","has_intent_engine":true,"is_selected":false}"#;
480        let info: DirectoryTraversalInfo = serde_json::from_str(json).unwrap();
481
482        assert_eq!(info.path, "/test/path");
483        assert!(info.has_intent_engine);
484        assert!(!info.is_selected);
485    }
486
487    /// Test DatabasePathInfo structure creation
488    #[test]
489    fn test_database_path_info_creation() {
490        let info = DatabasePathInfo {
491            current_working_directory: "/test/cwd".to_string(),
492            env_var_set: false,
493            env_var_path: None,
494            env_var_valid: None,
495            directories_checked: vec![],
496            home_directory: Some("/home/user".to_string()),
497            home_has_intent_engine: false,
498            final_database_path: Some("/test/db.db".to_string()),
499            resolution_method: Some("Test Method".to_string()),
500        };
501
502        assert_eq!(info.current_working_directory, "/test/cwd");
503        assert!(!info.env_var_set);
504        assert_eq!(info.env_var_path, None);
505        assert_eq!(info.home_directory, Some("/home/user".to_string()));
506        assert!(!info.home_has_intent_engine);
507        assert_eq!(info.final_database_path, Some("/test/db.db".to_string()));
508        assert_eq!(info.resolution_method, Some("Test Method".to_string()));
509    }
510
511    /// Test DatabasePathInfo with environment variable set
512    #[test]
513    fn test_database_path_info_with_env_var() {
514        let info = DatabasePathInfo {
515            current_working_directory: "/test/cwd".to_string(),
516            env_var_set: true,
517            env_var_path: Some("/env/path".to_string()),
518            env_var_valid: Some(true),
519            directories_checked: vec![],
520            home_directory: Some("/home/user".to_string()),
521            home_has_intent_engine: false,
522            final_database_path: Some("/env/path/.intent-engine/project.db".to_string()),
523            resolution_method: Some("Environment Variable".to_string()),
524        };
525
526        assert!(info.env_var_set);
527        assert_eq!(info.env_var_path, Some("/env/path".to_string()));
528        assert_eq!(info.env_var_valid, Some(true));
529        assert_eq!(
530            info.resolution_method,
531            Some("Environment Variable".to_string())
532        );
533    }
534
535    /// Test DatabasePathInfo with directories checked
536    #[test]
537    fn test_database_path_info_with_directories() {
538        let dirs = vec![
539            DirectoryTraversalInfo {
540                path: "/test/path1".to_string(),
541                has_intent_engine: false,
542                is_selected: false,
543            },
544            DirectoryTraversalInfo {
545                path: "/test/path2".to_string(),
546                has_intent_engine: true,
547                is_selected: true,
548            },
549        ];
550
551        let info = DatabasePathInfo {
552            current_working_directory: "/test/path1".to_string(),
553            env_var_set: false,
554            env_var_path: None,
555            env_var_valid: None,
556            directories_checked: dirs.clone(),
557            home_directory: Some("/home/user".to_string()),
558            home_has_intent_engine: false,
559            final_database_path: Some("/test/path2/.intent-engine/project.db".to_string()),
560            resolution_method: Some("Upward Directory Traversal".to_string()),
561        };
562
563        assert_eq!(info.directories_checked.len(), 2);
564        assert!(!info.directories_checked[0].has_intent_engine);
565        assert!(info.directories_checked[1].has_intent_engine);
566        assert!(info.directories_checked[1].is_selected);
567    }
568
569    /// Test DatabasePathInfo Debug trait
570    #[test]
571    fn test_database_path_info_debug() {
572        let info = DatabasePathInfo {
573            current_working_directory: "/test/cwd".to_string(),
574            env_var_set: false,
575            env_var_path: None,
576            env_var_valid: None,
577            directories_checked: vec![],
578            home_directory: Some("/home/user".to_string()),
579            home_has_intent_engine: false,
580            final_database_path: Some("/test/db.db".to_string()),
581            resolution_method: Some("Test".to_string()),
582        };
583
584        let debug_str = format!("{:?}", info);
585        assert!(debug_str.contains("DatabasePathInfo"));
586        assert!(debug_str.contains("/test/cwd"));
587    }
588
589    /// Test DatabasePathInfo serialization
590    #[test]
591    fn test_database_path_info_serialization() {
592        let info = DatabasePathInfo {
593            current_working_directory: "/test/cwd".to_string(),
594            env_var_set: true,
595            env_var_path: Some("/env/path".to_string()),
596            env_var_valid: Some(true),
597            directories_checked: vec![],
598            home_directory: Some("/home/user".to_string()),
599            home_has_intent_engine: false,
600            final_database_path: Some("/test/db.db".to_string()),
601            resolution_method: Some("Test Method".to_string()),
602        };
603
604        let json = serde_json::to_string(&info).unwrap();
605        assert!(json.contains("current_working_directory"));
606        assert!(json.contains("env_var_set"));
607        assert!(json.contains("env_var_path"));
608        assert!(json.contains("final_database_path"));
609        assert!(json.contains("/test/cwd"));
610        assert!(json.contains("/env/path"));
611    }
612
613    /// Test DatabasePathInfo deserialization
614    #[test]
615    fn test_database_path_info_deserialization() {
616        let json = r#"{
617            "current_working_directory": "/test/cwd",
618            "env_var_set": true,
619            "env_var_path": "/env/path",
620            "env_var_valid": true,
621            "directories_checked": [],
622            "home_directory": "/home/user",
623            "home_has_intent_engine": false,
624            "final_database_path": "/test/db.db",
625            "resolution_method": "Test Method"
626        }"#;
627
628        let info: DatabasePathInfo = serde_json::from_str(json).unwrap();
629        assert_eq!(info.current_working_directory, "/test/cwd");
630        assert!(info.env_var_set);
631        assert_eq!(info.env_var_path, Some("/env/path".to_string()));
632        assert_eq!(info.env_var_valid, Some(true));
633        assert_eq!(info.home_directory, Some("/home/user".to_string()));
634        assert_eq!(info.final_database_path, Some("/test/db.db".to_string()));
635        assert_eq!(info.resolution_method, Some("Test Method".to_string()));
636    }
637
638    /// Test DatabasePathInfo with complete directory traversal data
639    #[test]
640    fn test_database_path_info_complete_structure() {
641        let dirs = vec![
642            DirectoryTraversalInfo {
643                path: "/home/user/project/src".to_string(),
644                has_intent_engine: false,
645                is_selected: false,
646            },
647            DirectoryTraversalInfo {
648                path: "/home/user/project".to_string(),
649                has_intent_engine: true,
650                is_selected: true,
651            },
652            DirectoryTraversalInfo {
653                path: "/home/user".to_string(),
654                has_intent_engine: false,
655                is_selected: false,
656            },
657        ];
658
659        let info = DatabasePathInfo {
660            current_working_directory: "/home/user/project/src".to_string(),
661            env_var_set: false,
662            env_var_path: None,
663            env_var_valid: None,
664            directories_checked: dirs,
665            home_directory: Some("/home/user".to_string()),
666            home_has_intent_engine: false,
667            final_database_path: Some("/home/user/project/.intent-engine/project.db".to_string()),
668            resolution_method: Some("Upward Directory Traversal".to_string()),
669        };
670
671        // Verify the complete structure
672        assert_eq!(info.directories_checked.len(), 3);
673        assert_eq!(info.directories_checked[0].path, "/home/user/project/src");
674        assert_eq!(info.directories_checked[1].path, "/home/user/project");
675        assert_eq!(info.directories_checked[2].path, "/home/user");
676
677        // Only the second directory should be selected
678        assert!(!info.directories_checked[0].is_selected);
679        assert!(info.directories_checked[1].is_selected);
680        assert!(!info.directories_checked[2].is_selected);
681
682        // Only the second directory has intent-engine
683        assert!(!info.directories_checked[0].has_intent_engine);
684        assert!(info.directories_checked[1].has_intent_engine);
685        assert!(!info.directories_checked[2].has_intent_engine);
686    }
687
688    /// Test get_database_path_info returns valid structure
689    #[test]
690    fn test_get_database_path_info_structure() {
691        let info = ProjectContext::get_database_path_info();
692
693        // Verify basic structure is populated
694        assert!(!info.current_working_directory.is_empty());
695
696        // Verify that we got some result for directories checked or home directory
697        let has_data = !info.directories_checked.is_empty()
698            || info.home_directory.is_some()
699            || info.env_var_set;
700
701        assert!(
702            has_data,
703            "get_database_path_info should return some directory information"
704        );
705    }
706
707    /// Test get_database_path_info with actual filesystem
708    #[test]
709    fn test_get_database_path_info_checks_current_dir() {
710        let info = ProjectContext::get_database_path_info();
711
712        // The current working directory should be set
713        assert!(!info.current_working_directory.is_empty());
714
715        // At minimum, it should check the current directory (unless env var is set and valid)
716        if !info.env_var_set || info.env_var_valid != Some(true) {
717            assert!(
718                !info.directories_checked.is_empty(),
719                "Should check at least the current directory"
720            );
721        }
722    }
723
724    /// Test get_database_path_info includes current directory in checked list
725    #[test]
726    fn test_get_database_path_info_includes_cwd() {
727        let info = ProjectContext::get_database_path_info();
728
729        // If env var is not set or invalid, should check directories
730        if !info.env_var_set || info.env_var_valid != Some(true) {
731            assert!(!info.directories_checked.is_empty());
732
733            // First checked directory should start with the CWD or be a parent
734            let cwd = &info.current_working_directory;
735            let first_checked = &info.directories_checked[0].path;
736
737            assert!(
738                cwd.starts_with(first_checked) || first_checked.starts_with(cwd),
739                "First checked directory should be related to CWD"
740            );
741        }
742    }
743
744    /// Test get_database_path_info resolution method is set when database found
745    #[test]
746    fn test_get_database_path_info_resolution_method() {
747        let info = ProjectContext::get_database_path_info();
748
749        // If a database path was found, resolution method should be set
750        if info.final_database_path.is_some() {
751            assert!(
752                info.resolution_method.is_some(),
753                "Resolution method should be set when database path is found"
754            );
755
756            let method = info.resolution_method.unwrap();
757            assert!(
758                method.contains("Environment Variable")
759                    || method.contains("Upward Directory Traversal")
760                    || method.contains("Home Directory"),
761                "Resolution method should be one of the known strategies"
762            );
763        }
764    }
765
766    /// Test get_database_path_info marks selected directory correctly
767    #[test]
768    fn test_get_database_path_info_selected_directory() {
769        let info = ProjectContext::get_database_path_info();
770
771        // If env var not used and directories were checked
772        if (!info.env_var_set || info.env_var_valid != Some(true))
773            && !info.directories_checked.is_empty()
774            && info.final_database_path.is_some()
775        {
776            // Exactly one directory should be marked as selected
777            let selected_count = info
778                .directories_checked
779                .iter()
780                .filter(|d| d.is_selected)
781                .count();
782
783            assert!(
784                selected_count <= 1,
785                "At most one directory should be marked as selected"
786            );
787
788            // If one is selected, it should have intent_engine
789            if let Some(selected) = info.directories_checked.iter().find(|d| d.is_selected) {
790                assert!(
791                    selected.has_intent_engine,
792                    "Selected directory should have .intent-engine"
793                );
794            }
795        }
796    }
797
798    /// Test DatabasePathInfo with no database found scenario
799    #[test]
800    fn test_database_path_info_no_database_found() {
801        let info = DatabasePathInfo {
802            current_working_directory: "/test/path".to_string(),
803            env_var_set: false,
804            env_var_path: None,
805            env_var_valid: None,
806            directories_checked: vec![
807                DirectoryTraversalInfo {
808                    path: "/test/path".to_string(),
809                    has_intent_engine: false,
810                    is_selected: false,
811                },
812                DirectoryTraversalInfo {
813                    path: "/test".to_string(),
814                    has_intent_engine: false,
815                    is_selected: false,
816                },
817            ],
818            home_directory: Some("/home/user".to_string()),
819            home_has_intent_engine: false,
820            final_database_path: None,
821            resolution_method: None,
822        };
823
824        assert!(info.final_database_path.is_none());
825        assert!(info.resolution_method.is_none());
826        assert_eq!(info.directories_checked.len(), 2);
827        assert!(!info.home_has_intent_engine);
828    }
829
830    /// Test DatabasePathInfo with env var set but invalid
831    #[test]
832    fn test_database_path_info_env_var_invalid() {
833        let info = DatabasePathInfo {
834            current_working_directory: "/test/cwd".to_string(),
835            env_var_set: true,
836            env_var_path: Some("/invalid/path".to_string()),
837            env_var_valid: Some(false),
838            directories_checked: vec![DirectoryTraversalInfo {
839                path: "/test/cwd".to_string(),
840                has_intent_engine: true,
841                is_selected: true,
842            }],
843            home_directory: Some("/home/user".to_string()),
844            home_has_intent_engine: false,
845            final_database_path: Some("/test/cwd/.intent-engine/project.db".to_string()),
846            resolution_method: Some("Upward Directory Traversal".to_string()),
847        };
848
849        assert!(info.env_var_set);
850        assert_eq!(info.env_var_valid, Some(false));
851        assert!(info.final_database_path.is_some());
852        // Should fallback to directory traversal when env var is invalid
853        assert!(info.resolution_method.unwrap().contains("Upward Directory"));
854    }
855
856    /// Test DatabasePathInfo with home directory fallback
857    #[test]
858    fn test_database_path_info_home_directory_used() {
859        let info = DatabasePathInfo {
860            current_working_directory: "/tmp/work".to_string(),
861            env_var_set: false,
862            env_var_path: None,
863            env_var_valid: None,
864            directories_checked: vec![
865                DirectoryTraversalInfo {
866                    path: "/tmp/work".to_string(),
867                    has_intent_engine: false,
868                    is_selected: false,
869                },
870                DirectoryTraversalInfo {
871                    path: "/tmp".to_string(),
872                    has_intent_engine: false,
873                    is_selected: false,
874                },
875            ],
876            home_directory: Some("/home/user".to_string()),
877            home_has_intent_engine: true,
878            final_database_path: Some("/home/user/.intent-engine/project.db".to_string()),
879            resolution_method: Some("Home Directory Fallback".to_string()),
880        };
881
882        assert!(info.home_has_intent_engine);
883        assert_eq!(
884            info.final_database_path,
885            Some("/home/user/.intent-engine/project.db".to_string())
886        );
887        assert_eq!(
888            info.resolution_method,
889            Some("Home Directory Fallback".to_string())
890        );
891    }
892
893    /// Test DatabasePathInfo serialization round-trip with all fields
894    #[test]
895    fn test_database_path_info_full_roundtrip() {
896        let original = DatabasePathInfo {
897            current_working_directory: "/test/cwd".to_string(),
898            env_var_set: true,
899            env_var_path: Some("/env/path".to_string()),
900            env_var_valid: Some(false),
901            directories_checked: vec![
902                DirectoryTraversalInfo {
903                    path: "/test/cwd".to_string(),
904                    has_intent_engine: false,
905                    is_selected: false,
906                },
907                DirectoryTraversalInfo {
908                    path: "/test".to_string(),
909                    has_intent_engine: true,
910                    is_selected: true,
911                },
912            ],
913            home_directory: Some("/home/user".to_string()),
914            home_has_intent_engine: false,
915            final_database_path: Some("/test/.intent-engine/project.db".to_string()),
916            resolution_method: Some("Upward Directory Traversal".to_string()),
917        };
918
919        // Serialize
920        let json = serde_json::to_string(&original).unwrap();
921
922        // Deserialize
923        let deserialized: DatabasePathInfo = serde_json::from_str(&json).unwrap();
924
925        // Verify all fields match
926        assert_eq!(
927            deserialized.current_working_directory,
928            original.current_working_directory
929        );
930        assert_eq!(deserialized.env_var_set, original.env_var_set);
931        assert_eq!(deserialized.env_var_path, original.env_var_path);
932        assert_eq!(deserialized.env_var_valid, original.env_var_valid);
933        assert_eq!(
934            deserialized.directories_checked.len(),
935            original.directories_checked.len()
936        );
937        assert_eq!(deserialized.home_directory, original.home_directory);
938        assert_eq!(
939            deserialized.home_has_intent_engine,
940            original.home_has_intent_engine
941        );
942        assert_eq!(
943            deserialized.final_database_path,
944            original.final_database_path
945        );
946        assert_eq!(deserialized.resolution_method, original.resolution_method);
947    }
948
949    /// Test DirectoryTraversalInfo with all boolean combinations
950    #[test]
951    fn test_directory_traversal_info_all_combinations() {
952        // Test all 4 combinations of boolean flags
953        let combinations = [(false, false), (false, true), (true, false), (true, true)];
954
955        for (has_ie, is_sel) in combinations.iter() {
956            let info = DirectoryTraversalInfo {
957                path: format!("/test/path/{}_{}", has_ie, is_sel),
958                has_intent_engine: *has_ie,
959                is_selected: *is_sel,
960            };
961
962            assert_eq!(info.has_intent_engine, *has_ie);
963            assert_eq!(info.is_selected, *is_sel);
964        }
965    }
966
967    /// Test DirectoryTraversalInfo serialization preserves exact values
968    #[test]
969    fn test_directory_traversal_info_exact_serialization() {
970        let info = DirectoryTraversalInfo {
971            path: "/exact/path/with/special-chars_123".to_string(),
972            has_intent_engine: true,
973            is_selected: false,
974        };
975
976        let json = serde_json::to_string(&info).unwrap();
977        let deserialized: DirectoryTraversalInfo = serde_json::from_str(&json).unwrap();
978
979        assert_eq!(info.path, deserialized.path);
980        assert_eq!(info.has_intent_engine, deserialized.has_intent_engine);
981        assert_eq!(info.is_selected, deserialized.is_selected);
982    }
983
984    /// Test DatabasePathInfo with None values for optional fields
985    #[test]
986    fn test_database_path_info_all_none() {
987        let info = DatabasePathInfo {
988            current_working_directory: "/test".to_string(),
989            env_var_set: false,
990            env_var_path: None,
991            env_var_valid: None,
992            directories_checked: vec![],
993            home_directory: None,
994            home_has_intent_engine: false,
995            final_database_path: None,
996            resolution_method: None,
997        };
998
999        assert!(!info.env_var_set);
1000        assert!(info.env_var_path.is_none());
1001        assert!(info.env_var_valid.is_none());
1002        assert!(info.directories_checked.is_empty());
1003        assert!(info.home_directory.is_none());
1004        assert!(info.final_database_path.is_none());
1005        assert!(info.resolution_method.is_none());
1006    }
1007
1008    /// Test DatabasePathInfo with Some values for all optional fields
1009    #[test]
1010    fn test_database_path_info_all_some() {
1011        let info = DatabasePathInfo {
1012            current_working_directory: "/test".to_string(),
1013            env_var_set: true,
1014            env_var_path: Some("/env".to_string()),
1015            env_var_valid: Some(true),
1016            directories_checked: vec![DirectoryTraversalInfo {
1017                path: "/test".to_string(),
1018                has_intent_engine: true,
1019                is_selected: true,
1020            }],
1021            home_directory: Some("/home".to_string()),
1022            home_has_intent_engine: true,
1023            final_database_path: Some("/test/.intent-engine/project.db".to_string()),
1024            resolution_method: Some("Test Method".to_string()),
1025        };
1026
1027        assert!(info.env_var_set);
1028        assert!(info.env_var_path.is_some());
1029        assert!(info.env_var_valid.is_some());
1030        assert!(!info.directories_checked.is_empty());
1031        assert!(info.home_directory.is_some());
1032        assert!(info.final_database_path.is_some());
1033        assert!(info.resolution_method.is_some());
1034    }
1035
1036    /// Test get_database_path_info home directory field is set
1037    #[test]
1038    fn test_get_database_path_info_home_directory() {
1039        let info = ProjectContext::get_database_path_info();
1040
1041        // Home directory should typically be set (unless in very restricted environment)
1042        // This tests that the home directory detection logic runs
1043        if std::env::var("HOME").is_ok() {
1044            assert!(
1045                info.home_directory.is_some(),
1046                "HOME env var is set, so home_directory should be Some"
1047            );
1048        }
1049    }
1050
1051    /// Test get_database_path_info doesn't panic with edge cases
1052    #[test]
1053    fn test_get_database_path_info_no_panic() {
1054        // This test ensures the function handles edge cases gracefully
1055        // Even in unusual environments, it should return valid data
1056        let info = ProjectContext::get_database_path_info();
1057
1058        // Basic sanity checks - should always have these
1059        assert!(!info.current_working_directory.is_empty());
1060
1061        // If final_database_path is None, that's okay - it means no database was found
1062        // The function should still provide diagnostic information
1063        if info.final_database_path.is_none() {
1064            // Should still have checked some directories or reported env var status
1065            let has_diagnostic_info = !info.directories_checked.is_empty()
1066                || info.env_var_set
1067                || info.home_directory.is_some();
1068
1069            assert!(
1070                has_diagnostic_info,
1071                "Even without finding a database, should provide diagnostic information"
1072            );
1073        }
1074    }
1075
1076    /// Test get_database_path_info with multiple .intent-engine directories
1077    #[test]
1078    fn test_get_database_path_info_prefers_first_match() {
1079        let info = ProjectContext::get_database_path_info();
1080
1081        // If database was found via directory traversal and multiple directories were checked
1082        if info
1083            .resolution_method
1084            .as_ref()
1085            .is_some_and(|m| m.contains("Upward Directory"))
1086            && info.directories_checked.len() > 1
1087        {
1088            // Find all directories with .intent-engine
1089            let with_ie: Vec<_> = info
1090                .directories_checked
1091                .iter()
1092                .filter(|d| d.has_intent_engine)
1093                .collect();
1094
1095            if with_ie.len() > 1 {
1096                // Only the first one found (closest to CWD) should be selected
1097                let selected: Vec<_> = with_ie.iter().filter(|d| d.is_selected).collect();
1098                assert!(
1099                    selected.len() <= 1,
1100                    "Only the first .intent-engine found should be selected"
1101                );
1102            }
1103        }
1104    }
1105
1106    /// Test DatabasePathInfo deserialization with missing optional fields
1107    #[test]
1108    fn test_database_path_info_partial_deserialization() {
1109        // Test with minimal required fields
1110        let json = r#"{
1111            "current_working_directory": "/test",
1112            "env_var_set": false,
1113            "env_var_path": null,
1114            "env_var_valid": null,
1115            "directories_checked": [],
1116            "home_directory": null,
1117            "home_has_intent_engine": false,
1118            "final_database_path": null,
1119            "resolution_method": null
1120        }"#;
1121
1122        let info: DatabasePathInfo = serde_json::from_str(json).unwrap();
1123        assert_eq!(info.current_working_directory, "/test");
1124        assert!(!info.env_var_set);
1125    }
1126
1127    /// Test DatabasePathInfo JSON format matches expected schema
1128    #[test]
1129    fn test_database_path_info_json_schema() {
1130        let info = DatabasePathInfo {
1131            current_working_directory: "/test".to_string(),
1132            env_var_set: true,
1133            env_var_path: Some("/env".to_string()),
1134            env_var_valid: Some(true),
1135            directories_checked: vec![],
1136            home_directory: Some("/home".to_string()),
1137            home_has_intent_engine: false,
1138            final_database_path: Some("/db".to_string()),
1139            resolution_method: Some("Test".to_string()),
1140        };
1141
1142        let json_value: serde_json::Value = serde_json::to_value(&info).unwrap();
1143
1144        // Verify all expected fields are present
1145        assert!(json_value.get("current_working_directory").is_some());
1146        assert!(json_value.get("env_var_set").is_some());
1147        assert!(json_value.get("env_var_path").is_some());
1148        assert!(json_value.get("env_var_valid").is_some());
1149        assert!(json_value.get("directories_checked").is_some());
1150        assert!(json_value.get("home_directory").is_some());
1151        assert!(json_value.get("home_has_intent_engine").is_some());
1152        assert!(json_value.get("final_database_path").is_some());
1153        assert!(json_value.get("resolution_method").is_some());
1154    }
1155
1156    /// Test DirectoryTraversalInfo with empty path
1157    #[test]
1158    fn test_directory_traversal_info_empty_path() {
1159        let info = DirectoryTraversalInfo {
1160            path: "".to_string(),
1161            has_intent_engine: false,
1162            is_selected: false,
1163        };
1164
1165        assert_eq!(info.path, "");
1166        let json = serde_json::to_string(&info).unwrap();
1167        let deserialized: DirectoryTraversalInfo = serde_json::from_str(&json).unwrap();
1168        assert_eq!(deserialized.path, "");
1169    }
1170
1171    /// Test DirectoryTraversalInfo with unicode path
1172    #[test]
1173    fn test_directory_traversal_info_unicode_path() {
1174        let info = DirectoryTraversalInfo {
1175            path: "/test/路径/مسار/путь".to_string(),
1176            has_intent_engine: true,
1177            is_selected: false,
1178        };
1179
1180        let json = serde_json::to_string(&info).unwrap();
1181        let deserialized: DirectoryTraversalInfo = serde_json::from_str(&json).unwrap();
1182        assert_eq!(deserialized.path, "/test/路径/مسار/путь");
1183    }
1184
1185    /// Test DatabasePathInfo with very long paths
1186    #[test]
1187    fn test_database_path_info_long_paths() {
1188        let long_path = "/".to_owned() + &"very_long_directory_name/".repeat(50);
1189        let info = DatabasePathInfo {
1190            current_working_directory: long_path.clone(),
1191            env_var_set: false,
1192            env_var_path: None,
1193            env_var_valid: None,
1194            directories_checked: vec![],
1195            home_directory: Some(long_path.clone()),
1196            home_has_intent_engine: false,
1197            final_database_path: Some(long_path.clone()),
1198            resolution_method: Some("Test".to_string()),
1199        };
1200
1201        let json = serde_json::to_string(&info).unwrap();
1202        let deserialized: DatabasePathInfo = serde_json::from_str(&json).unwrap();
1203        assert_eq!(deserialized.current_working_directory, long_path);
1204    }
1205
1206    /// Test get_database_path_info env var handling
1207    #[test]
1208    fn test_get_database_path_info_env_var_detection() {
1209        let info = ProjectContext::get_database_path_info();
1210
1211        // Check if INTENT_ENGINE_PROJECT_DIR is set
1212        if std::env::var("INTENT_ENGINE_PROJECT_DIR").is_ok() {
1213            assert!(
1214                info.env_var_set,
1215                "env_var_set should be true when INTENT_ENGINE_PROJECT_DIR is set"
1216            );
1217            assert!(
1218                info.env_var_path.is_some(),
1219                "env_var_path should contain the path when env var is set"
1220            );
1221            assert!(
1222                info.env_var_valid.is_some(),
1223                "env_var_valid should be set when env var is present"
1224            );
1225        } else {
1226            assert!(
1227                !info.env_var_set,
1228                "env_var_set should be false when INTENT_ENGINE_PROJECT_DIR is not set"
1229            );
1230            assert!(
1231                info.env_var_path.is_none(),
1232                "env_var_path should be None when env var is not set"
1233            );
1234            assert!(
1235                info.env_var_valid.is_none(),
1236                "env_var_valid should be None when env var is not set"
1237            );
1238        }
1239    }
1240}