//! Conversions from domain events to WebSocket response messages.

use crate::http::workflow::PipelineState;
use crate::io::watcher::WatcherEvent;

use super::response::{WizardStepInfo, WsResponse};

/// Convert a [`WatcherEvent`] to an optional [`WsResponse`].
///
/// Returns `None` for events that have no WebSocket representation
/// (e.g. `MergeFailure`, `StoryBlocked` — handled elsewhere).
pub fn watcher_event_to_response(e: WatcherEvent) -> Option<WsResponse> {
    match e {
        WatcherEvent::WorkItem {
            stage,
            item_id,
            action,
            commit_msg,
            ..
        } => Some(WsResponse::WorkItemChanged {
            stage,
            item_id,
            action,
            commit_msg,
        }),
        WatcherEvent::ConfigChanged => Some(WsResponse::AgentConfigChanged),
        WatcherEvent::AgentStateChanged => Some(WsResponse::AgentStateChanged),
        // MergeFailure, RateLimitWarning, StoryBlocked, and RateLimitHardBlock are handled
        // by the chat notification listener only; no WebSocket message is needed for the frontend.
        WatcherEvent::MergeFailure { .. } => None,
        WatcherEvent::RateLimitWarning { .. } => None,
        WatcherEvent::StoryBlocked { .. } => None,
        WatcherEvent::RateLimitHardBlock { .. } => None,
        // OAuth events are forwarded to chat transports only; no WebSocket message for the frontend.
        WatcherEvent::OAuthAccountSwapped { .. } => None,
        WatcherEvent::OAuthAccountsExhausted { .. } => None,
        // Agent lifecycle events are forwarded to chat transports only; no WebSocket message.
        WatcherEvent::AgentStarted { .. } => None,
        WatcherEvent::AgentCompleted { .. } => None,
        // Creation notifications are forwarded to chat transports only; no WebSocket message.
        WatcherEvent::NewItemCreated { .. } => None,
        // Merge-auto-retry notifications are forwarded to chat transports only; no WebSocket message.
        WatcherEvent::MergeAutoRetry { .. } => None,
        // Disk-space events are forwarded to chat transports only; no WebSocket message (story 1200).
        WatcherEvent::DiskSpaceWarning { .. } => None,
        WatcherEvent::DiskSpaceRecovered { .. } => None,
    }
}

/// Returns `true` if this watcher event should trigger a pipeline state refresh.
pub fn needs_pipeline_refresh(evt: &WatcherEvent) -> bool {
    matches!(
        evt,
        WatcherEvent::WorkItem { .. } | WatcherEvent::AgentStateChanged
    )
}

/// Convert a [`PipelineState`] to a [`WsResponse::PipelineState`].
pub fn pipeline_state_to_response(s: PipelineState) -> WsResponse {
    WsResponse::PipelineState {
        backlog: s.backlog,
        current: s.current,
        qa: s.qa,
        merge: s.merge,
        done: s.done,
        deterministic_merges_in_flight: s.deterministic_merges_in_flight,
    }
}

/// Build a [`WizardStepInfo`] list from wizard step states.
///
/// Pure conversion — reads no filesystem, just transforms data.
pub fn wizard_steps_to_info(steps: &[crate::io::wizard::StepState]) -> Vec<WizardStepInfo> {
    steps
        .iter()
        .map(|s| WizardStepInfo {
            step: serde_json::to_value(s.step)
                .ok()
                .and_then(|v| v.as_str().map(String::from))
                .unwrap_or_default(),
            label: s.step.label().to_string(),
            status: serde_json::to_value(&s.status)
                .ok()
                .and_then(|v| v.as_str().map(String::from))
                .unwrap_or_default(),
            content: s.content.clone(),
        })
        .collect()
}

// Keep backward-compatible From impls so existing code compiles during migration.
impl From<WatcherEvent> for Option<WsResponse> {
    fn from(e: WatcherEvent) -> Self {
        watcher_event_to_response(e)
    }
}

