augent 0.6.6

Lean package manager for various AI coding platforms
//! Platform detection for finding AI coding platforms in a workspace

use std::path::Path;

use crate::error::{AugentError, Result};

use super::{Platform, loader::PlatformLoader};

/// Detect which platforms are present in the workspace
///
/// Returns platforms whose directory exists in the workspace (e.g. `.opencode`, `.cursor`).
/// Root-level agent files (AGENTS.md, CLAUDE.md, etc.) do not add any platform; only
/// platform directories are used so install targets only the platforms the user actually has.
pub fn detect_platforms(workspace_root: &Path) -> Result<Vec<Platform>> {
    if !workspace_root.exists() {
        return Err(AugentError::WorkspaceNotFound {
            path: workspace_root.display().to_string(),
        });
    }

    let loader = PlatformLoader::new(workspace_root);
    let platforms = loader.load()?;

    let detected: Vec<Platform> = platforms
        .into_iter()
        .filter(|p| workspace_root.join(&p.directory).exists())
        .collect();

    Ok(detected)
}

/// Detect platforms or return an error if none found
#[allow(dead_code)] // Used by tests
pub fn detect_platforms_or_error(workspace_root: &Path) -> Result<Vec<Platform>> {
    let platforms = detect_platforms(workspace_root)?;

    if platforms.is_empty() {
        return Err(AugentError::NoPlatformsDetected);
    }

    Ok(platforms)
}

/// Get a platform by ID, loading from platforms.jsonc if available
/// First tries to find exact ID match, then looks for alias matches
///
/// If workspace_root is provided, loads custom platforms from platforms.jsonc.
/// Otherwise, falls back to default platforms.
pub fn get_platform(id: &str, workspace_root: Option<&Path>) -> Option<Platform> {
    let platforms = if let Some(root) = workspace_root {
        PlatformLoader::new(root).load().ok().unwrap_or_default()
    } else {
        // Try to find workspace root from current directory
        if let Ok(current_dir) = std::env::current_dir() {
            PlatformLoader::new(&current_dir)
                .load()
                .ok()
                .unwrap_or_default()
        } else {
            super::default_platforms()
        }
    };

    // First try exact ID match
    if let Some(platform) = platforms.iter().find(|p| p.id == id) {
        return Some(platform.clone());
    }

    // Handle aliases: cursor-ai -> cursor
    let alias_id = match id {
        "cursor-ai" => "cursor",
        _ => return None,
    };

    platforms.iter().find(|p| p.id == alias_id).cloned()
}

/// Get multiple platforms by ID
/// Supports alias resolution - each ID is resolved through get_platform()
///
/// If workspace_root is provided, loads custom platforms from platforms.jsonc.
pub fn get_platforms(ids: &[String], workspace_root: Option<&Path>) -> Result<Vec<Platform>> {
    let mut platforms = Vec::new();

    for id in ids {
        match get_platform(id, workspace_root) {
            Some(p) => platforms.push(p),
            None => {
                return Err(AugentError::PlatformNotSupported {
                    platform: id.clone(),
                });
            }
        }
    }

    Ok(platforms)
}

