//! Handler for the `convert` chat command (story 1141).
//!
//! `convert <number> <type>` changes the item-type register of a work item
//! in place.  All other CRDT registers (ACs, epic, name, stage, …) are
//! untouched.  Rejected for archived items.

use super::CommandContext;

/// Handle the `convert` command.
///
/// Parses `<number> <type>` from `ctx.args` and delegates to
/// [`convert_by_number`].  Returns `None` (route to LLM) when args do not
/// look like a numeric ID followed by a type keyword.
pub(super) fn handle_convert(ctx: &CommandContext) -> Option<String> {
    let args = ctx.args.trim();
    let (num_str, type_str) = args.split_once(char::is_whitespace)?;
    let num_str = num_str.trim();
    let type_str = type_str.trim();

    // Route to LLM if the first token is not a bare number.
    if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) {
        return None;
    }
    // Route to LLM if the type looks like natural language (contains spaces).
    if type_str.is_empty() || type_str.contains(char::is_whitespace) {
        return None;
    }

    Some(convert_by_number(ctx.effective_root(), num_str, type_str))
}

/// Core convert logic: find item by numeric prefix and change its type.
///
/// Returns a Markdown-formatted response suitable for all chat transports.
pub(crate) fn convert_by_number(
    project_root: &std::path::Path,
    story_number: &str,
    new_type_str: &str,
) -> String {
    let Some(new_type) = crate::io::story_metadata::ItemType::from_str(new_type_str) else {
        return format!(
            "Unknown type **{new_type_str}**. Accepted types: story, bug, spike, refactor, epic."
        );
    };

    let (story_id, _, _, _) =
        match crate::chat::lookup::find_story_by_number(project_root, story_number) {
            Some(found) => found,
            None => {
                return format!(
                    "No story, bug, spike, or refactor with number **{story_number}** found."
                );
            }
        };

    let item = match crate::crdt_state::read_item(&story_id) {
        Some(i) => i,
        None => {
            return format!("Work item **{story_number}** ({story_id}) not found in CRDT.");
        }
    };

    if matches!(item.stage(), crate::pipeline_state::Stage::Archived { .. }) {
        return format!(
            "Cannot convert **{story_id}**: type change on an archived item is not allowed."
        );
    }

    let old_type = item.item_type().map(|t| t.as_str()).unwrap_or("(inferred)");
    let story_name = item.name().to_string();
    let new_type_s = new_type.as_str();

    if !crate::crdt_state::set_item_type(&story_id, Some(new_type)) {
        return format!("Failed to convert **{story_id}**: CRDT write rejected.");
    }

    format!("Converted **{story_name}** ({story_id}) from type `{old_type}` to `{new_type_s}`.")
}

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

#[cfg(test)]
mod tests {
    use super::super::{CommandDispatch, try_handle_command};

    fn convert_cmd(root: &std::path::Path, args: &str) -> Option<String> {
        let services = crate::services::Services::new_test(root.to_path_buf(), "Timmy".to_string());
        let room_id = "!test:example.com".to_string();
        let dispatch = CommandDispatch {
            services: &services,
            project_root: &services.project_root,
            bot_user_id: "@timmy:homeserver.local",
            room_id: &room_id,
        };
        try_handle_command(&dispatch, &format!("@timmy convert {args}"))
    }

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

    #[test]
    fn convert_no_args_routes_to_llm() {
        let tmp = tempfile::TempDir::new().unwrap();
        let result = convert_cmd(tmp.path(), "");
        assert!(result.is_none(), "no args should route to LLM: {result:?}");
    }

    #[test]
    fn convert_natural_language_routes_to_llm() {
        let tmp = tempfile::TempDir::new().unwrap();
        let result = convert_cmd(tmp.path(), "the login bug to a story");
        assert!(
            result.is_none(),
            "natural-language args should route to LLM: {result:?}"
        );
    }

    #[test]
    fn convert_well_formed_runs_handler() {
        let tmp = tempfile::TempDir::new().unwrap();
        let result = convert_cmd(tmp.path(), "999 story");
        assert!(
            result.is_some(),
            "well-formed args should run the handler: {result:?}"
        );
    }

    #[test]
    fn convert_invalid_type_returns_error() {
        let tmp = tempfile::TempDir::new().unwrap();
        let result = convert_cmd(tmp.path(), "999 banana").unwrap();
        assert!(
            result.contains("Unknown type") || result.contains("banana"),
            "unknown type should show error: {result}"
        );
    }

    #[test]
    fn convert_not_found_returns_error() {
        let tmp = tempfile::TempDir::new().unwrap();
        let result = convert_cmd(tmp.path(), "9988 story").unwrap();
        assert!(
            result.contains("9988") && result.contains("found"),
            "not-found message should include number and 'found': {result}"
        );
    }

    #[test]
    fn convert_changes_item_type_in_crdt() {
        let tmp = tempfile::TempDir::new().unwrap();
        crate::crdt_state::init_for_test();
        crate::db::ensure_content_store();
        crate::chat::test_helpers::write_story_file(
            tmp.path(),
            "backlog",
            "9120_spike_convert_chat.md",
            "# Spike\n",
            Some("Convert Chat Test"),
        );
        crate::crdt_state::set_item_type(
            "9120_spike_convert_chat",
            Some(crate::io::story_metadata::ItemType::Spike),
        );

        let result = convert_cmd(tmp.path(), "9120 story").unwrap();
        assert!(
            result.contains("story") || result.contains("Converted"),
            "should confirm conversion: {result}"
        );

        let item =
            crate::crdt_state::read_item("9120_spike_convert_chat").expect("item should exist");
        assert_eq!(
            item.item_type(),
            Some(crate::io::story_metadata::ItemType::Story),
            "item_type should be Story after conversion: {:?}",
            item.item_type()
        );
    }
}
