directory-indexer 0.0.10

AI-powered directory indexing with semantic search for MCP servers
Documentation
use log::{info, warn};
use std::path::PathBuf;
use std::time::Duration;
use tokio::time::interval;

use crate::error::Result;

pub struct FileMonitor {
    watched_directories: Vec<PathBuf>,
    check_interval: Duration,
}

impl FileMonitor {
    pub fn new(watched_directories: Vec<PathBuf>, check_interval: Duration) -> Self {
        Self {
            watched_directories,
            check_interval,
        }
    }

    pub async fn start_monitoring<F>(&self, _on_change: F) -> Result<()>
    where
        F: Fn(FileChangeEvent) + Send + Sync + 'static,
    {
        let dir_count = self.watched_directories.len();
        info!("Starting file monitoring for {dir_count} directories");

        // TODO: Implement actual file watching
        // This could use platform-specific file watching APIs:
        // - inotify on Linux
        // - FSEvents on macOS
        // - ReadDirectoryChangesW on Windows
        //
        // For now, we'll implement a simple polling-based approach

        let mut interval = interval(self.check_interval);

        loop {
            interval.tick().await;

            // TODO: Check for file changes and call on_change callback
            // This would involve:
            // 1. Comparing current file states with stored states
            // 2. Detecting new, modified, and deleted files
            // 3. Calling the callback with appropriate events

            warn!("File monitoring not yet implemented - this is a placeholder");

            // For now, just log that we're checking
            for dir in &self.watched_directories {
                info!("Checking directory for changes: {dir:?}");
            }
        }
    }

    pub fn add_directory(&mut self, path: PathBuf) {
        if !self.watched_directories.contains(&path) {
            self.watched_directories.push(path);
        }
    }

    pub fn remove_directory(&mut self, path: &PathBuf) {
        self.watched_directories.retain(|p| p != path);
    }
}

#[derive(Debug, Clone)]
pub enum FileChangeEvent {
    Created(PathBuf),
    Modified(PathBuf),
    Deleted(PathBuf),
}

impl FileChangeEvent {
    pub fn path(&self) -> &PathBuf {
        match self {
            FileChangeEvent::Created(path) => path,
            FileChangeEvent::Modified(path) => path,
            FileChangeEvent::Deleted(path) => path,
        }
    }

