ui-cli 0.3.4

A CLI to add components to your app.
Documentation
use std::collections::{HashMap, HashSet};

use crate::shared::cli_error::CliResult;

#[derive(Debug, Clone)]
pub struct TreeParser {
    components: HashMap<String, ComponentEntry>,
}

#[derive(Debug, Clone)]
pub struct ComponentEntry {
    pub name: String,
    pub category: String,
    pub dependencies: Vec<String>,
    pub cargo_deps: Vec<String>,
}

#[derive(Debug, Clone)]
pub struct ResolvedSet {
    pub components: HashSet<String>,
    pub cargo_deps: HashSet<String>,
    pub parent_dirs: HashSet<String>,
}

impl TreeParser {
    pub fn parse_tree_md(content: &str) -> CliResult<Self> {
        let mut components = HashMap::new();
        let mut current_component: Option<ComponentEntry> = None;
        let mut dependency_stack: Vec<String> = Vec::new();

        for line in content.lines() {
            let line = line.trim();

            // Skip empty lines and code block markers
            if line.is_empty() || line.starts_with("```") {
                continue;
            }

            // Parse component lines (*)
            if let Some(line_content) = line.strip_prefix("* ") {
                // Save previous component if exists
                if let Some(component) = current_component.take() {
                    components.insert(component.name.clone(), component);
                }

                if let Some((name_part, category_part)) = line_content.rsplit_once(" (") {
                    let name = name_part.trim().to_string();
                    let category = category_part.trim_end_matches(')').to_string();

                    current_component = Some(ComponentEntry {
                        name: name.clone(),
                        category,
                        dependencies: Vec::new(),
                        cargo_deps: Vec::new(),
                    });

                    dependency_stack.clear();
                    dependency_stack.push(name);
                }
            }
            // Parse dependency lines (**)
            else if let Some(dep_content) = line.strip_prefix("** ") {
                if let Some(cargo_dep_name) = dep_content.strip_prefix("cargo: ") {
                    // Cargo dependency
                    let cargo_dep = cargo_dep_name.trim().to_string();
                    if let Some(ref mut component) = current_component {
                        component.cargo_deps.push(cargo_dep);
                    }
                } else if let Some((dep_name, _)) = dep_content.rsplit_once(" (") {
                    // Registry dependency
                    let dep_name = dep_name.trim().to_string();
                    if let Some(ref mut component) = current_component {
                        component.dependencies.push(dep_name.clone());
                    }

                    // Update dependency stack for nested dependencies
                    dependency_stack.truncate(1); // Keep only root component
                    dependency_stack.push(dep_name);
                }
            }
            // Parse nested dependency lines (***)
            else if let Some(dep_content) = line.strip_prefix("*** ") {
                if let Some(cargo_dep_name) = dep_content.strip_prefix("cargo: ") {
                    // Nested cargo dependency - add to root component
                    let cargo_dep = cargo_dep_name.trim().to_string();
                    if let Some(ref mut component) = current_component {
                        component.cargo_deps.push(cargo_dep);
                    }
                } else if let Some((dep_name, _)) = dep_content.rsplit_once(" (") {
                    // Nested registry dependency - add to root component
                    let dep_name = dep_name.trim().to_string();
                    if let Some(ref mut component) = current_component {
                        component.dependencies.push(dep_name);
                    }
                }
            }
        }

        // Save last component
        if let Some(component) = current_component {
            components.insert(component.name.clone(), component);
        }

        Ok(TreeParser { components })
    }

    pub fn get_all_component_names(&self) -> Vec<String> {
        let mut names: Vec<String> = self.components.keys().cloned().collect();
        names.sort();
        names
    }

    pub fn get_dependencies_map(&self) -> HashMap<String, Vec<String>> {
        self.components
            .iter()
            .map(|(name, entry)| (name.clone(), entry.dependencies.clone()))
            .collect()
    }

