malvin 0.2.4

Non-interactive research and coding agent
use super::{ANSI_RESET, ERROR_WHO, WARNING_WHO, WHO_B, format_who_tag_prefix};
use crate::terminal_palette::{ansi_tool_amber, ansi_tool_coral, ansi_tool_navy};

pub(crate) use super::stdout_render::{flush_stdout_rendered_line, print_stdout_rendered_line};
pub(crate) use super::who_tag_ansi;

pub(crate) fn logical_lines(text: &str) -> impl Iterator<Item = &str> {
    text.split_inclusive('\n')
        .map(|part| part.strip_suffix('\n').unwrap_or(part))
}

#[must_use]
pub fn format_line_stdout(who: &str, line: &str) -> String {
    format!("{}{line}", format_who_tag_prefix(who))
}

#[must_use]
pub fn format_line_stdout_ansi(who: &str, line: &str) -> String {
    let prefix = format_who_tag_prefix(who);
    match who {
        WARNING_WHO => format!("{}{prefix}{line}{ANSI_RESET}", ansi_tool_amber()),
        ERROR_WHO => format!("{}{prefix}{line}{ANSI_RESET}", ansi_tool_coral()),
        WHO_B => format!("{}{prefix}{line}{ANSI_RESET}", super::ANSI_DIM),
        _ => {
            let tag_color = who_tag_ansi(who);
            format!("{tag_color}{prefix}{ANSI_RESET}{line}")
        }
    }
}

#[must_use]
pub fn format_heartbeat_stdout_ansi(who: &str, line: &str) -> String {
    format!(
        "{}{}{line}{ANSI_RESET}",
        ansi_tool_navy(),
        super::format_who_tag_delim(who)
    )
}

pub fn print_stdout_line(who: &str, line: &str) {
    super::stdout_display_md::print_stdout_line_with_markdown(who, line, false);
}

pub fn print_stdout_text(who: &str, text: &str) {
    for line in logical_lines(text) {
        print_stdout_line(who, line);
    }
}

pub fn print_stdout_raw_line(line: &str) {
    print_stdout_raw_line_with_ts(line, None);
}

pub fn print_stdout_raw_line_with_ts(line: &str, ts: Option<&str>) {
    let (display, log) = super::stdout_log_pair::stdout_raw_display_and_log_line(line, ts);
    print_stdout_rendered_line(&display, &log);
}

#[allow(dead_code)]
pub(crate) fn flush_stdout_raw_line_with_ts(line: &str, ts: Option<&str>) {
    let (display, log) = super::stdout_log_pair::stdout_raw_display_and_log_line(line, ts);
    flush_stdout_rendered_line(&display, &log);
}

#[cfg(test)]
mod tests {
    use super::{format_line_stdout, format_line_stdout_ansi};
    use crate::output::{
        LOG_TAG_INNER_WIDTH, WHO_H, WHO_M, format_log_tag_inner, format_who_tag_prefix,
    };

    #[test]
    fn stdout_line_omits_timestamp_prefix() {
        let prefix = format_who_tag_prefix(WHO_M);
        assert_eq!(format_line_stdout(WHO_M, "hello"), format!("{prefix}hello"));
        assert!(!format_line_stdout(WHO_M, "hello").starts_with("20"));
    }

    #[test]
    fn stdout_ansi_line_omits_timestamp_prefix() {
        let plain = format_line_stdout(WHO_M, "hello");
        let ansi = format_line_stdout_ansi(WHO_M, "hello");
        assert!(ansi.contains('\x1b'));
        assert!(ansi.ends_with("hello"));
        assert!(!plain.contains('\x1b'));
    }

    #[test]
    fn stdout_display_and_log_splits_timestamp_for_disk() {
        let (display, log) = crate::output::stdout_log_pair::stdout_tagged_display_and_log_line(
            WHO_M, "payload", None,
        );
        assert!(!display.starts_with("20"));
        assert!(log.starts_with("20"));
        assert!(log.contains("|payload"));
    }

