bzr 0.2.0

A CLI for Bugzilla, inspired by gh
Documentation
use std::io::{self, Write as _};

use serde::Serialize;
use tabled::{Table, Tabled};

use super::formatting::{print_formatted, yes_no};
use crate::types::{FieldValue, OutputFormat};

#[derive(Tabled)]
struct FieldValueRow {
    #[tabled(rename = "NAME")]
    name: String,
    #[tabled(rename = "ACTIVE")]
    active: String,
    #[tabled(rename = "CAN CHANGE TO")]
    can_change_to: String,
}

pub fn print_field_values(values: &[FieldValue], format: OutputFormat) {
    print_formatted(values, format, |values| {
        let rows: Vec<FieldValueRow> = values
            .iter()
            .map(|v| {
                let transitions = v
                    .can_change_to
                    .as_ref()
                    .map(|t| {
                        t.iter()
                            .map(|s| s.name.as_str())
                            .collect::<Vec<_>>()
                            .join(", ")
                    })
                    .unwrap_or_default();
                FieldValueRow {
                    name: v.name.clone(),
                    active: yes_no(v.is_active).into(),
                    can_change_to: transitions,
                }
            })
            .collect();
        let _ = writeln!(io::stdout(), "{}", Table::new(rows));
    });
}

#[derive(Serialize, Tabled)]
struct FieldAliasRow {
    #[tabled(rename = "ALIAS")]
    alias: &'static str,
    #[tabled(rename = "API FIELD NAME")]
    api_name: &'static str,
}

pub fn print_field_aliases(aliases: &[(&'static str, &'static str)], format: OutputFormat) {
    let rows: Vec<FieldAliasRow> = aliases
        .iter()
        .map(|&(alias, api_name)| FieldAliasRow { alias, api_name })
        .collect();
    print_formatted(&rows, format, |rows| {
        let _ = writeln!(io::stdout(), "{}", Table::new(rows));
    });
}

