//! Handler for the `move` command.
//!
//! `{bot_name} move {number} {stage}` finds the work item by number across all
//! pipeline stages, moves it to the specified stage, and returns a confirmation
//! with the story title, old stage, and new stage.

use super::CommandContext;
use crate::agents::move_story_to_stage;

/// Valid stage names accepted by the move command.
const VALID_STAGES: &[&str] = &["backlog", "current", "qa", "merge", "done"];

/// All pipeline stage directories to search when finding a work item by number.
const SEARCH_DIRS: &[&str] = &[
    "1_backlog",
    "2_current",
    "3_qa",
    "4_merge",
    "5_done",
    "6_archived",
];

/// Handle the `move` command.
///
/// Parses `<number> <stage>` from `ctx.args`, locates the work item by its
/// numeric prefix, moves it to the target stage using the shared lifecycle
/// function, and returns a Markdown confirmation string.
pub(super) fn handle_move(ctx: &CommandContext) -> Option<String> {
    let args = ctx.args.trim();

    // Parse `number stage` from args.
    let (num_str, stage_raw) = match args.split_once(char::is_whitespace) {
        Some((n, s)) => (n.trim(), s.trim()),
        None => {
            return Some(format!(
                "Usage: `{} move <number> <stage>`\n\nValid stages: {}",
                ctx.bot_name,
                VALID_STAGES.join(", ")
            ));
        }
    };

    if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) {
        return Some(format!(
            "Invalid story number: `{num_str}`. Usage: `{} move <number> <stage>`",
            ctx.bot_name
        ));
    }

    let target_stage = stage_raw.to_ascii_lowercase();
    if !VALID_STAGES.contains(&target_stage.as_str()) {
        return Some(format!(
            "Invalid stage: `{stage_raw}`. Valid stages: {}",
            VALID_STAGES.join(", ")
        ));
    }

    // Find the story file across all pipeline stages by numeric prefix.
    let mut found_story_id: Option<String> = None;
    let mut found_name: Option<String> = None;

    'outer: for stage_dir in SEARCH_DIRS {
        let dir = ctx
            .project_root
            .join(".storkit")
            .join("work")
            .join(stage_dir);
        if !dir.exists() {
            continue;
        }
        if let Ok(entries) = std::fs::read_dir(&dir) {
            for entry in entries.flatten() {
                let path = entry.path();
                if path.extension().and_then(|e| e.to_str()) != Some("md") {
                    continue;
                }
                if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
                    let file_num = stem
                        .split('_')
                        .next()
                        .filter(|s| !s.is_empty() && s.chars().all(|c| c.is_ascii_digit()))
                        .unwrap_or("");
                    if file_num == num_str {
                        found_story_id = Some(stem.to_string());
                        found_name = std::fs::read_to_string(&path)
                            .ok()
                            .and_then(|contents| {
                                crate::io::story_metadata::parse_front_matter(&contents)
                                    .ok()
                                    .and_then(|m| m.name)
                            });
                        break 'outer;
                    }
                }
            }
        }
    }

    let story_id = match found_story_id {
        Some(id) => id,
        None => {
            return Some(format!(
                "No story, bug, or spike with number **{num_str}** found."
            ));
        }
    };

    let display_name = found_name.as_deref().unwrap_or(&story_id);

    match move_story_to_stage(ctx.project_root, &story_id, &target_stage) {
        Ok((from_stage, to_stage)) => Some(format!(
            "Moved **{display_name}** from **{from_stage}** to **{to_stage}**."
        )),
        Err(e) => Some(format!("Failed to move story {num_str}: {e}")),
    }
}

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

#[cfg(test)]
mod tests {
    use crate::agents::AgentPool;
    use std::collections::HashSet;
    use std::sync::{Arc, Mutex};

    use super::super::{CommandDispatch, try_handle_command};

    fn move_cmd_with_root(root: &std::path::Path, args: &str) -> Option<String> {
        let agents = Arc::new(AgentPool::new_test(3000));
        let ambient_rooms = Arc::new(Mutex::new(HashSet::new()));
        let room_id = "!test:example.com".to_string();
        let dispatch = CommandDispatch {
            bot_name: "Timmy",
            bot_user_id: "@timmy:homeserver.local",
            project_root: root,
            agents: &agents,
            ambient_rooms: &ambient_rooms,
            room_id: &room_id,
        };
        try_handle_command(&dispatch, &format!("@timmy move {args}"))
    }

