//! Freeze and unfreeze work items — CRDT state transitions for pipeline lifecycle control.
//!
//! Both the Matrix bot commands (`freeze`/`unfreeze`) and the MCP tools
//! (`freeze_story`/`unfreeze_story`) delegate here so the CRDT mutation is
//! defined in exactly one place.

/// Outcome of a successful [`freeze`] call.
#[derive(Debug, PartialEq, Eq)]
pub enum FreezeStatus {
    /// The work item was already in the frozen stage; no state change occurred.
    AlreadyFrozen,
    /// The work item was successfully transitioned to the frozen stage.
    Frozen,
}

/// Outcome of a successful [`unfreeze`] call.
#[derive(Debug, PartialEq, Eq)]
pub enum UnfreezeStatus {
    /// The work item was not frozen; no state change occurred.
    NotFrozen,
    /// The work item was successfully unfrozen and restored to its prior stage.
    Unfrozen,
}

/// Freeze a work item, suppressing pipeline advancement and auto-assign.
///
/// Returns [`FreezeStatus::AlreadyFrozen`] if the item is already in the frozen
/// stage without making any CRDT writes.  Returns `Err` if the state transition
/// fails (e.g. the item is not found or is in a terminal stage).
pub fn freeze(story_id: &str) -> Result<FreezeStatus, String> {
    let already_frozen = crate::pipeline_state::read_typed(story_id)
        .ok()
        .flatten()
        .map(|item| matches!(item.stage, crate::pipeline_state::Stage::Frozen { .. }))
        .unwrap_or(false);

    if already_frozen {
        return Ok(FreezeStatus::AlreadyFrozen);
    }

    crate::pipeline_state::transition_to_frozen(story_id)
        .map(|_| FreezeStatus::Frozen)
        .map_err(|e| e.to_string())
}

