//! 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::pipeline_state::Stage;
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 = if story_name.is_empty() {
                story_id.as_str()
            } else {
                story_name.as_str()
            };
            let from_typed = Stage::from_dir(from_stage).unwrap_or(Stage::Upcoming);
            let to_typed = Stage::from_dir(to_stage).unwrap_or(Stage::Upcoming);
            let from = stage_display_name(&from_typed);
            let to = stage_display_name(&to_typed);
            let prefix = if matches!(to_typed, Stage::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 = if story_name.is_empty() {
                story_id.as_str()
            } else {
                story_name.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 = if story_name.is_empty() {
                story_id.as_str()
            } else {
                story_name.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 = if story_name.is_empty() {
                story_id.as_str()
            } else {
                story_name.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 = if story_name.is_empty() {
                story_id.as_str()
            } else {
                story_name.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}"
            )
        }
        StatusEvent::DiskSpaceWarning {
            level,
            free_bytes,
            host_id,
            ..
        } => {
            let free_gb = *free_bytes as f64 / 1_000_000_000.0;
            format!("\u{26a0}\u{fe0f} Low disk space on {host_id} ({level}): {free_gb:.1}GB free")
        }
        StatusEvent::DiskSpaceRecovered {
            free_bytes,
            host_id,
        } => {
            let free_gb = *free_bytes as f64 / 1_000_000_000.0;
            format!("\u{2705} Disk space recovered on {host_id}: {free_gb:.1}GB free")
        }
    }
}

#[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: "Foo Story".to_string(),
            from_stage: "merge".to_string(),
            to_stage: "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: "Bar".to_string(),
            from_stage: "backlog".to_string(),
            to_stage: "coding".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: String::new(),
            from_stage: "coding".to_string(),
            to_stage: "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: "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: "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: "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: "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: String::new(),
                from_stage: "backlog".to_string(),
                to_stage: "coding".to_string(),
            },
            StatusEvent::MergeFailure {
                story_id: "2_story_b".to_string(),
                story_name: String::new(),
                reason: "test".to_string(),
            },
            StatusEvent::StoryBlocked {
                story_id: "3_story_c".to_string(),
                story_name: String::new(),
                reason: "limit".to_string(),
            },
        ];
        for e in &events {
            let s = format_status_event(e);
            assert!(!s.is_empty());
        }
    }
}
