standout 9.0.0

Styled CLI template rendering with automatic terminal detection
use serde_json::json;
use standout::cli::hooks::Hooks;
use standout::cli::FnHandler;
use standout::cli::{App, Output};
use standout::EmbeddedTemplates;

const TEMPLATES: &[(&str, &str)] = &[("show", "ok"), ("help", "ok"), ("show-2", "{{ name }}")];

fn build_error(result: Result<standout::cli::App, standout::SetupError>) -> String {
    match result {
        Ok(_) => panic!("expected build to fail"),
        Err(error) => error.to_string(),
    }
}

#[test]
fn render_named_without_registry_names_the_builder_calls() {
    let app = App::builder()
        .include_framework_templates(false)
        .build()
        .unwrap();

    let error = app.render_with(
        standout::TemplateRef::Named(("missing").to_string()),
        &json!({}),
        standout::OutputMode::Text,
        standout::TargetProperties::detect(),
    );
    let message = error
        .expect_err("named render needs a template registry")
        .to_string();

    assert!(message
        .contains("render_with(TemplateRef::Named(\"missing\"), ...) needs a template registry"));
    assert!(message.contains(".templates(embed_templates!"));
    assert!(message.contains(".templates_dir(\"path/to/templates\")"));
    assert!(message.contains("TemplateRef::Inline"));
}

#[test]
fn render_named_structured_without_registry_serializes() {
    let app = App::builder()
        .include_framework_templates(false)
        .build()
        .unwrap();
    let data = json!({"name": "Ada"});

    let json = app
        .render_with(
            standout::TemplateRef::Named(("unused").to_string()),
            &data,
            standout::OutputMode::Json,
            standout::TargetProperties::detect(),
        )
        .expect("structured render does not need a registry");
    assert!(json.contains("\"name\": \"Ada\""));

    let yaml = app
        .render_with(
            standout::TemplateRef::Named(("unused").to_string()),
            &data,
            standout::OutputMode::Yaml,
            standout::TargetProperties::detect(),
        )
        .expect("structured render does not need a registry");
    assert!(yaml.contains("name: Ada"));

    let xml = app
        .render_with(
            standout::TemplateRef::Named(("unused").to_string()),
            &data,
            standout::OutputMode::Xml,
            standout::TargetProperties::detect(),
        )
        .expect("structured render does not need a registry");
    assert!(xml.contains("<name>Ada</name>"));

    let csv = app
        .render_with(
            standout::TemplateRef::Named(("unused").to_string()),
            &data,
            standout::OutputMode::Csv,
            standout::TargetProperties::detect(),
        )
        .expect("structured render does not need a registry");
    assert!(csv.contains("name"));
    assert!(csv.contains("Ada"));
}

#[test]
fn render_named_structured_with_unregistered_name_serializes() {
    let dir = tempfile::tempdir().unwrap();
    std::fs::write(dir.path().join("show.jinja"), "Hello {{ name }}").unwrap();
    let app = App::builder()
        .templates_dir(dir.path())
        .unwrap()
        .build()
        .unwrap();
    let data = json!({"name": "Ada"});

    for mode in [
        standout::OutputMode::Json,
        standout::OutputMode::Yaml,
        standout::OutputMode::Xml,
        standout::OutputMode::Csv,
    ] {
        app.render_with(
            standout::TemplateRef::Named(("not-registered").to_string()),
            &data,
            mode,
            standout::TargetProperties::detect(),
        )
        .unwrap_or_else(|error| panic!("structured {mode:?} ignored the missing name: {error}"));
    }
}

