//! Matrix message formatting — Markdown-to-HTML conversion and startup announcements.
use pulldown_cmark::{Options, Parser, html};

/// Format the startup greeting the bot sends to each room when it comes online
/// via a normal (non-trampoline) process start.
///
/// Uses the bot's configured display name plus the same version/git-hash/model
/// details as [`format_gateway_ready_announcement`], so a plain restart carries
/// the same diagnostic information as a trampoline-triggered rebuild instead of
/// just "Timmy is online.". `git_hash` should already have been resolved to
/// `"unknown"` by the caller if unavailable. `model` is `None` when `bot.toml`
/// has no `model` override configured, in which case the CLI's own default
/// applies.
pub fn format_startup_announcement(bot_name: &str, git_hash: &str, model: Option<&str>) -> String {
    format!(
        "{bot_name} is online — gateway {} ({git_hash}) — model: {}",
        env!("CARGO_PKG_VERSION"),
        model.unwrap_or("default")
    )
}

/// Format the ready announcement sent after a successful gateway trampoline restart.
///
/// Returns "gateway X.Y.Z (git_hash) ready — model: <model>" using the compiled-in
/// crate version (same source as `/api/version`), the given git hash, and the
/// configured chat model, so the operator can confirm which binary and model are
/// running after a rebuild. `git_hash` should already have been resolved to
/// `"unknown"` by the caller if unavailable. `model` is `None` when `bot.toml`
/// has no `model` override configured, in which case the CLI's own default applies.
pub fn format_gateway_ready_announcement(git_hash: &str, model: Option<&str>) -> String {
    format!(
        "gateway {} ({git_hash}) ready — model: {}",
        env!("CARGO_PKG_VERSION"),
        model.unwrap_or("default")
    )
}

/// Format the failure announcement sent when the trampoline rolls back to the
/// previous binary.
///
/// `reason` is the human-readable failure description from the trampoline
/// (e.g. "port 3000 already in use").
pub fn format_gateway_rollback_announcement(reason: &str) -> String {
    format!("Gateway rebuild failed: {reason}. Previous version restored.")
}

