bzr 0.1.0

A CLI for Bugzilla, inspired by gh
Documentation
use std::collections::HashMap;

use crate::types::{BugTemplate, OutputFormat};

use super::formatting::{print_field, print_formatted, print_json, print_optional_field};

pub fn print_template_saved(name: &str, verb: &str, format: OutputFormat) {
    match format {
        OutputFormat::Json => {
            print_json(&serde_json::json!({"name": name, "action": verb.to_lowercase()}));
        }
        OutputFormat::Table => {
            println!("{verb} template '{name}'");
        }
    }
}

pub fn print_template_list(templates: &HashMap<String, BugTemplate>, format: OutputFormat) {
    print_formatted(templates, format, |templates| {
        if templates.is_empty() {
            println!("No templates configured.");
            return;
        }
        let mut names: Vec<&str> = templates.keys().map(String::as_str).collect();
        names.sort_unstable();
        for name in names {
            let tmpl = &templates[name];
            let mut parts = Vec::new();
            if let Some(p) = &tmpl.product {
                parts.push(format!("product={p}"));
            }
            if let Some(c) = &tmpl.component {
                parts.push(format!("component={c}"));
            }
            if let Some(p) = &tmpl.priority {
                parts.push(format!("priority={p}"));
            }
            if let Some(s) = &tmpl.severity {
                parts.push(format!("severity={s}"));
            }
            let summary = if parts.is_empty() {
                String::new()
            } else {
                format!(" ({})", parts.join(", "))
            };
            println!("{name}{summary}");
        }
    });
}

pub fn print_template_detail(name: &str, template: &BugTemplate, format: OutputFormat) {
    #[derive(serde::Serialize)]
    struct TemplateView<'a> {
        name: &'a str,
        #[serde(flatten)]
        template: &'a BugTemplate,
    }

    let view = TemplateView { name, template };
    print_formatted(&view, format, |view| {
        print_field("Name", view.name);
        print_optional_field("Product", view.template.product.as_deref());
        print_optional_field("Component", view.template.component.as_deref());
        print_optional_field("Version", view.template.version.as_deref());
        print_optional_field("Priority", view.template.priority.as_deref());
        print_optional_field("Severity", view.template.severity.as_deref());
        print_optional_field("Assignee", view.template.assignee.as_deref());
        print_optional_field("OS", view.template.op_sys.as_deref());
        print_optional_field("Platform", view.template.rep_platform.as_deref());
        print_optional_field("Description", view.template.description.as_deref());
    });
}

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

    fn make_template() -> BugTemplate {
        BugTemplate {
            product: Some("Widget".into()),
            component: Some("Backend".into()),
            version: None,
            priority: Some("P1".into()),
            severity: Some("major".into()),
            assignee: None,
            op_sys: None,
            rep_platform: None,
            description: Some("Default description".into()),
        }
    }

    #[test]
    fn template_saved_json() {
        let json = serde_json::json!({"name": "my-tmpl", "action": "saved"});
        let parsed: serde_json::Value =
            serde_json::from_str(&serde_json::to_string(&json).unwrap()).unwrap();
        assert_eq!(parsed["name"], "my-tmpl");
        assert_eq!(parsed["action"], "saved");
    }

    #[test]
    fn template_list_json_serializes() {
        let mut templates: HashMap<String, BugTemplate> = HashMap::new();
        templates.insert("default".into(), make_template());
        let json = serde_json::to_string_pretty(&templates).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert!(parsed["default"].is_object());
        assert_eq!(parsed["default"]["product"], "Widget");
        assert_eq!(parsed["default"]["component"], "Backend");
    }

    #[test]
    fn template_detail_json_with_flatten() {
        #[derive(serde::Serialize)]
        struct TemplateView<'a> {
            name: &'a str,
            #[serde(flatten)]
            template: &'a BugTemplate,
        }
        let template = make_template();
        let view = TemplateView {
            name: "test-tmpl",
            template: &template,
        };
        let json = serde_json::to_string_pretty(&view).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["name"], "test-tmpl");
        assert_eq!(parsed["product"], "Widget");
        assert_eq!(parsed["priority"], "P1");
        assert!(parsed["version"].is_null());
    }

    #[test]
    fn template_empty_fields_omitted_in_json() {
        let template = BugTemplate {
            product: None,
            component: None,
            version: None,
            priority: None,
            severity: None,
            assignee: None,
            op_sys: None,
            rep_platform: None,
            description: None,
        };
        let json = serde_json::to_string(&template).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert!(parsed.as_object().unwrap().is_empty());
    }
}