#[test]
fn render_named_file_refresh_error_keeps_read_context() {
    let dir = tempfile::tempdir().unwrap();
    let template_path = dir.path().join("show.jinja");
    std::fs::write(&template_path, "Hello {{ name }}").unwrap();

    let app = App::builder()
        .templates_dir(dir.path())
        .unwrap()
        .build()
        .unwrap();
    std::fs::remove_file(&template_path).unwrap();

    let message = app
        .render_with(
            standout::TemplateRef::Named(("show").to_string()),
            &json!({"name": "Ada"}),
            standout::OutputMode::Text,
            standout::TargetProperties::detect(),
        )
        .expect_err("deleted template should fail during render refresh")
        .to_string();

    assert!(message.contains("could not refresh the registered template"));
    assert!(message.contains(&template_path.display().to_string()));
    assert!(
        !message.contains("could not find the named template"),
        "{message}"
    );
}

#[test]
fn hook_conflict_error_names_the_phase_and_single_registration_fix() {
    let result = App::builder()
        .templates(EmbeddedTemplates::new(TEMPLATES, ""))
        .command_with(
            "show",
            FnHandler::new(|_m, _ctx| Ok(Output::Render(json!({"name": "Ada"})))),
            |config| config.template_name("show-2").pre_dispatch(|_, _| Ok(())),
        )
        .unwrap()
        .hooks("show", Hooks::new().pre_dispatch(|_, _| Ok(())))
        .build();

    let message = build_error(result);

    assert!(message.contains("command `show` registers pre-dispatch hooks"));
    assert!(message.contains("CommandConfig"));
    assert!(message.contains("AppBuilder::hooks"));
    assert!(message.contains("keep each (path, phase) in one registration path"));
}

#[test]
fn help_configuration_errors_name_the_call_that_turned_help_off() {
    let result = App::builder()
        .templates(EmbeddedTemplates::new(TEMPLATES, ""))
        .help_handling(false)
        .command_groups(vec![standout::cli::help::CommandGroup {
            title: "Commands".into(),
            help: None,
            commands: vec![Some("show".into())],
        }])
        .build();

    let message = build_error(result);

    assert!(message.contains("command_groups is configured while help handling is off"));
    assert!(message.contains("intercepting help"));
    assert!(message.contains(".help_handling(false)"));
}

#[test]
fn duplicate_command_still_names_the_conflicting_command() {
    let result = App::builder()
        .templates(EmbeddedTemplates::new(TEMPLATES, ""))
        .command_with(
            "show",
            FnHandler::new(|_m, _ctx| Ok(Output::Render(json!({})))),
            |cfg| cfg,
        )
        .unwrap()
        .command_with(
            "show",
            FnHandler::new(|_m, _ctx| Ok(Output::Render(json!({})))),
            |cfg| cfg,
        );

    let message = match result {
        Ok(_) => panic!("duplicate command must fail"),
        Err(error) => error.to_string(),
    };
    assert!(message.contains("duplicate command: show"));
}

#[test]
fn registered_help_collision_names_the_setting_and_rename_fix() {
    let result = App::builder()
        .templates(EmbeddedTemplates::new(TEMPLATES, ""))
        .command_with(
            "help",
            FnHandler::new(|_m, _ctx| Ok(Output::Render(json!({})))),
            |cfg| cfg,
        )
        .unwrap()
        .build();

    let message = build_error(result);
    assert!(message.contains("duplicate command: help"));
    assert!(message.contains(".help_handling(false)"));
    assert!(message.contains("Rename"));
}

#[test]
fn command_named_template_without_registry_names_the_fix() {
    let app = App::builder()
        .include_framework_templates(false)
        .command_with(
            "show",
            FnHandler::new(|_m, _ctx| Ok(Output::Render(json!({"name": "Ada"})))),
            |config| config.template_name("show"),
        )
        .unwrap()
        .build()
        .map_err(|error| error.to_string());

    let message = match app {
        Ok(_) => panic!("build catches missing registries before dispatch"),
        Err(message) => message,
    };
    assert!(message.contains("no application templates are configured"));
    assert!(message.contains(".templates(embed_templates!"));
    assert!(message.contains(".templates_dir(\"path/to/templates\")"));
}