//! MCP tool for converting a work item's type in place (story 1141).
//!
//! `convert_item_type` changes the type register of an existing CRDT item
//! from any value to another (story ↔ bug ↔ spike ↔ refactor) without
//! touching the story_id, ACs, epic association, or any other register.

use crate::http::context::AppContext;
use crate::pipeline_state::Stage;
use serde_json::Value;

/// Convert a work item's type in the CRDT.
///
/// Accepts `story_id` (full filename stem, e.g. `"42_spike_my_spike"`) and
/// `new_type` (one of `"story"`, `"bug"`, `"spike"`, `"refactor"`, `"epic"`).
/// Returns an error when the item does not exist or is in the `Archived` stage.
pub(crate) fn tool_convert_item_type(args: &Value, _ctx: &AppContext) -> Result<String, String> {
    let req = crate::validation::ConvertItemTypeRequest::from_json(args)?;
    let story_id = req.story_id.as_str();

    let item = crate::crdt_state::read_item(story_id)
        .ok_or_else(|| format!("Work item '{story_id}' not found in CRDT."))?;

    if matches!(item.stage(), Stage::Archived { .. }) {
        return Err(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 new_type_str = req.new_type.as_str();

    if !crate::crdt_state::set_item_type(story_id, Some(req.new_type)) {
        return Err(format!(
            "Failed to update item type for '{story_id}': CRDT write was rejected."
        ));
    }

    Ok(format!(
        "Converted '{story_id}' from type '{old_type}' to '{new_type_str}'."
    ))
}

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

#[cfg(test)]
mod tests {
    use super::*;
    use crate::http::test_helpers::test_ctx;
    use crate::io::story_metadata::ItemType;
    use serde_json::json;

    fn make_spike(spike_id: &str) {
        crate::crdt_state::init_for_test();
        crate::db::ensure_content_store();
        crate::db::write_item_with_content(
            spike_id,
            "backlog",
            "---\nname: Test Spike\n---\n",
            crate::db::ItemMeta::named("Test Spike"),
        );
    }

    #[test]
    fn converts_spike_to_story_and_preserves_epic() {
        crate::crdt_state::init_for_test();
        let spike_id = "9111_spike_convert_regression";
        make_spike(spike_id);

        // Attach an epic.
        crate::crdt_state::set_item_type(spike_id, Some(ItemType::Spike));
        crate::crdt_state::set_epic(spike_id, crate::crdt_state::EpicId::from_crdt_str("9000"));

        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());

        // (i) Convert spike → story.
        let result =
            tool_convert_item_type(&json!({"story_id": spike_id, "new_type": "story"}), &ctx);
        assert!(result.is_ok(), "convert should succeed: {result:?}");
        assert!(
            result.unwrap().contains("story"),
            "response should mention new type"
        );

        // (i) Verify type is now Story in CRDT.
        let item = crate::crdt_state::read_item(spike_id).expect("item must exist");
        assert_eq!(
            item.item_type(),
            Some(ItemType::Story),
            "item_type should be Story after conversion"
        );

        // (ii) Verify the conversion is visible in dump_crdt.
        let dump = crate::crdt_state::dump_crdt_state(Some(spike_id));
        let found = dump
            .items
            .iter()
            .any(|i| i.item_type.as_deref() == Some("story") && !i.is_deleted);
        assert!(
            found,
            "dump_crdt should show item_type='story' after conversion"
        );

        // (iii) Epic association is preserved.
        assert_eq!(
            item.epic(),
            crate::crdt_state::EpicId::from_crdt_str("9000"),
            "epic should be unchanged after type conversion"
        );
    }

    #[test]
    fn rejects_missing_story_id() {
        crate::crdt_state::init_for_test();
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let err = tool_convert_item_type(&json!({"new_type": "story"}), &ctx).unwrap_err();
        assert!(
            err.contains("story_id"),
            "error should mention story_id: {err}"
        );
    }

    #[test]
    fn rejects_invalid_new_type() {
        crate::crdt_state::init_for_test();
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let err = tool_convert_item_type(
            &json!({"story_id": "9112_spike_foo", "new_type": "banana"}),
            &ctx,
        )
        .unwrap_err();
        assert!(
            err.contains("new_type") || err.contains("InvalidValue"),
            "error should mention new_type: {err}"
        );
    }

    #[test]
    fn rejects_nonexistent_item() {
        crate::crdt_state::init_for_test();
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let err = tool_convert_item_type(
            &json!({"story_id": "9999_spike_not_real", "new_type": "story"}),
            &ctx,
        )
        .unwrap_err();
        assert!(
            err.contains("not found"),
            "error should say not found: {err}"
        );
    }

    #[test]
    fn rejects_archived_item() {
        crate::crdt_state::init_for_test();
        let spike_id = "9113_spike_archived_convert";
        crate::db::ensure_content_store();
        crate::db::write_item_with_content(
            spike_id,
            "archived",
            "---\nname: Archived Spike\n---\n",
            crate::db::ItemMeta::named("Archived Spike"),
        );
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let err = tool_convert_item_type(&json!({"story_id": spike_id, "new_type": "story"}), &ctx)
            .unwrap_err();
        assert!(
            err.contains("archived"),
            "error should mention archived: {err}"
        );
    }
}