#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
    use super::{print_field_aliases, print_field_values};
    use crate::types::{FieldValue, OutputFormat, StatusTransition};

    #[test]
    fn print_field_values_json_empty() {
        let values: Vec<FieldValue> = vec![];
        let json = serde_json::to_string_pretty(&values).unwrap();
        assert_eq!(json, "[]");
    }

    #[test]
    fn print_field_values_json_with_transitions() {
        let values = vec![FieldValue {
            name: "NEW".into(),
            sort_key: 0,
            is_active: true,
            can_change_to: Some(vec![
                StatusTransition {
                    name: "ASSIGNED".into(),
                },
                StatusTransition {
                    name: "RESOLVED".into(),
                },
            ]),
        }];
        let json = serde_json::to_string_pretty(&values).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed[0]["name"], "NEW");
        assert_eq!(parsed[0]["is_active"], true);
        let transitions = parsed[0]["can_change_to"].as_array().unwrap();
        assert_eq!(transitions.len(), 2);
        assert_eq!(transitions[0]["name"], "ASSIGNED");
    }

    #[test]
    fn print_field_values_json_active_and_inactive() {
        let values = vec![
            FieldValue {
                name: "NEW".into(),
                sort_key: 0,
                is_active: true,
                can_change_to: Some(vec![StatusTransition {
                    name: "ASSIGNED".into(),
                }]),
            },
            FieldValue {
                name: "CLOSED".into(),
                sort_key: 1,
                is_active: false,
                can_change_to: None,
            },
        ];
        let json = serde_json::to_string_pretty(&values).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed[0]["name"], "NEW");
        assert_eq!(parsed[0]["is_active"], true);
        assert_eq!(parsed[1]["name"], "CLOSED");
        assert_eq!(parsed[1]["is_active"], false);
        assert!(parsed[1]["can_change_to"].is_null());
    }

    #[test]
    fn print_field_aliases_json_serialization() {
        let aliases: &[(&str, &str)] = &[("status", "bug_status"), ("severity", "bug_severity")];
        let rows: Vec<super::FieldAliasRow> = aliases
            .iter()
            .map(|&(alias, api_name)| super::FieldAliasRow { alias, api_name })
            .collect();
        let json = serde_json::to_string_pretty(&rows).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        let arr = parsed.as_array().unwrap();
        assert_eq!(arr.len(), 2);
        assert_eq!(arr[0]["alias"], "status");
        assert_eq!(arr[0]["api_name"], "bug_status");
        assert_eq!(arr[1]["alias"], "severity");
        assert_eq!(arr[1]["api_name"], "bug_severity");
    }

    // ── capture_stdout-based formatter tests ─────────────────────────

    #[cfg(unix)]
    #[tokio::test]
    async fn print_field_values_table_empty_renders_only_headers() {
        let _lock = crate::ENV_LOCK.lock().await;
        let ((), output) = crate::test_helpers::capture_stdout(async {
            print_field_values(&[], OutputFormat::Table);
        })
        .await;
        assert!(output.contains("NAME"));
        assert!(output.contains("ACTIVE"));
        assert!(output.contains("CAN CHANGE TO"));
    }

    #[cfg(unix)]
    #[tokio::test]
    async fn print_field_values_json_empty_renders_empty_array() {
        let _lock = crate::ENV_LOCK.lock().await;
        let ((), output) = crate::test_helpers::capture_stdout(async {
            print_field_values(&[], OutputFormat::Json);
        })
        .await;
        let parsed = crate::test_helpers::extract_json(&output);
        assert!(parsed.is_array());
        assert_eq!(parsed.as_array().unwrap().len(), 0);
    }

    #[cfg(unix)]
    #[tokio::test]
    async fn print_field_values_table_renders_transitions_and_inactive() {
        let _lock = crate::ENV_LOCK.lock().await;
        let values = vec![
            FieldValue {
                name: "NEW".into(),
                sort_key: 0,
                is_active: true,
                can_change_to: Some(vec![
                    StatusTransition {
                        name: "ASSIGNED".into(),
                    },
                    StatusTransition {
                        name: "RESOLVED".into(),
                    },
                ]),
            },
            FieldValue {
                name: "CLOSED".into(),
                sort_key: 1,
                is_active: false,
                can_change_to: None,
            },
        ];
        let ((), output) = crate::test_helpers::capture_stdout(async {
            print_field_values(&values, OutputFormat::Table);
        })
        .await;
        assert!(output.contains("NEW"));
        assert!(output.contains("CLOSED"));
        assert!(output.contains("Yes"));
        assert!(output.contains("No"));
        assert!(output.contains("ASSIGNED, RESOLVED"));
    }

    #[cfg(unix)]
    #[tokio::test]
    async fn print_field_values_table_handles_unicode_value_name() {
        let _lock = crate::ENV_LOCK.lock().await;
        let values = vec![FieldValue {
            name: "résolu".into(),
            sort_key: 0,
            is_active: true,
            can_change_to: Some(vec![StatusTransition {
                name: "fermé".into(),
            }]),
        }];
        let ((), output) = crate::test_helpers::capture_stdout(async {
            print_field_values(&values, OutputFormat::Table);
        })
        .await;
        assert!(output.contains("résolu"));
        assert!(output.contains("fermé"));
    }

    #[cfg(unix)]
    #[tokio::test]
    async fn print_field_values_json_via_print() {
        let _lock = crate::ENV_LOCK.lock().await;
        let values = vec![FieldValue {
            name: "NEW".into(),
            sort_key: 0,
            is_active: true,
            can_change_to: Some(vec![StatusTransition {
                name: "ASSIGNED".into(),
            }]),
        }];
        let ((), output) = crate::test_helpers::capture_stdout(async {
            print_field_values(&values, OutputFormat::Json);
        })
        .await;
        let parsed = crate::test_helpers::extract_json(&output);
        assert_eq!(parsed[0]["name"], "NEW");
        assert_eq!(parsed[0]["is_active"], true);
    }

    #[cfg(unix)]
    #[tokio::test]
    async fn print_field_aliases_table_renders_rows() {
        let _lock = crate::ENV_LOCK.lock().await;
        let aliases: &[(&str, &str)] = &[("status", "bug_status"), ("severity", "bug_severity")];
        let ((), output) = crate::test_helpers::capture_stdout(async {
            print_field_aliases(aliases, OutputFormat::Table);
        })
        .await;
        assert!(output.contains("ALIAS"));
        assert!(output.contains("API FIELD NAME"));
        assert!(output.contains("status"));
        assert!(output.contains("bug_status"));
        assert!(output.contains("severity"));
        assert!(output.contains("bug_severity"));
    }

    #[cfg(unix)]
    #[tokio::test]
    async fn print_field_aliases_json_via_print() {
        let _lock = crate::ENV_LOCK.lock().await;
        let aliases: &[(&str, &str)] = &[("status", "bug_status")];
        let ((), output) = crate::test_helpers::capture_stdout(async {
            print_field_aliases(aliases, OutputFormat::Json);
        })
        .await;
        let parsed = crate::test_helpers::extract_json(&output);
        assert_eq!(parsed[0]["alias"], "status");
        assert_eq!(parsed[0]["api_name"], "bug_status");
    }
}