//! Pure agent selection and filtering logic — no I/O, no side effects.
//!
//! All functions in this module are pure: they take data, transform it, and
//! return a result without touching the filesystem, network, or any mutable
//! global state. This makes them fast to test without tempdirs or async runtimes.
use crate::agent_log::LogEntry;
use crate::agents::AgentInfo;

/// Filter a list of agents, removing any whose story is archived.
///
/// `is_archived` is a predicate injected by the caller — typically a closure
/// over the project root that calls `io::is_archived`. This keeps the function
/// pure: it never touches the filesystem itself.
pub fn filter_non_archived<F>(agents: Vec<AgentInfo>, is_archived: F) -> Vec<AgentInfo>
where
    F: Fn(&str) -> bool,
{
    agents
        .into_iter()
        .filter(|info| !is_archived(&info.story_id))
        .collect()
}

/// Concatenate the text of all `output` events from an agent log.
///
/// Non-output events (status, done, error, agent_json, thinking) are silently
/// skipped. Returns an empty string when `entries` is empty or contains no
/// output events.
pub fn collect_output_text(entries: &[LogEntry]) -> String {
    entries
        .iter()
        .filter(|e| e.event.get("type").and_then(|t| t.as_str()) == Some("output"))
        .filter_map(|e| {
            e.event
                .get("text")
                .and_then(|t| t.as_str())
                .map(str::to_owned)
        })
        .collect()
}

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

    fn make_agent(story_id: &str) -> AgentInfo {
        AgentInfo {
            story_id: story_id.to_string(),
            agent_name: "coder-1".to_string(),
            status: AgentStatus::Running,
            session_id: None,
            worktree_path: None,
            base_branch: None,
            completion: None,
            log_session_id: None,
            throttled: false,
            termination_reason: None,
        }
    }

    fn make_log_entry(event_type: &str, text: Option<&str>) -> LogEntry {
        let mut obj = serde_json::Map::new();
        obj.insert(
            "type".to_string(),
            serde_json::Value::String(event_type.to_string()),
        );
        if let Some(t) = text {
            obj.insert("text".to_string(), serde_json::Value::String(t.to_string()));
        }
        LogEntry {
            timestamp: "2024-01-01T00:00:00Z".to_string(),
            event: serde_json::Value::Object(obj),
        }
    }

    // ── filter_non_archived ───────────────────────────────────────────────────

    #[test]
    fn filter_keeps_non_archived_agents() {
        let agents = vec![make_agent("10_active"), make_agent("11_active")];
        let result = filter_non_archived(agents, |_| false);
        assert_eq!(result.len(), 2);
    }

    #[test]
    fn filter_removes_archived_agents() {
        let agents = vec![make_agent("10_archived"), make_agent("11_active")];
        let result = filter_non_archived(agents, |id| id == "10_archived");
        assert_eq!(result.len(), 1);
        assert_eq!(result[0].story_id, "11_active");
    }

    #[test]
    fn filter_removes_all_when_all_archived() {
        let agents = vec![make_agent("10_a"), make_agent("11_b")];
        let result = filter_non_archived(agents, |_| true);
        assert!(result.is_empty());
    }

    #[test]
    fn filter_returns_empty_for_empty_input() {
        let result = filter_non_archived(vec![], |_| false);
        assert!(result.is_empty());
    }

    #[test]
    fn filter_preserves_order() {
        let agents = vec![
            make_agent("1_a"),
            make_agent("2_b"),
            make_agent("3_c"),
            make_agent("4_d"),
        ];
        let result = filter_non_archived(agents, |id| id == "2_b");
        assert_eq!(result.len(), 3);
        assert_eq!(result[0].story_id, "1_a");
        assert_eq!(result[1].story_id, "3_c");
        assert_eq!(result[2].story_id, "4_d");
    }

    // ── collect_output_text ───────────────────────────────────────────────────

    #[test]
    fn collect_output_text_empty_entries() {
        let result = collect_output_text(&[]);
        assert_eq!(result, "");
    }

    #[test]
    fn collect_output_text_skips_non_output_events() {
        let entries = vec![
            make_log_entry("status", Some("running")),
            make_log_entry("done", None),
        ];
        let result = collect_output_text(&entries);
        assert_eq!(result, "");
    }

    #[test]
    fn collect_output_text_concatenates_output_events() {
        let entries = vec![
            make_log_entry("output", Some("Hello ")),
            make_log_entry("output", Some("world\n")),
        ];
        let result = collect_output_text(&entries);
        assert_eq!(result, "Hello world\n");
    }

    #[test]
    fn collect_output_text_skips_output_without_text_field() {
        let entry = LogEntry {
            timestamp: "2024-01-01T00:00:00Z".to_string(),
            event: serde_json::json!({"type": "output"}),
        };
        let result = collect_output_text(&[entry]);
        assert_eq!(result, "");
    }

    #[test]
    fn collect_output_text_mixed_event_types() {
        let entries = vec![
            make_log_entry("status", Some("running")),
            make_log_entry("output", Some("line1\n")),
            make_log_entry("agent_json", None),
            make_log_entry("output", Some("line2\n")),
            make_log_entry("done", None),
        ];
        let result = collect_output_text(&entries);
        assert_eq!(result, "line1\nline2\n");
    }
}
