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