/// Convert a Markdown string to an HTML string using pulldown-cmark.
///
/// Enables the standard extension set (tables, footnotes, strikethrough,
/// tasklists) so that common Markdown constructs render correctly in Matrix
/// clients such as Element.
pub fn markdown_to_html(markdown: &str) -> String {
    let normalized = crate::chat::util::normalize_line_breaks(markdown);
    let options = Options::ENABLE_TABLES
        | Options::ENABLE_FOOTNOTES
        | Options::ENABLE_STRIKETHROUGH
        | Options::ENABLE_TASKLISTS;
    let parser = Parser::new_ext(&normalized, options);
    let mut html_output = String::new();
    html::push_html(&mut html_output, parser);
    html_output
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn markdown_to_html_bold() {
        let html = markdown_to_html("**bold**");
        assert!(
            html.contains("<strong>bold</strong>"),
            "expected <strong>: {html}"
        );
    }

    #[test]
    fn markdown_to_html_unordered_list() {
        let html = markdown_to_html("- item one\n- item two");
        assert!(html.contains("<ul>"), "expected <ul>: {html}");
        assert!(
            html.contains("<li>item one</li>"),
            "expected list item: {html}"
        );
    }

    #[test]
    fn markdown_to_html_inline_code() {
        let html = markdown_to_html("`inline_code()`");
        assert!(
            html.contains("<code>inline_code()</code>"),
            "expected <code>: {html}"
        );
    }

    #[test]
    fn markdown_to_html_code_block() {
        let html = markdown_to_html("```rust\nfn main() {}\n```");
        assert!(html.contains("<pre>"), "expected <pre>: {html}");
        assert!(html.contains("<code"), "expected <code> inside pre: {html}");
        assert!(
            html.contains("fn main() {}"),
            "expected code content: {html}"
        );
    }

    #[test]
    fn markdown_to_html_plain_text_passthrough() {
        let html = markdown_to_html("Hello, world!");
        assert!(
            html.contains("Hello, world!"),
            "expected plain text passthrough: {html}"
        );
    }

    #[test]
    fn markdown_to_html_single_newline_prose_becomes_paragraphs() {
        // Single newlines between prose sentences should produce separate paragraphs.
        let html = markdown_to_html("Sentence one.\nSentence two.");
        assert!(
            html.contains("<p>Sentence one.</p>"),
            "expected separate paragraph for first sentence: {html}"
        );
        assert!(
            html.contains("<p>Sentence two.</p>"),
            "expected separate paragraph for second sentence: {html}"
        );
    }

    #[test]
    fn markdown_to_html_heading_renders_as_h_tag() {
        let html = markdown_to_html("## Section\nContent here.");
        assert!(
            html.contains("<h2>Section</h2>"),
            "expected <h2> heading tag: {html}"
        );
        assert!(
            html.contains("<p>Content here.</p>"),
            "expected paragraph after heading: {html}"
        );
    }

    #[test]
    fn markdown_to_html_heading_with_preceding_prose_renders_correctly() {
        let html = markdown_to_html("Intro text.\n## Section\nBody.");
        assert!(
            html.contains("<h2>Section</h2>"),
            "expected <h2> heading tag: {html}"
        );
        assert!(
            html.contains("<p>Intro text.</p>"),
            "expected intro paragraph: {html}"
        );
        assert!(
            html.contains("<p>Body.</p>"),
            "expected body paragraph: {html}"
        );
    }

    #[test]
    fn markdown_to_html_multiple_headings_each_render_as_h_tags() {
        let html = markdown_to_html("## Section 1\nContent one.\n\n## Section 2\nContent two.");
        assert!(
            html.contains("<h2>Section 1</h2>"),
            "expected first <h2>: {html}"
        );
        assert!(
            html.contains("<h2>Section 2</h2>"),
            "expected second <h2>: {html}"
        );
    }

    #[test]
    fn startup_announcement_uses_bot_name() {
        let msg = format_startup_announcement("Timmy", "abc1234", Some("claude-sonnet-5"));
        assert!(
            msg.starts_with("Timmy is online"),
            "expected bot name: {msg}"
        );
    }

    #[test]
    fn startup_announcement_uses_configured_display_name_not_hardcoded() {
        let msg = format_startup_announcement("HAL", "abc1234", Some("claude-sonnet-5"));
        assert!(msg.starts_with("HAL is online"), "expected bot name: {msg}");
        let msg = format_startup_announcement("Assistant", "abc1234", Some("claude-sonnet-5"));
        assert!(
            msg.starts_with("Assistant is online"),
            "expected bot name: {msg}"
        );
    }

    #[test]
    fn startup_announcement_includes_version_hash_and_model() {
        let msg = format_startup_announcement("Timmy", "abc1234", Some("claude-sonnet-5"));
        assert!(
            msg.contains(env!("CARGO_PKG_VERSION")),
            "expected crate version in announcement: {msg}"
        );
        assert!(msg.contains("abc1234"), "expected git hash: {msg}");
        assert!(
            msg.contains("claude-sonnet-5"),
            "expected configured model: {msg}"
        );
    }

    #[test]
    fn startup_announcement_renders_unknown_git_hash_without_suppressing_message() {
        let msg = format_startup_announcement("Timmy", "unknown", Some("claude-sonnet-5"));
        assert!(
            msg.contains("unknown"),
            "expected 'unknown' fallback for missing git hash: {msg}"
        );
        assert!(
            msg.starts_with("Timmy is online"),
            "message must still be sent: {msg}"
        );
    }

    #[test]
    fn startup_announcement_falls_back_to_default_when_model_absent() {
        let msg = format_startup_announcement("Timmy", "abc1234", None);
        assert!(
            msg.contains("default"),
            "expected 'default' fallback when no model configured: {msg}"
        );
    }

    #[test]
    fn gateway_ready_announcement_includes_version_hash_and_model() {
        let msg = format_gateway_ready_announcement("abc1234", Some("claude-sonnet-5"));
        assert!(
            msg.contains(env!("CARGO_PKG_VERSION")),
            "expected crate version in announcement: {msg}"
        );
        assert!(msg.contains("abc1234"), "expected git hash: {msg}");
        assert!(
            msg.contains("claude-sonnet-5"),
            "expected configured model: {msg}"
        );
    }

    #[test]
    fn gateway_ready_announcement_renders_unknown_git_hash_cleanly() {
        let msg = format_gateway_ready_announcement("unknown", Some("claude-sonnet-5"));
        assert!(
            msg.contains("unknown"),
            "expected 'unknown' fallback for missing git hash: {msg}"
        );
    }

    #[test]
    fn gateway_ready_announcement_falls_back_to_default_when_model_absent() {
        let msg = format_gateway_ready_announcement("abc1234", None);
        assert!(
            msg.contains("default"),
            "expected 'default' fallback when no model configured: {msg}"
        );
    }

    #[test]
    fn bot_name_defaults_to_assistant_when_display_name_absent() {
        // When display_name is not set in bot.toml, bot_name should be "Assistant".
        // This mirrors the logic in run_bot: config.display_name.clone().unwrap_or_else(...)
        fn resolve_bot_name(display_name: Option<String>) -> String {
            display_name.unwrap_or_else(|| "Assistant".to_string())
        }
        assert_eq!(resolve_bot_name(None), "Assistant");
        assert_eq!(resolve_bot_name(Some("Timmy".to_string())), "Timmy");
    }
}
