//! `projects` chat command — list all registered gateway projects.
//!
//! Reads the live `gateway_projects_store` and returns one row per project:
//! name, URL, ssh_port (if set), host_path (if set), and an `[adopted]` or
//! `[built-in]` marker.  Output is alphabetised by project name (BTreeMap
//! order) and the active project is marked with a leading `*`.
//! No liveness checks are performed — this is a pure enumeration.

use crate::chat::transport::matrix::bot::context::BotContext;

/// Run the `projects` command and return a formatted Markdown string.
///
/// Returns a short message when not in gateway mode or when no projects are
/// registered.  The active project is prefixed with `*`; all others with
/// a space.  Rows are alphabetised because the underlying store is a
/// [`BTreeMap`](std::collections::BTreeMap).
pub async fn run_projects_list(ctx: &BotContext) -> String {
    let store = match ctx.gateway_projects_store.as_ref() {
        Some(s) => s,
        None => return "Not running in gateway mode.".to_string(),
    };
    let active = match ctx.gateway_active_project.as_ref() {
        Some(ap) => ap.read().await.clone(),
        None => String::new(),
    };
    let projects = store.read().await;
    if projects.is_empty() {
        return "No projects registered.".to_string();
    }
    let mut lines = Vec::with_capacity(projects.len() + 1);
    lines.push(format!("Projects ({} registered):", projects.len()));
    for (name, entry) in projects.iter() {
        let marker = if *name == active { "*" } else { " " };
        let mut parts = vec![name.clone()];
        if let Some(ref url) = entry.url {
            parts.push(url.clone());
        }
        if let Some(port) = entry.ssh_port {
            parts.push(format!("ssh:{port}"));
        }
        if let Some(ref path) = entry.host_path {
            parts.push(path.clone());
        }
        let adopted = if entry.host_path.is_some() {
            "[adopted]"
        } else {
            "[built-in]"
        };
        parts.push(adopted.to_string());
        lines.push(format!("{marker} {}", parts.join("  ")));
    }
    lines.join("\n")
}

