cardinal-app 0.1.3

Thin CLI entrypoint for Cardinal.
use std::process::Command;

use serde_json::Value;

fn cardinal(args: &[&str]) -> std::process::Output {
    let mut command = Command::new(env!("CARGO_BIN_EXE_cardinal"));
    command.args(args);
    for key in [
        "CARDINAL_MAILDIR",
        "CARDINAL_VDIR",
        "CARDINAL_SMTP_HOST",
        "CARDINAL_SMTP_PORT",
        "CARDINAL_SMTP_FROM",
        "CARDINAL_SMTP_HELO",
        "CARDINAL_SMTP_USERNAME",
        "CARDINAL_SMTP_PASSWORD_CMD",
        "CARDINAL_SMTP_TIMEOUT_SECS",
        "CARDINAL_SYNC_MAIL_CMD",
        "CARDINAL_SYNC_CALENDAR_CMD",
        "CARDINAL_SYNC_CONTACTS_CMD",
        "CARDINAL_LOG_PATH",
        "CARDINAL_LOG_STDERR",
    ] {
        command.env_remove(key);
    }
    command.output().expect("failed to run cardinal")
}

#[test]
fn parse_subcommand_prints_parsed_command() {
    let output = cardinal(&["parse", ":list inboxes"]);
    assert!(output.status.success());

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("List("));
    assert!(stdout.contains("Inboxes"));
}

#[test]
fn parse_subcommand_returns_exit_code_2_for_invalid_command() {
    let output = cardinal(&["parse", ":explode"]);
    assert_eq!(output.status.code(), Some(2));

    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("unknown command: explode"));
}

#[test]
fn parse_subcommand_json_success_is_machine_readable() {
    let output = cardinal(&["parse", "--format", "json", ":list inboxes"]);
    assert!(output.status.success());

    let stdout = String::from_utf8_lossy(&output.stdout);
    let parsed: Value = serde_json::from_str(&stdout).expect("valid parse json");
    assert_eq!(parsed["ok"], Value::Bool(true));
    assert_eq!(parsed["command"]["kind"], Value::String("list".to_owned()));
    assert_eq!(
        parsed["command"]["target"],
        Value::String("inboxes".to_owned())
    );
    assert_eq!(
        parsed["isa"]["safety"],
        Value::String("read-only".to_owned())
    );
}

#[test]
fn parse_subcommand_json_error_is_machine_readable() {
    let output = cardinal(&["parse", "--format", "json", ":explode"]);
    assert_eq!(output.status.code(), Some(2));

    let stdout = String::from_utf8_lossy(&output.stdout);
    let parsed: Value = serde_json::from_str(&stdout).expect("valid parse error json");
    assert_eq!(parsed["ok"], Value::Bool(false));
    assert_eq!(
        parsed["error"]["kind"],
        Value::String("unknown_command".to_owned())
    );
    assert_eq!(
        parsed["error"]["details"]["command"],
        Value::String("explode".to_owned())
    );
}

#[test]
fn doctor_reports_scaffold_status() {
    let output = cardinal(&["doctor"]);
    assert!(output.status.success());

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("cardinal: pre-alpha scaffold"));
    assert!(stdout.contains("core: command parser available"));
    assert!(stdout.contains("tui: milestone 1-10 scaffold available"));
}

#[test]
fn commands_lists_examples() {
    let output = cardinal(&["commands"]);
    assert!(output.status.success());

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains(":list inboxes"));
    assert!(stdout.contains(":calendar today"));
}

#[test]
fn config_validate_returns_success_without_errors() {
    let output = cardinal(&["config", "validate"]);
    assert!(output.status.success());
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("config:"));
}

#[test]
fn completions_subcommand_emits_script() {
    let output = cardinal(&["completions", "bash"]);
    assert!(output.status.success());
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("cardinal"));
}

#[test]
fn man_subcommand_emits_manpage_content() {
    let output = cardinal(&["man"]);
    assert!(output.status.success());
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains(".TH"));
    assert!(stdout.to_ascii_lowercase().contains("cardinal"));
}