bzr 0.1.1

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};

fn template_saved_message(name: &str, verb: &str) -> String {
    format!("{verb} template '{name}'")
}

fn template_summary_line(name: &str, tmpl: &BugTemplate) -> String {
    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}"));
    }
    if parts.is_empty() {
        name.to_string()
    } else {
        format!("{name} ({})", parts.join(", "))
    }
}

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!("{}", template_saved_message(name, verb));
        }
    }
}

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 {
            println!("{}", template_summary_line(name, &templates[name]));
        }
    });
}

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());
    }

    #[test]
    fn template_saved_message_renders_table_text() {
        assert_eq!(
            template_saved_message("default", "Saved"),
            "Saved template 'default'"
        );
    }

    #[test]
    fn template_summary_line_renders_all_present_fields() {
        let line = template_summary_line("aaa", &make_template());
        assert_eq!(
            line,
            "aaa (product=Widget, component=Backend, priority=P1, severity=major)"
        );
    }

    #[test]
    fn template_summary_line_without_fields_is_name_only() {
        let line = template_summary_line(
            "zzz",
            &BugTemplate {
                product: None,
                component: None,
                version: None,
                priority: None,
                severity: None,
                assignee: None,
                op_sys: None,
                rep_platform: None,
                description: None,
            },
        );
        assert_eq!(line, "zzz");
    }

    #[cfg(unix)]
    #[tokio::test]
    async fn print_template_detail_table_renders_missing_fields_as_dash() {
        let _lock = crate::ENV_LOCK.lock().await;
        let template = BugTemplate {
            product: Some("Widget".into()),
            component: None,
            version: None,
            priority: Some("P1".into()),
            severity: None,
            assignee: None,
            op_sys: None,
            rep_platform: None,
            description: Some("Default description".into()),
        };

        let ((), output) = crate::test_helpers::capture_stdout(async {
            print_template_detail("default", &template, OutputFormat::Table);
        })
        .await;

        assert!(output.contains("Name"));
        assert!(output.contains("default"));
        assert!(output.contains("Product"));
        assert!(output.contains("Widget"));
        assert!(output.contains("Component"));
        assert!(output.contains("  -"));
        assert!(output.contains("Description"));
        assert!(output.contains("Default description"));
    }

    #[cfg(unix)]
    #[tokio::test]
    async fn template_list_names_sort_before_render() {
        let _lock = crate::ENV_LOCK.lock().await;
        let mut templates: HashMap<String, BugTemplate> = HashMap::new();
        templates.insert("zzz".into(), make_template());
        templates.insert(
            "aaa".into(),
            BugTemplate {
                product: Some("Alpha".into()),
                component: None,
                version: None,
                priority: None,
                severity: None,
                assignee: None,
                op_sys: None,
                rep_platform: None,
                description: None,
            },
        );

        let mut names: Vec<&str> = templates.keys().map(String::as_str).collect();
        names.sort_unstable();
        let rendered: Vec<String> = names
            .into_iter()
            .map(|name| template_summary_line(name, &templates[name]))
            .collect();

        assert_eq!(
            rendered,
            vec![
                "aaa (product=Alpha)".to_string(),
                "zzz (product=Widget, component=Backend, priority=P1, severity=major)".to_string(),
            ]
        );
    }
}