// ── Tests ─────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;
    use crate::service::gateway::config::ProjectEntry;
    use std::collections::{BTreeMap, HashMap, HashSet};
    use std::sync::Arc;
    use std::sync::atomic::AtomicI64;
    use tokio::sync::Mutex as TokioMutex;
    use tokio::sync::RwLock;

    async fn run_with_projects(projects: BTreeMap<String, ProjectEntry>, active: &str) -> String {
        let services = crate::services::Services::new_test(
            std::path::PathBuf::from("/tmp"),
            "bot".to_string(),
        );
        let store = Arc::new(RwLock::new(projects));
        let active_project = Arc::new(RwLock::new(active.to_string()));
        let ctx = BotContext {
            services,
            matrix_user_id: "@bot:example.com".parse().unwrap(),
            target_room_ids: vec![],
            allowed_users: vec![],
            history: Arc::new(TokioMutex::new(HashMap::new())),
            history_size: 20,
            bot_sent_event_ids: Arc::new(TokioMutex::new(HashSet::new())),
            htop_sessions: Arc::new(TokioMutex::new(HashMap::new())),
            transport: Arc::new(crate::chat::transport::whatsapp::WhatsAppTransport::new(
                "test-phone".to_string(),
                "test-token".to_string(),
                "pipeline_notification".to_string(),
            )),
            timer_store: Arc::new(crate::service::timer::TimerStore::load(
                std::path::PathBuf::from("/tmp/timers-projects.json"),
            )),
            gateway_active_project: Some(active_project),
            gateway_projects_store: Some(store),
            gateway_channels_store: None,
            handled_incoming_event_ids: Arc::new(TokioMutex::new(
                crate::chat::transport::matrix::bot::context::SeenEventIds::new(
                    crate::chat::transport::matrix::bot::context::SEEN_EVENT_IDS_CAP,
                ),
            )),
            gateway_port: None,
            last_matrix_event_ms: Arc::new(AtomicI64::new(chrono::Utc::now().timestamp_millis())),
            model: None,
            compact_seed_max_bytes: 8_000,
            cache_read_suggest_threshold: 50_000,
            compact_suggest_cooldown_secs: 3_600,
        };
        run_projects_list(&ctx).await
    }

    #[tokio::test]
    async fn four_projects_all_names_appear_exactly_once() {
        let mut projects = BTreeMap::new();
        projects.insert("alpha".into(), ProjectEntry::with_url("http://sled-a:3001"));
        projects.insert("beta".into(), ProjectEntry::with_url("http://sled-b:3002"));
        projects.insert(
            "gamma".into(),
            ProjectEntry {
                url: Some("http://sled-c:3003".into()),
                auth_token: None,
                ssh_port: Some(2203),
                host_path: Some("/home/user/workspace".into()),
                expected_node_id: None,
            },
        );
        projects.insert(
            "delta".into(),
            ProjectEntry {
                url: None,
                auth_token: Some("tok".into()),
                ssh_port: None,
                host_path: None,
                expected_node_id: None,
            },
        );

        let response = run_with_projects(projects, "alpha").await;
        for name in &["alpha", "beta", "gamma", "delta"] {
            let count = response.matches(name).count();
            assert_eq!(
                count, 1,
                "project '{name}' should appear exactly once, response:\n{response}"
            );
        }
    }

    #[tokio::test]
    async fn active_project_marked_with_star() {
        let mut projects = BTreeMap::new();
        projects.insert("proj-a".into(), ProjectEntry::with_url("http://a:3001"));
        projects.insert("proj-b".into(), ProjectEntry::with_url("http://b:3002"));

        let response = run_with_projects(projects, "proj-b").await;
        let active_line = response.lines().find(|l| l.contains("proj-b")).unwrap();
        assert!(
            active_line.starts_with('*'),
            "active project line should start with '*': {active_line}"
        );
        let inactive_line = response.lines().find(|l| l.contains("proj-a")).unwrap();
        assert!(
            !inactive_line.starts_with('*'),
            "inactive project line should not start with '*': {inactive_line}"
        );
    }

    #[tokio::test]
    async fn adopted_project_shows_marker() {
        let mut projects = BTreeMap::new();
        projects.insert(
            "adopted".into(),
            ProjectEntry {
                url: Some("http://a:3001".into()),
                auth_token: None,
                ssh_port: None,
                host_path: Some("/home/user/adopted".into()),
                expected_node_id: None,
            },
        );
        projects.insert("builtin".into(), ProjectEntry::with_url("http://b:3002"));

        let response = run_with_projects(projects, "adopted").await;
        assert!(
            response.contains("[adopted]"),
            "adopted project should be marked: {response}"
        );
        assert!(
            response.contains("[built-in]"),
            "built-in project should be marked: {response}"
        );
    }

    #[tokio::test]
    async fn non_gateway_returns_not_in_gateway_mode() {
        let services = crate::services::Services::new_test(
            std::path::PathBuf::from("/tmp"),
            "bot".to_string(),
        );
        let ctx = BotContext {
            services,
            matrix_user_id: "@bot:example.com".parse().unwrap(),
            target_room_ids: vec![],
            allowed_users: vec![],
            history: Arc::new(TokioMutex::new(HashMap::new())),
            history_size: 20,
            bot_sent_event_ids: Arc::new(TokioMutex::new(HashSet::new())),
            htop_sessions: Arc::new(TokioMutex::new(HashMap::new())),
            transport: Arc::new(crate::chat::transport::whatsapp::WhatsAppTransport::new(
                "test-phone".to_string(),
                "test-token".to_string(),
                "pipeline_notification".to_string(),
            )),
            timer_store: Arc::new(crate::service::timer::TimerStore::load(
                std::path::PathBuf::from("/tmp/timers-projects2.json"),
            )),
            gateway_active_project: None,
            gateway_projects_store: None,
            gateway_channels_store: None,
            handled_incoming_event_ids: Arc::new(TokioMutex::new(
                crate::chat::transport::matrix::bot::context::SeenEventIds::new(
                    crate::chat::transport::matrix::bot::context::SEEN_EVENT_IDS_CAP,
                ),
            )),
            gateway_port: None,
            last_matrix_event_ms: Arc::new(AtomicI64::new(chrono::Utc::now().timestamp_millis())),
            model: None,
            compact_seed_max_bytes: 8_000,
            cache_read_suggest_threshold: 50_000,
            compact_suggest_cooldown_secs: 3_600,
        };
        let response = run_projects_list(&ctx).await;
        assert!(
            response.contains("Not running in gateway mode"),
            "should indicate not in gateway mode: {response}"
        );
    }

    #[test]
    fn projects_command_registered_in_commands() {
        let cmds = crate::chat::commands::commands();
        assert!(
            cmds.iter().any(|c| c.name == "projects"),
            "projects must be registered in commands()"
        );
    }
}