    pub fn event_type(&self) -> &'static str {
        match self {
            FileChangeEvent::Created(_) => "created",
            FileChangeEvent::Modified(_) => "modified",
            FileChangeEvent::Deleted(_) => "deleted",
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::time::Duration;

    #[test]
    fn test_file_monitor_creation() {
        let directories = vec![PathBuf::from("/test/dir1"), PathBuf::from("/test/dir2")];
        let interval = Duration::from_secs(5);

        let monitor = FileMonitor::new(directories.clone(), interval);

        assert_eq!(monitor.watched_directories, directories);
        assert_eq!(monitor.check_interval, interval);
    }

    #[test]
    fn test_file_monitor_add_directory() {
        let initial_dirs = vec![PathBuf::from("/test/dir1")];
        let mut monitor = FileMonitor::new(initial_dirs, Duration::from_secs(1));

        let new_dir = PathBuf::from("/test/dir2");
        monitor.add_directory(new_dir.clone());

        assert_eq!(monitor.watched_directories.len(), 2);
        assert!(monitor.watched_directories.contains(&new_dir));
    }

    #[test]
    fn test_file_monitor_add_duplicate_directory() {
        let dir = PathBuf::from("/test/dir");
        let initial_dirs = vec![dir.clone()];
        let mut monitor = FileMonitor::new(initial_dirs, Duration::from_secs(1));

        // Adding the same directory should not create duplicates
        monitor.add_directory(dir.clone());

        assert_eq!(monitor.watched_directories.len(), 1);
        assert_eq!(monitor.watched_directories[0], dir);
    }

    #[test]
    fn test_file_monitor_remove_directory() {
        let dir1 = PathBuf::from("/test/dir1");
        let dir2 = PathBuf::from("/test/dir2");
        let initial_dirs = vec![dir1.clone(), dir2.clone()];
        let mut monitor = FileMonitor::new(initial_dirs, Duration::from_secs(1));

        monitor.remove_directory(&dir1);

        assert_eq!(monitor.watched_directories.len(), 1);
        assert!(!monitor.watched_directories.contains(&dir1));
        assert!(monitor.watched_directories.contains(&dir2));
    }

    #[test]
    fn test_file_monitor_remove_nonexistent_directory() {
        let dir1 = PathBuf::from("/test/dir1");
        let initial_dirs = vec![dir1.clone()];
        let mut monitor = FileMonitor::new(initial_dirs, Duration::from_secs(1));

        let nonexistent = PathBuf::from("/test/nonexistent");
        monitor.remove_directory(&nonexistent);

        // Should still have the original directory
        assert_eq!(monitor.watched_directories.len(), 1);
        assert!(monitor.watched_directories.contains(&dir1));
    }

    #[test]
    fn test_file_change_event_created() {
        let path = PathBuf::from("/test/file.txt");
        let event = FileChangeEvent::Created(path.clone());

        assert_eq!(event.path(), &path);
        assert_eq!(event.event_type(), "created");
    }

    #[test]
    fn test_file_change_event_modified() {
        let path = PathBuf::from("/test/file.txt");
        let event = FileChangeEvent::Modified(path.clone());

        assert_eq!(event.path(), &path);
        assert_eq!(event.event_type(), "modified");
    }

    #[test]
    fn test_file_change_event_deleted() {
        let path = PathBuf::from("/test/file.txt");
        let event = FileChangeEvent::Deleted(path.clone());

        assert_eq!(event.path(), &path);
        assert_eq!(event.event_type(), "deleted");
    }

    #[test]
    fn test_file_change_event_clone() {
        let path = PathBuf::from("/test/file.txt");
        let event = FileChangeEvent::Created(path.clone());
        let cloned = event.clone();

        assert_eq!(event.path(), cloned.path());
        assert_eq!(event.event_type(), cloned.event_type());
    }

    #[test]
    fn test_file_change_event_debug() {
        let path = PathBuf::from("/test/file.txt");
        let event = FileChangeEvent::Modified(path);

        let debug_output = format!("{event:?}");
        assert!(debug_output.contains("Modified"));
        assert!(debug_output.contains("file.txt"));
    }

    #[test]
    fn test_empty_monitor() {
        let monitor = FileMonitor::new(vec![], Duration::from_millis(100));

        assert!(monitor.watched_directories.is_empty());
        assert_eq!(monitor.check_interval, Duration::from_millis(100));
    }

    #[test]
    fn test_monitor_configuration_variations() {
        // Test different interval configurations
        let configs = vec![
            Duration::from_millis(100),
            Duration::from_secs(1),
            Duration::from_secs(60),
        ];

        for interval in configs {
            let monitor = FileMonitor::new(vec![], interval);
            assert_eq!(monitor.check_interval, interval);
        }
    }

    #[test]
    fn test_path_operations() {
        let mut monitor = FileMonitor::new(vec![], Duration::from_secs(1));

        let paths = vec![
            PathBuf::from("/home/user/documents"),
            PathBuf::from("/var/log"),
            PathBuf::from("/tmp/test"),
        ];

        // Add multiple paths
        for path in &paths {
            monitor.add_directory(path.clone());
        }

        assert_eq!(monitor.watched_directories.len(), 3);

        // Remove one path
        monitor.remove_directory(&paths[1]);
        assert_eq!(monitor.watched_directories.len(), 2);
        assert!(!monitor.watched_directories.contains(&paths[1]));

        // Verify remaining paths
        assert!(monitor.watched_directories.contains(&paths[0]));
        assert!(monitor.watched_directories.contains(&paths[2]));
    }

    #[test]
    fn test_file_change_event_pattern_matching() {
        let path = PathBuf::from("/test/file.txt");

        let events = vec![
            FileChangeEvent::Created(path.clone()),
            FileChangeEvent::Modified(path.clone()),
            FileChangeEvent::Deleted(path.clone()),
        ];

        for event in events {
            match &event {
                FileChangeEvent::Created(p) => {
                    assert_eq!(p, &path);
                    assert_eq!(event.event_type(), "created");
                }
                FileChangeEvent::Modified(p) => {
                    assert_eq!(p, &path);
                    assert_eq!(event.event_type(), "modified");
                }
                FileChangeEvent::Deleted(p) => {
                    assert_eq!(p, &path);
                    assert_eq!(event.event_type(), "deleted");
                }
            }
        }
    }

    // Mock test for the monitoring functionality (since actual monitoring is not implemented)
    #[tokio::test]
    async fn test_monitoring_placeholder() {
        let monitor = FileMonitor::new(vec![PathBuf::from("/test")], Duration::from_millis(10));

        // Test that the monitoring function exists and can be called
        // We'll use a timeout to avoid infinite loop in the placeholder implementation
        let result = tokio::time::timeout(
            Duration::from_millis(50),
            monitor.start_monitoring(|_event| {
                // Mock callback - this won't be called in the placeholder implementation
            }),
        )
        .await;

        // Should timeout because the placeholder implementation runs forever
        assert!(result.is_err());
    }

    #[test]
    fn test_monitor_with_relative_paths() {
        let relative_paths = vec![
            PathBuf::from("./documents"),
            PathBuf::from("../parent"),
            PathBuf::from("subdir/nested"),
        ];

        let monitor = FileMonitor::new(relative_paths.clone(), Duration::from_secs(1));

        assert_eq!(monitor.watched_directories, relative_paths);
    }

    #[test]
    fn test_monitor_with_unicode_paths() {
        let unicode_paths = vec![
            PathBuf::from("/test/файл.txt"),
            PathBuf::from("/test/文件.txt"),
            PathBuf::from("/test/🦀.rs"),
        ];

        let monitor = FileMonitor::new(unicode_paths.clone(), Duration::from_secs(1));

        assert_eq!(monitor.watched_directories, unicode_paths);
    }
}