/// Detect platforms, or use specified platforms if provided
#[allow(dead_code)] // Used by tests
pub fn resolve_platforms(workspace_root: &Path, specified: &[String]) -> Result<Vec<Platform>> {
    if specified.is_empty() {
        detect_platforms_or_error(workspace_root)
    } else {
        get_platforms(specified, Some(workspace_root))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::TempDir;

    #[test]
    fn test_detect_platforms_empty() {
        let temp = TempDir::new_in(crate::temp::temp_dir_base()).unwrap();
        let platforms = detect_platforms(temp.path()).unwrap();
        assert!(platforms.is_empty());
    }

    #[test]
    fn test_detect_platforms_claude() {
        let temp = TempDir::new_in(crate::temp::temp_dir_base()).unwrap();
        std::fs::create_dir(temp.path().join(".claude")).unwrap();

        let platforms = detect_platforms(temp.path()).unwrap();
        assert_eq!(platforms.len(), 1);
        assert_eq!(platforms[0].id, "claude");
    }

    #[test]
    fn test_detect_platforms_multiple() {
        let temp = TempDir::new_in(crate::temp::temp_dir_base()).unwrap();
        std::fs::create_dir(temp.path().join(".claude")).unwrap();
        std::fs::create_dir(temp.path().join(".cursor")).unwrap();

        let platforms = detect_platforms(temp.path()).unwrap();
        assert_eq!(platforms.len(), 2);
    }

    #[test]
    fn test_root_agent_file_adds_no_platform() {
        let temp = TempDir::new_in(crate::temp::temp_dir_base()).unwrap();
        std::fs::write(temp.path().join("CLAUDE.md"), "# Claude").unwrap();

        let platforms = detect_platforms(temp.path()).unwrap();
        assert!(
            platforms.is_empty(),
            "root agent files (CLAUDE.md, AGENTS.md, etc.) must not add any platform"
        );
    }

    #[test]
    fn test_detect_platforms_or_error_empty() {
        let temp = TempDir::new_in(crate::temp::temp_dir_base()).unwrap();
        let result = detect_platforms_or_error(temp.path());
        assert!(result.is_err());
    }

    #[test]
    fn test_get_platform() {
        let claude = get_platform("claude", None);
        assert!(claude.is_some());
        assert_eq!(claude.unwrap().id, "claude");

        let unknown = get_platform("unknown", None);
        assert!(unknown.is_none());
    }

    #[test]
    fn test_get_platforms() {
        let platforms = get_platforms(&["claude".to_string(), "cursor".to_string()], None).unwrap();
        assert_eq!(platforms.len(), 2);
    }

    #[test]
    fn test_get_platforms_unknown() {
        let result = get_platforms(&["unknown".to_string()], None);
        assert!(result.is_err());
    }

    #[test]
    fn test_resolve_platforms_specified() {
        let temp = TempDir::new_in(crate::temp::temp_dir_base()).unwrap();
        // Even without any platform directories, specified platforms work
        let platforms = resolve_platforms(temp.path(), &["claude".to_string()]).unwrap();
        assert_eq!(platforms.len(), 1);
        assert_eq!(platforms[0].id, "claude");
    }

    #[test]
    fn test_resolve_platforms_auto_detect() {
        let temp = TempDir::new_in(crate::temp::temp_dir_base()).unwrap();
        std::fs::create_dir(temp.path().join(".opencode")).unwrap();

        let platforms = resolve_platforms(temp.path(), &[]).unwrap();
        assert_eq!(platforms.len(), 1);
        assert_eq!(platforms[0].id, "opencode");
    }

    #[test]
    fn test_get_platform_resolves_alias() {
        let cursor = get_platform("cursor", None);
        assert!(cursor.is_some());
        assert_eq!(cursor.clone().unwrap().id, "cursor");

        let cursor_ai = get_platform("cursor-ai", None);
        assert!(cursor_ai.is_some());
        assert_eq!(cursor_ai.clone().unwrap().id, "cursor");
        assert_eq!(cursor_ai.clone().unwrap().name, "Cursor");
    }

    #[test]
    fn test_get_platforms_resolves_aliases() {
        let platforms =
            get_platforms(&["cursor-ai".to_string(), "claude".to_string()], None).unwrap();
        assert_eq!(platforms.len(), 2);
        assert_eq!(platforms[0].id, "cursor");
        assert_eq!(platforms[0].name, "Cursor");
        assert_eq!(platforms[1].id, "claude");
        assert_eq!(platforms[1].name, "Claude Code");
    }

    #[test]
    fn test_get_platform_unknown_alias() {
        let unknown = get_platform("unknown-alias", None);
        assert!(unknown.is_none());
    }
}