//! Assign-and-start: unified service for pinning an agent to a story and starting it.
//!
//! Both the Matrix `assign` command and the MCP `start_agent` tool delegate
//! here when an explicit `agent_name` is provided, ensuring that the CRDT
//! `agent` register is always written before the agent is spawned.  This
//! eliminates the asymmetry where the chat path wrote YAML + CRDT while the
//! MCP path only called `start_agent`.

use crate::agents::{AgentInfo, AgentPool};
use std::path::Path;

/// Pin an agent to a story in the CRDT and immediately start it.
///
/// Writes `agent_name` into the typed CRDT `agent` register for `story_id`,
/// then delegates to [`AgentPool::start_agent`] with the explicit name.
///
/// Callers that need to stop an already-running agent first (e.g. the Matrix
/// `assign` command replacing a running coder) must do so before calling this
/// function; the pool's concurrency guard will reject a duplicate start.
///
/// Returns the [`AgentInfo`] for the newly started agent, or an error string.
pub async fn assign_and_start(
    story_id: &str,
    agent_name: &str,
    project_root: &Path,
    agents: &AgentPool,
) -> Result<AgentInfo, String> {
    crate::crdt_state::set_agent(
        story_id,
        agent_name.parse::<crate::config::AgentName>().ok(),
    );
    agents
        .start_agent(project_root, story_id, Some(agent_name), None, None)
        .await
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::agents::AgentPool;
    use std::sync::Arc;

    fn make_agents() -> Arc<AgentPool> {
        Arc::new(AgentPool::new_test(3000))
    }

    /// Regression test (story 877, AC4): assigning via the Matrix chat path
    /// and via the MCP `start_agent` path both call `assign_and_start`, which
    /// writes the typed CRDT `agent` register before spawning.  This test
    /// verifies the CRDT write is unconditional even when the pool call fails
    /// (no git repo in the test environment).
    #[tokio::test]
    async fn assign_and_start_writes_crdt_agent_register_before_pool_call() {
        crate::crdt_state::init_for_test();
        let story_id = "8770_story_assign_regression_crdt";

        // Seed the CRDT so set_agent can find the item.
        crate::crdt_state::write_item_str(
            story_id,
            "2_current",
            Some("Assign Regression"),
            None,
            None,
            None,
        );

        let tmp = tempfile::tempdir().unwrap();
        let agents = make_agents();

        // The pool call will fail (no git repo / project config), but the CRDT
        // write must succeed before it is attempted.
        let _ = assign_and_start(story_id, "coder-opus", tmp.path(), &agents).await;

        let dump = crate::crdt_state::dump_crdt_state(Some(story_id));
        let item = dump
            .items
            .iter()
            .find(|i| i.story_id.as_deref() == Some(story_id))
            .expect("item must be in CRDT");
        assert_eq!(
            item.agent.as_deref(),
            Some("coder-opus"),
            "CRDT agent register must be set to coder-opus after assign_and_start"
        );
    }

    /// Both paths (Matrix and MCP) that call `assign_and_start` leave the same
    /// CRDT register value, regardless of which path is used.
    #[tokio::test]
    async fn assign_and_start_same_crdt_state_from_both_paths() {
        crate::crdt_state::init_for_test();
        let story_id_a = "8771_story_assign_path_a";
        let story_id_b = "8772_story_assign_path_b";

        for sid in &[story_id_a, story_id_b] {
            crate::crdt_state::write_item_str(
                sid,
                "2_current",
                Some("Path Test"),
                None,
                None,
                None,
            );
        }

        let tmp = tempfile::tempdir().unwrap();
        let agents = make_agents();

        // Simulate Matrix path: call assign_and_start directly (as handle_assign does).
        let _ = assign_and_start(story_id_a, "coder-opus", tmp.path(), &agents).await;

        // Simulate MCP path: call assign_and_start directly (as tool_start_agent does).
        let _ = assign_and_start(story_id_b, "coder-opus", tmp.path(), &agents).await;

        // Both must leave the same CRDT agent register value.
        for sid in &[story_id_a, story_id_b] {
            let dump = crate::crdt_state::dump_crdt_state(Some(sid));
            let item = dump
                .items
                .iter()
                .find(|i| i.story_id.as_deref() == Some(*sid))
                .expect("item must be in CRDT");
            assert_eq!(
                item.agent.as_deref(),
                Some("coder-opus"),
                "CRDT agent register must match for story {sid}"
            );
        }
    }
}