impl From<PipelineState> for WsResponse {
    fn from(s: PipelineState) -> Self {
        pipeline_state_to_response(s)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::http::workflow::UpcomingStory;
    use crate::io::watcher::WatcherEvent;

    #[test]
    fn watcher_work_item_converts_to_ws_response() {
        let evt = WatcherEvent::WorkItem {
            stage: "2_current".to_string(),
            item_id: "42_story_foo".to_string(),
            action: "start".to_string(),
            commit_msg: "huskies: start 42_story_foo".to_string(),
            from_stage: None,
        };
        let ws_msg = watcher_event_to_response(evt).expect("WorkItem should produce Some");
        let json = serde_json::to_value(&ws_msg).unwrap();
        assert_eq!(json["type"], "work_item_changed");
        assert_eq!(json["stage"], "2_current");
        assert_eq!(json["item_id"], "42_story_foo");
        assert_eq!(json["action"], "start");
    }

    #[test]
    fn watcher_config_changed_converts_to_ws_response() {
        let evt = WatcherEvent::ConfigChanged;
        let ws_msg = watcher_event_to_response(evt).expect("ConfigChanged should produce Some");
        let json = serde_json::to_value(&ws_msg).unwrap();
        assert_eq!(json["type"], "agent_config_changed");
    }

    #[test]
    fn watcher_agent_state_changed_converts_to_ws_response() {
        let evt = WatcherEvent::AgentStateChanged;
        let ws_msg = watcher_event_to_response(evt).expect("AgentStateChanged should produce Some");
        let json = serde_json::to_value(&ws_msg).unwrap();
        assert_eq!(json["type"], "agent_state_changed");
    }

    #[test]
    fn watcher_merge_failure_produces_none() {
        let evt = WatcherEvent::MergeFailure {
            story_id: "x".to_string(),
            reason: "conflict".to_string(),
        };
        assert!(watcher_event_to_response(evt).is_none());
    }

    #[test]
    fn watcher_rate_limit_warning_produces_none() {
        let evt = WatcherEvent::RateLimitWarning {
            story_id: "x".to_string(),
            agent_name: "coder".to_string(),
        };
        assert!(watcher_event_to_response(evt).is_none());
    }

    #[test]
    fn watcher_story_blocked_produces_none() {
        let evt = WatcherEvent::StoryBlocked {
            story_id: "x".to_string(),
            reason: "retries exhausted".to_string(),
        };
        assert!(watcher_event_to_response(evt).is_none());
    }

    #[test]
    fn watcher_rate_limit_hard_block_produces_none() {
        let evt = WatcherEvent::RateLimitHardBlock {
            story_id: "x".to_string(),
            agent_name: "coder".to_string(),
            reset_at: chrono::Utc::now(),
        };
        assert!(watcher_event_to_response(evt).is_none());
    }

    #[test]
    fn work_item_needs_pipeline_refresh() {
        let evt = WatcherEvent::WorkItem {
            stage: "2_current".to_string(),
            item_id: "x".to_string(),
            action: "start".to_string(),
            commit_msg: "msg".to_string(),
            from_stage: None,
        };
        assert!(needs_pipeline_refresh(&evt));
    }

    #[test]
    fn agent_state_changed_needs_pipeline_refresh() {
        assert!(needs_pipeline_refresh(&WatcherEvent::AgentStateChanged));
    }

    #[test]
    fn config_changed_does_not_need_pipeline_refresh() {
        assert!(!needs_pipeline_refresh(&WatcherEvent::ConfigChanged));
    }

    #[test]
    fn merge_failure_does_not_need_pipeline_refresh() {
        let evt = WatcherEvent::MergeFailure {
            story_id: "x".to_string(),
            reason: "y".to_string(),
        };
        assert!(!needs_pipeline_refresh(&evt));
    }

    #[test]
    fn pipeline_state_converts_to_ws_response() {
        let state = PipelineState {
            backlog: vec![UpcomingStory {
                story_id: "1_story_a".to_string(),
                name: "Story A".to_string(),
                error: None,
                merge_failure: None,
                agent: None,
                pipeline: crate::pipeline_state::Pipeline::Backlog,
                status: crate::pipeline_state::Status::Active,
                review_hold: None,
                qa: None,
                retry_count: None,
                blocked: None,
                frozen: None,
                depends_on: None,
                epic_id: None,
            }],
            current: vec![UpcomingStory {
                story_id: "2_story_b".to_string(),
                name: "Story B".to_string(),
                error: None,
                merge_failure: None,
                agent: None,
                pipeline: crate::pipeline_state::Pipeline::Coding,
                status: crate::pipeline_state::Status::Active,
                review_hold: None,
                qa: None,
                retry_count: None,
                blocked: None,
                frozen: None,
                depends_on: None,
                epic_id: None,
            }],
            qa: vec![],
            merge: vec![],
            done: vec![UpcomingStory {
                story_id: "50_story_done".to_string(),
                name: "Done Story".to_string(),
                error: None,
                merge_failure: None,
                agent: None,
                pipeline: crate::pipeline_state::Pipeline::Done,
                status: crate::pipeline_state::Status::Done,
                review_hold: None,
                qa: None,
                retry_count: None,
                blocked: None,
                frozen: None,
                depends_on: None,
                epic_id: None,
            }],
            closed: vec![],
            archived: vec![],
            deterministic_merges_in_flight: vec![],
        };
        let resp = pipeline_state_to_response(state);
        let json = serde_json::to_value(&resp).unwrap();
        assert_eq!(json["type"], "pipeline_state");
        assert_eq!(json["backlog"].as_array().unwrap().len(), 1);
        assert_eq!(json["backlog"][0]["story_id"], "1_story_a");
        assert_eq!(json["current"].as_array().unwrap().len(), 1);
        assert_eq!(json["current"][0]["story_id"], "2_story_b");
        assert!(json["qa"].as_array().unwrap().is_empty());
        assert!(json["merge"].as_array().unwrap().is_empty());
        assert_eq!(json["done"].as_array().unwrap().len(), 1);
        assert_eq!(json["done"][0]["story_id"], "50_story_done");
    }

    #[test]
    fn empty_pipeline_state_converts_to_ws_response() {
        let state = PipelineState {
            backlog: vec![],
            current: vec![],
            qa: vec![],
            merge: vec![],
            done: vec![],
            closed: vec![],
            archived: vec![],
            deterministic_merges_in_flight: vec![],
        };
        let resp = pipeline_state_to_response(state);
        let json = serde_json::to_value(&resp).unwrap();
        assert_eq!(json["type"], "pipeline_state");
        assert!(json["backlog"].as_array().unwrap().is_empty());
        assert!(json["current"].as_array().unwrap().is_empty());
        assert!(json["qa"].as_array().unwrap().is_empty());
        assert!(json["merge"].as_array().unwrap().is_empty());
        assert!(json["done"].as_array().unwrap().is_empty());
    }

    #[test]
    fn pipeline_state_with_agent_converts_correctly() {
        let state = PipelineState {
            backlog: vec![],
            current: vec![UpcomingStory {
                story_id: "10_story_x".to_string(),
                name: "Story X".to_string(),
                error: None,
                merge_failure: None,
                agent: Some(crate::http::workflow::pipeline::AgentAssignment {
                    agent_name: crate::config::AgentName::Coder1,
                    model: Some(crate::agents::AgentModel::Sonnet),
                    status: crate::agents::AgentStatus::Running,
                }),
                pipeline: crate::pipeline_state::Pipeline::Coding,
                status: crate::pipeline_state::Status::Active,
                review_hold: None,
                qa: None,
                retry_count: None,
                blocked: None,
                frozen: None,
                depends_on: None,
                epic_id: None,
            }],
            qa: vec![],
            merge: vec![],
            done: vec![],
            closed: vec![],
            archived: vec![],
            deterministic_merges_in_flight: vec![],
        };
        let resp: WsResponse = state.into();
        let json = serde_json::to_value(&resp).unwrap();
        assert_eq!(json["current"][0]["agent"]["agent_name"], "coder-1");
        assert_eq!(json["current"][0]["agent"]["model"], "sonnet");
        assert_eq!(json["current"][0]["agent"]["status"], "running");
    }

    #[test]
    fn wizard_steps_to_info_empty() {
        let result = wizard_steps_to_info(&[]);
        assert!(result.is_empty());
    }
}