    fn write_story_file(root: &std::path::Path, stage: &str, filename: &str, content: &str) {
        let dir = root.join(".storkit/work").join(stage);
        std::fs::create_dir_all(&dir).unwrap();
        std::fs::write(dir.join(filename), content).unwrap();
    }

    #[test]
    fn move_command_is_registered() {
        use super::super::commands;
        let found = commands().iter().any(|c| c.name == "move");
        assert!(found, "move command must be in the registry");
    }

    #[test]
    fn move_command_appears_in_help() {
        let result = super::super::tests::try_cmd_addressed(
            "Timmy",
            "@timmy:homeserver.local",
            "@timmy help",
        );
        let output = result.unwrap();
        assert!(
            output.contains("move"),
            "help should list move command: {output}"
        );
    }

    #[test]
    fn move_command_no_args_returns_usage() {
        let tmp = tempfile::TempDir::new().unwrap();
        let output = move_cmd_with_root(tmp.path(), "").unwrap();
        assert!(
            output.contains("Usage"),
            "no args should show usage hint: {output}"
        );
    }

    #[test]
    fn move_command_missing_stage_returns_usage() {
        let tmp = tempfile::TempDir::new().unwrap();
        let output = move_cmd_with_root(tmp.path(), "42").unwrap();
        assert!(
            output.contains("Usage"),
            "missing stage should show usage hint: {output}"
        );
    }

    #[test]
    fn move_command_invalid_stage_returns_error() {
        let tmp = tempfile::TempDir::new().unwrap();
        let output = move_cmd_with_root(tmp.path(), "42 invalid_stage").unwrap();
        assert!(
            output.contains("Invalid stage"),
            "invalid stage should return error: {output}"
        );
    }

    #[test]
    fn move_command_non_numeric_number_returns_error() {
        let tmp = tempfile::TempDir::new().unwrap();
        let output = move_cmd_with_root(tmp.path(), "abc current").unwrap();
        assert!(
            output.contains("Invalid story number"),
            "non-numeric number should return error: {output}"
        );
    }

    #[test]
    fn move_command_not_found_returns_friendly_message() {
        let tmp = tempfile::TempDir::new().unwrap();
        let output = move_cmd_with_root(tmp.path(), "999 current").unwrap();
        assert!(
            output.contains("999") && output.contains("found"),
            "not-found message should include number and 'found': {output}"
        );
    }

    #[test]
    fn move_command_moves_story_and_confirms() {
        let tmp = tempfile::TempDir::new().unwrap();
        write_story_file(
            tmp.path(),
            "1_backlog",
            "42_story_some_feature.md",
            "---\nname: Some Feature\n---\n\n# Story 42\n",
        );

        let output = move_cmd_with_root(tmp.path(), "42 current").unwrap();
        assert!(
            output.contains("Some Feature"),
            "confirmation should include story name: {output}"
        );
        assert!(
            output.contains("backlog"),
            "confirmation should include old stage: {output}"
        );
        assert!(
            output.contains("current"),
            "confirmation should include new stage: {output}"
        );

        // Verify the file was actually moved.
        let new_path = tmp
            .path()
            .join(".storkit/work/2_current/42_story_some_feature.md");
        assert!(new_path.exists(), "story file should be in 2_current/");
    }

    #[test]
    fn move_command_case_insensitive_stage() {
        let tmp = tempfile::TempDir::new().unwrap();
        write_story_file(
            tmp.path(),
            "2_current",
            "10_story_test.md",
            "---\nname: Test\n---\n",
        );
        let output = move_cmd_with_root(tmp.path(), "10 BACKLOG").unwrap();
        assert!(
            output.contains("Test") && output.contains("backlog"),
            "stage matching should be case-insensitive: {output}"
        );
    }

    #[test]
    fn move_command_idempotent_when_already_in_target() {
        let tmp = tempfile::TempDir::new().unwrap();
        write_story_file(
            tmp.path(),
            "2_current",
            "5_story_already_current.md",
            "---\nname: Already Current\n---\n",
        );
        // Moving to the stage it's already in should return a success message.
        let output = move_cmd_with_root(tmp.path(), "5 current").unwrap();
        assert!(
            output.contains("Moved") || output.contains("current"),
            "idempotent move should succeed: {output}"
        );
    }

    #[test]
    fn move_command_case_insensitive_command() {
        let result = super::super::tests::try_cmd_addressed(
            "Timmy",
            "@timmy:homeserver.local",
            "@timmy MOVE 1 backlog",
        );
        // Returns Some (the registry matched, regardless of result content)
        assert!(result.is_some(), "MOVE should match case-insensitively");
    }
}
