//! Pure status-event message formatter.
//!
//! A single [`format_status_event`] function converts any [`StatusEvent`] into
//! a human-readable string.  Adding a new event type means adding one match arm
//! here — no per-transport duplication anywhere in the codebase.

use crate::service::common::item_id::extract_item_number;
use crate::service::notifications::format::stage_display_name;
use crate::service::status::StatusEvent;

/// Render a [`StatusEvent`] into a human-readable plain-text string.
///
/// This is the single formatter for all status event types.  Every transport
/// (chat, Web UI, agent context) calls this function rather than duplicating
/// formatting logic.
// Used by chat/agent transports (stories 642/644); the web UI uses StatusUpdate frames instead.
#[allow(dead_code)]
pub fn format_status_event(event: &StatusEvent) -> String {
    match event {
        StatusEvent::StageTransition {
            story_id,
            story_name,
            from_stage,
            to_stage,
        } => {
            let number = extract_item_number(story_id).unwrap_or(story_id.as_str());
            let name = story_name.as_deref().unwrap_or(story_id.as_str());
            let from = stage_display_name(from_stage);
            let to = stage_display_name(to_stage);
            let prefix = if to == "Done" { "\u{1f389} " } else { "" };
            format!("{prefix}#{number} {name} \u{2014} {from} \u{2192} {to}")
        }
        StatusEvent::MergeFailure {
            story_id,
            story_name,
            reason,
        } => {
            let number = extract_item_number(story_id).unwrap_or(story_id.as_str());
            let name = story_name.as_deref().unwrap_or(story_id.as_str());
            format!("\u{274c} #{number} {name} \u{2014} {reason}")
        }
        StatusEvent::StoryBlocked {
            story_id,
            story_name,
            reason,
        } => {
            let number = extract_item_number(story_id).unwrap_or(story_id.as_str());
            let name = story_name.as_deref().unwrap_or(story_id.as_str());
            format!("\u{1f6ab} #{number} {name} \u{2014} BLOCKED: {reason}")
        }
        StatusEvent::RateLimitWarning {
            story_id,
            story_name,
            agent_name,
        } => {
            let number = extract_item_number(story_id).unwrap_or(story_id.as_str());
            let name = story_name.as_deref().unwrap_or(story_id.as_str());
            format!("\u{26a0}\u{fe0f} #{number} {name} \u{2014} {agent_name} hit an API rate limit")
        }
        StatusEvent::RateLimitHardBlock {
            story_id,
            story_name,
            agent_name,
            reset_at,
        } => {
            let number = extract_item_number(story_id).unwrap_or(story_id.as_str());
            let name = story_name.as_deref().unwrap_or(story_id.as_str());
            let reset = reset_at.format("%H:%M UTC").to_string();
            format!(
                "\u{26d4} #{number} {name} \u{2014} {agent_name} hard rate-limited until {reset}"
            )
        }
    }
}

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

    #[test]
    fn formats_stage_transition_to_done_with_emoji() {
        let event = StatusEvent::StageTransition {
            story_id: "42_story_foo".to_string(),
            story_name: Some("Foo Story".to_string()),
            from_stage: "4_merge".to_string(),
            to_stage: "5_done".to_string(),
        };
        let s = format_status_event(&event);
        assert!(
            s.contains("\u{1f389}"),
            "done transition should include party emoji"
        );
        assert!(s.contains("#42"));
        assert!(s.contains("Foo Story"));
        assert!(s.contains("Merge \u{2192} Done"));
    }

    #[test]
    fn formats_stage_transition_no_emoji_for_non_done() {
        let event = StatusEvent::StageTransition {
            story_id: "10_story_bar".to_string(),
            story_name: Some("Bar".to_string()),
            from_stage: "1_backlog".to_string(),
            to_stage: "2_current".to_string(),
        };
        let s = format_status_event(&event);
        assert!(!s.contains("\u{1f389}"));
        assert!(s.contains("Backlog \u{2192} Current"));
    }

    #[test]
    fn formats_stage_transition_falls_back_to_story_id_when_no_name() {
        let event = StatusEvent::StageTransition {
            story_id: "5_story_x".to_string(),
            story_name: None,
            from_stage: "2_current".to_string(),
            to_stage: "3_qa".to_string(),
        };
        let s = format_status_event(&event);
        assert!(s.contains("5_story_x"));
    }

    #[test]
    fn formats_merge_failure() {
        let event = StatusEvent::MergeFailure {
            story_id: "7_story_fail".to_string(),
            story_name: Some("Failing Story".to_string()),
            reason: "conflicts detected".to_string(),
        };
        let s = format_status_event(&event);
        assert!(s.contains("\u{274c}"));
        assert!(s.contains("#7"));
        assert!(s.contains("conflicts detected"));
    }

    #[test]
    fn formats_story_blocked() {
        let event = StatusEvent::StoryBlocked {
            story_id: "8_story_blk".to_string(),
            story_name: Some("Blocked Story".to_string()),
            reason: "retry limit exceeded".to_string(),
        };
        let s = format_status_event(&event);
        assert!(s.contains("\u{1f6ab}"));
        assert!(s.contains("BLOCKED: retry limit exceeded"));
    }

    #[test]
    fn formats_rate_limit_warning() {
        let event = StatusEvent::RateLimitWarning {
            story_id: "9_story_rl".to_string(),
            story_name: Some("RL Story".to_string()),
            agent_name: "coder-1".to_string(),
        };
        let s = format_status_event(&event);
        assert!(s.contains("coder-1 hit an API rate limit"));
    }

    #[test]
    fn formats_rate_limit_hard_block() {
        let reset = chrono::Utc
            .with_ymd_and_hms(2026, 4, 26, 15, 30, 0)
            .unwrap();
        let event = StatusEvent::RateLimitHardBlock {
            story_id: "3_story_hb".to_string(),
            story_name: Some("HB Story".to_string()),
            agent_name: "coder-2".to_string(),
            reset_at: reset,
        };
        let s = format_status_event(&event);
        assert!(s.contains("\u{26d4}"));
        assert!(s.contains("coder-2"));
        assert!(s.contains("hard rate-limited"));
        assert!(s.contains("15:30 UTC"));
    }

    #[test]
    fn single_formatter_no_per_transport_duplication() {
        // Each event type produces output through a single call — no per-transport variants.
        let events: Vec<StatusEvent> = vec![
            StatusEvent::StageTransition {
                story_id: "1_story_a".to_string(),
                story_name: None,
                from_stage: "1_backlog".to_string(),
                to_stage: "2_current".to_string(),
            },
            StatusEvent::MergeFailure {
                story_id: "2_story_b".to_string(),
                story_name: None,
                reason: "test".to_string(),
            },
            StatusEvent::StoryBlocked {
                story_id: "3_story_c".to_string(),
                story_name: None,
                reason: "limit".to_string(),
            },
        ];
        for e in &events {
            let s = format_status_event(e);
            assert!(!s.is_empty());
        }
    }
}