/// Unfreeze a work item, resuming normal pipeline behaviour.
///
/// Returns [`UnfreezeStatus::NotFrozen`] if the item is not currently frozen.
/// Returns `Err` if the state transition fails.
pub fn unfreeze(story_id: &str) -> Result<UnfreezeStatus, String> {
    let is_frozen = crate::pipeline_state::read_typed(story_id)
        .ok()
        .flatten()
        .map(|item| matches!(item.stage, crate::pipeline_state::Stage::Frozen { .. }))
        .unwrap_or(false);

    if !is_frozen {
        return Ok(UnfreezeStatus::NotFrozen);
    }

    crate::pipeline_state::transition_to_unfrozen(story_id)
        .map(|_| UnfreezeStatus::Unfrozen)
        .map_err(|e| e.to_string())
}

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

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn freeze_transitions_item_to_frozen_stage() {
        crate::crdt_state::init_for_test();
        let story_id = "8780_story_freeze_service_basic";
        crate::db::write_item_with_content(
            story_id,
            "2_current",
            "---\nname: Freeze Service Test\n---\n",
            crate::db::ItemMeta::named("Freeze Service Test"),
        );

        let result = freeze(story_id);
        assert!(
            matches!(result, Ok(FreezeStatus::Frozen)),
            "should return Frozen: {result:?}"
        );

        let item = crate::pipeline_state::read_typed(story_id)
            .expect("read_typed should succeed")
            .expect("item should be present");
        assert!(
            matches!(item.stage, crate::pipeline_state::Stage::Frozen { .. }),
            "stage should be Frozen after freeze: {:?}",
            item.stage
        );
    }

    #[test]
    fn freeze_already_frozen_returns_already_frozen_without_error() {
        crate::crdt_state::init_for_test();
        let story_id = "8781_story_freeze_service_already";
        crate::db::write_item_with_content(
            story_id,
            "2_current",
            "---\nname: Already Frozen\n---\n",
            crate::db::ItemMeta::named("Already Frozen"),
        );

        freeze(story_id).expect("first freeze should succeed");

        let result = freeze(story_id);
        assert!(
            matches!(result, Ok(FreezeStatus::AlreadyFrozen)),
            "second freeze should return AlreadyFrozen: {result:?}"
        );
    }

    #[test]
    fn unfreeze_transitions_item_back_from_frozen() {
        crate::crdt_state::init_for_test();
        let story_id = "8782_story_unfreeze_service_basic";
        crate::db::write_item_with_content(
            story_id,
            "2_current",
            "---\nname: Unfreeze Service Test\n---\n",
            crate::db::ItemMeta::named("Unfreeze Service Test"),
        );

        freeze(story_id).expect("freeze should succeed");

        let result = unfreeze(story_id);
        assert!(
            matches!(result, Ok(UnfreezeStatus::Unfrozen)),
            "should return Unfrozen: {result:?}"
        );

        let item = crate::pipeline_state::read_typed(story_id)
            .expect("read_typed should succeed")
            .expect("item should be present");
        assert!(
            !matches!(item.stage, crate::pipeline_state::Stage::Frozen { .. }),
            "stage should not be Frozen after unfreeze: {:?}",
            item.stage
        );
    }

    #[test]
    fn unfreeze_not_frozen_item_returns_not_frozen() {
        crate::crdt_state::init_for_test();
        let story_id = "8783_story_unfreeze_service_not_frozen";
        crate::db::write_item_with_content(
            story_id,
            "2_current",
            "---\nname: Not Frozen\n---\n",
            crate::db::ItemMeta::named("Not Frozen"),
        );

        let result = unfreeze(story_id);
        assert!(
            matches!(result, Ok(UnfreezeStatus::NotFrozen)),
            "should return NotFrozen for a non-frozen item: {result:?}"
        );
    }

    /// Regression: both the chat command path and the MCP tool path delegate to
    /// `freeze()` / `unfreeze()`, so they must produce identical CRDT state.
    /// This test proves it by calling the service directly (as both handlers do)
    /// and asserting the resulting CRDT stages are both frozen.
    #[test]
    fn freeze_via_chat_and_mcp_paths_yields_identical_crdt_state() {
        crate::crdt_state::init_for_test();

        // Story A simulates the chat command path.
        let story_a = "8784_story_freeze_regression_chat";
        crate::db::write_item_with_content(
            story_a,
            "2_current",
            "---\nname: Regression Chat Path\n---\n",
            crate::db::ItemMeta::named("Regression Chat Path"),
        );

        // Story B simulates the MCP tool path.
        let story_b = "8785_story_freeze_regression_mcp";
        crate::db::write_item_with_content(
            story_b,
            "2_current",
            "---\nname: Regression MCP Path\n---\n",
            crate::db::ItemMeta::named("Regression MCP Path"),
        );

        // Both paths call service::work_item::freeze().
        let result_a = freeze(story_a);
        let result_b = freeze(story_b);

        assert!(
            matches!(result_a, Ok(FreezeStatus::Frozen)),
            "chat-path freeze should succeed: {result_a:?}"
        );
        assert!(
            matches!(result_b, Ok(FreezeStatus::Frozen)),
            "MCP-path freeze should succeed: {result_b:?}"
        );

        let state_a = crate::pipeline_state::read_typed(story_a)
            .expect("read_typed should succeed")
            .expect("chat-path item should be in CRDT");
        let state_b = crate::pipeline_state::read_typed(story_b)
            .expect("read_typed should succeed")
            .expect("MCP-path item should be in CRDT");

        assert!(
            matches!(state_a.stage, crate::pipeline_state::Stage::Frozen { .. }),
            "chat-path CRDT stage must be frozen: {:?}",
            state_a.stage
        );
        assert!(
            matches!(state_b.stage, crate::pipeline_state::Stage::Frozen { .. }),
            "MCP-path CRDT stage must be frozen: {:?}",
            state_b.stage
        );
        // Discriminant comparison: both stages must be the same variant.
        assert_eq!(
            std::mem::discriminant(&state_a.stage),
            std::mem::discriminant(&state_b.stage),
            "chat-path and MCP-path must produce identical CRDT stage variant"
        );
    }
}