    #[test]
    fn log_tag_inner_width_is_one() {
        assert_eq!(LOG_TAG_INNER_WIDTH, 1);
        assert_eq!(format_log_tag_inner("x").chars().count(), 1);
    }

    #[test]
    fn heartbeat_display_omits_timestamp_on_stdout() {
        use std::time::{Duration, Instant};

        use crate::output::is_log_timestamp_token;
        use crate::output::stdout_heartbeat::{
            heartbeat_rendered_if_due, reset_stdout_heartbeat_for_test,
            test_set_last_heartbeat_elapsed,
        };

        reset_stdout_heartbeat_for_test();
        test_set_last_heartbeat_elapsed(Duration::from_secs(61));
        let (display, log) =
            heartbeat_rendered_if_due(Instant::now(), false).expect("heartbeat due");
        assert!(
            !display.starts_with("20"),
            "stdout display must omit wall-clock prefix; got {display:?}"
        );
        let ts = log.split_whitespace().next().expect("log timestamp");
        assert!(is_log_timestamp_token(ts));
    }

    #[test]
    fn heartbeat_display_matches_logger_format_for_color_mode() {
        use crate::output::stdout_heartbeat_display_and_log_line;
        use crate::output::{init_stdout_style_for_test, stdout_use_color};

        init_stdout_style_for_test(false);
        let (display, _) = stdout_heartbeat_display_and_log_line(
            WHO_H,
            "HB: 20260524.000000",
            Some("20260524.000000.000"),
        );
        let expected = if stdout_use_color() {
            super::format_heartbeat_stdout_ansi(WHO_H, "HB: 20260524.000000")
        } else {
            format_line_stdout(WHO_H, "HB: 20260524.000000")
        };
        assert_eq!(display, expected);
        if stdout_use_color() {
            assert!(display.contains('\x1b'));
        }
    }

    #[test]
    fn heartbeat_ansi_display_uses_uniform_who_tag_color() {
        use crate::terminal_palette::ansi_tool_navy;

        let payload = "20260524.000000 Still alive.";
        let (display, log) =
            crate::output::stdout_log_pair::heartbeat_display_and_log_line_for_color(
                WHO_H,
                payload,
                Some("20260524.000000.000"),
                true,
            );
        let expected = super::format_heartbeat_stdout_ansi(WHO_H, payload);
        assert_eq!(display, expected);
        assert!(display.contains('\x1b'));
        assert!(!display.starts_with("20"));
        assert!(log.starts_with("20260524.000000.000"));
        assert!(
            display.starts_with(ansi_tool_navy()),
            "heartbeat line must open with navy who-tag; got {display:?}"
        );
        let reset_before_payload = format!("|{}{payload}", super::ANSI_RESET);
        assert!(
            !display.contains(&reset_before_payload),
            "must not reset color before payload"
        );
        assert!(display.ends_with(super::ANSI_RESET));
    }

    #[test]
    fn heartbeat_rendered_if_due_covers_arm_and_due_paths() {
        use std::time::{Duration, Instant};

        use crate::output::stdout_heartbeat::{
            heartbeat_due, heartbeat_rendered_if_due, reset_stdout_heartbeat_for_test,
            test_set_last_heartbeat_elapsed,
        };

        let now = Instant::now();
        assert!(!heartbeat_due(now, now));
        reset_stdout_heartbeat_for_test();
        assert!(heartbeat_rendered_if_due(Instant::now(), false).is_none());
        assert!(heartbeat_rendered_if_due(Instant::now(), true).is_none());
        assert!(heartbeat_rendered_if_due(Instant::now(), false).is_none());
        test_set_last_heartbeat_elapsed(Duration::from_secs(61));
        assert!(heartbeat_rendered_if_due(Instant::now(), false).is_some());
    }
}