    pub fn resolve_dependencies(&self, user_components: &[String]) -> CliResult<ResolvedSet> {
        let mut resolved_components = HashSet::new();
        let mut resolved_cargo_deps = HashSet::new();
        let mut resolved_parent_dirs = HashSet::new();

        // Process each user component
        for component_name in user_components {
            if let Some(component_entry) = self.components.get(component_name) {
                // Add the component itself
                resolved_components.insert(component_name.clone());
                resolved_parent_dirs.insert(component_entry.category.clone());

                // Add its direct dependencies
                for dep in &component_entry.dependencies {
                    resolved_components.insert(dep.clone());

                    // Add parent dir for dependency
                    if let Some(dep_entry) = self.components.get(dep) {
                        resolved_parent_dirs.insert(dep_entry.category.clone());
                    }
                }

                // Add cargo dependencies
                for cargo_dep in &component_entry.cargo_deps {
                    resolved_cargo_deps.insert(cargo_dep.clone());
                }
            } else {
                println!("⚠️  Component '{}' not found in registry. Skipping...", component_name);
            }
        }

        println!("📦 Final set of resolved components: {:?}", resolved_components);
        println!("📦 Final set of cargo dependencies: {:?}", resolved_cargo_deps);

        Ok(ResolvedSet {
            components: resolved_components,
            cargo_deps: resolved_cargo_deps,
            parent_dirs: resolved_parent_dirs,
        })
    }
}

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

    const SAMPLE_TREE: &str = r#"
* button (ui)
** badge (ui)
** cargo: some-crate

* badge (ui)

* card (ui)
** button (ui)
*** badge (ui)

* demo_button (demos)
** button (ui)
"#;

    #[test]
    fn parse_tree_md_extracts_components() {
        let parser = TreeParser::parse_tree_md(SAMPLE_TREE).unwrap();
        let names = parser.get_all_component_names();
        assert!(names.contains(&"button".to_string()));
        assert!(names.contains(&"badge".to_string()));
        assert!(names.contains(&"card".to_string()));
        assert!(names.contains(&"demo_button".to_string()));
    }

    #[test]
    fn parse_tree_md_extracts_dependencies() {
        let parser = TreeParser::parse_tree_md(SAMPLE_TREE).unwrap();
        let deps_map = parser.get_dependencies_map();

        assert_eq!(deps_map.get("button").unwrap(), &vec!["badge".to_string()]);
        assert!(deps_map.get("badge").unwrap().is_empty());
        assert_eq!(deps_map.get("card").unwrap(), &vec!["button".to_string(), "badge".to_string()]);
    }

    #[test]
    fn parse_tree_md_extracts_cargo_deps() {
        let parser = TreeParser::parse_tree_md(SAMPLE_TREE).unwrap();
        let entry = parser.components.get("button").unwrap();
        assert!(entry.cargo_deps.contains(&"some-crate".to_string()));
    }

    #[test]
    fn parse_tree_md_extracts_category() {
        let parser = TreeParser::parse_tree_md(SAMPLE_TREE).unwrap();
        assert_eq!(parser.components.get("button").unwrap().category, "ui");
        assert_eq!(parser.components.get("demo_button").unwrap().category, "demos");
    }

    #[test]
    fn get_all_component_names_sorted() {
        let parser = TreeParser::parse_tree_md(SAMPLE_TREE).unwrap();
        let names = parser.get_all_component_names();
        let mut sorted = names.clone();
        sorted.sort();
        assert_eq!(names, sorted);
    }

    #[test]
    fn resolve_dependencies_includes_transitive() {
        let parser = TreeParser::parse_tree_md(SAMPLE_TREE).unwrap();
        let resolved = parser.resolve_dependencies(&["card".to_string()]).unwrap();

        assert!(resolved.components.contains("card"));
        assert!(resolved.components.contains("button"));
        assert!(resolved.components.contains("badge"));
    }

    #[test]
    fn resolve_dependencies_collects_parent_dirs() {
        let parser = TreeParser::parse_tree_md(SAMPLE_TREE).unwrap();
        let resolved = parser.resolve_dependencies(&["demo_button".to_string()]).unwrap();

        assert!(resolved.parent_dirs.contains("demos"));
        assert!(resolved.parent_dirs.contains("ui"));
    }

    #[test]
    fn resolve_dependencies_missing_component_skipped() {
        let parser = TreeParser::parse_tree_md(SAMPLE_TREE).unwrap();
        let resolved = parser.resolve_dependencies(&["nonexistent".to_string()]).unwrap();
        assert!(resolved.components.is_empty());
    }
}