//! Story acceptance and full-cleanup deletion tools.

use crate::agents::{feature_branch_has_unmerged_changes, move_story_to_done};
use crate::http::context::AppContext;
use crate::pipeline_state::{Stage, read_typed};
use serde_json::Value;

/// Accept a story: move it to Done and clean up its agent pool entries.
pub(crate) async fn tool_accept_story(args: &Value, ctx: &AppContext) -> Result<String, String> {
    let story_id = args
        .get("story_id")
        .and_then(|v| v.as_str())
        .ok_or("Missing required argument: story_id")?;

    let project_root = ctx.services.agents.get_project_root(&ctx.state)?;

    // Determine whether the story is in MergeFailure (manual-recovery path).
    // For MergeFailure, the feature branch deliberately has unmerged code — the
    // operator is accepting the story without a clean merge, so skip the check.
    let in_merge_failure = read_typed(story_id)
        .ok()
        .flatten()
        .map(|item| matches!(item.stage, Stage::MergeFailure { .. }))
        .unwrap_or(false);

    // Bug 226: Refuse to accept if the feature branch has unmerged code.
    // The code must be squash-merged via merge_agent_work first.
    if !in_merge_failure && feature_branch_has_unmerged_changes(&project_root, story_id) {
        return Err(format!(
            "Cannot accept story '{story_id}': feature branch 'feature/story-{story_id}' \
             has unmerged changes. Use merge_agent_work to squash-merge the code into \
             master first."
        ));
    }

    move_story_to_done(story_id)?;
    ctx.services.agents.remove_agents_for_story(story_id).await;

    Ok(format!(
        "Story '{story_id}' accepted, moved to done/, and committed to master."
    ))
}

pub(crate) async fn tool_delete_story(args: &Value, ctx: &AppContext) -> Result<String, String> {
    let story_id = args
        .get("story_id")
        .and_then(|v| v.as_str())
        .ok_or("Missing required argument: story_id")?;

    let project_root = ctx.services.agents.get_project_root(&ctx.state)?;

    let outcome = crate::service::work_item::delete::delete_work_item(
        story_id,
        &project_root,
        &ctx.services.agents,
        Some(&ctx.timer_store),
    )
    .await?;

    if !outcome.failed_steps.is_empty() {
        return Err(format!(
            "Story '{story_id}' partially deleted. Failed steps: {}.",
            outcome.failed_steps.join("; ")
        ));
    }

    Ok(format!("Story '{story_id}' deleted from pipeline."))
}

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

    fn setup_git_repo_in(dir: &std::path::Path) {
        std::process::Command::new("git")
            .args(["init"])
            .current_dir(dir)
            .output()
            .unwrap();
        std::process::Command::new("git")
            .args(["config", "user.email", "test@test.com"])
            .current_dir(dir)
            .output()
            .unwrap();
        std::process::Command::new("git")
            .args(["config", "user.name", "Test"])
            .current_dir(dir)
            .output()
            .unwrap();
        std::process::Command::new("git")
            .args(["commit", "--allow-empty", "-m", "init"])
            .current_dir(dir)
            .output()
            .unwrap();
    }

    #[tokio::test]
    async fn tool_delete_story_missing_story_id() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_delete_story(&json!({}), &ctx).await;
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("story_id"));
    }

    #[tokio::test]
    async fn tool_delete_story_not_found_returns_error() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_delete_story(&json!({"story_id": "99_nonexistent"}), &ctx).await;
        assert!(result.is_err());
        assert!(
            result
                .unwrap_err()
                .contains("not found in any pipeline stage")
        );
    }

    #[tokio::test]
    async fn tool_delete_story_deletes_file_from_backlog() {
        let tmp = tempfile::tempdir().unwrap();
        let backlog = tmp.path().join(".huskies/work/1_backlog");
        fs::create_dir_all(&backlog).unwrap();
        let story_file = backlog.join("10_story_cleanup.md");
        fs::write(&story_file, "---\nname: Cleanup\n---\n").unwrap();

        let ctx = test_ctx(tmp.path());
        let result = tool_delete_story(&json!({"story_id": "10_story_cleanup"}), &ctx).await;
        assert!(result.is_ok(), "expected ok: {result:?}");
        assert!(!story_file.exists(), "story file should be deleted");
    }

    #[tokio::test]
    async fn tool_delete_story_deletes_file_from_current() {
        let tmp = tempfile::tempdir().unwrap();
        let current = tmp.path().join(".huskies/work/2_current");
        fs::create_dir_all(&current).unwrap();
        let story_file = current.join("11_story_active.md");
        fs::write(&story_file, "---\nname: Active\n---\n").unwrap();

        let ctx = test_ctx(tmp.path());
        let result = tool_delete_story(&json!({"story_id": "11_story_active"}), &ctx).await;
        assert!(result.is_ok(), "expected ok: {result:?}");
        assert!(!story_file.exists(), "story file should be deleted");
    }

    #[tokio::test]
    async fn tool_accept_story_missing_story_id() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_accept_story(&json!({}), &ctx).await;
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("story_id"));
    }

    #[tokio::test]
    async fn tool_accept_story_nonexistent_story_returns_error() {
        let tmp = tempfile::tempdir().unwrap();
        setup_git_repo_in(tmp.path());
        let ctx = test_ctx(tmp.path());
        // No story file in current/ — should fail
        let result = tool_accept_story(&json!({"story_id": "99_nonexistent"}), &ctx).await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn tool_accept_story_refuses_when_feature_branch_has_unmerged_code() {
        let tmp = tempfile::tempdir().unwrap();
        setup_git_repo_in(tmp.path());

        // Create a feature branch with code changes.
        std::process::Command::new("git")
            .args(["checkout", "-b", "feature/story-50_story_test"])
            .current_dir(tmp.path())
            .output()
            .unwrap();
        std::fs::write(tmp.path().join("feature.rs"), "fn main() {}").unwrap();
        std::process::Command::new("git")
            .args(["add", "."])
            .current_dir(tmp.path())
            .output()
            .unwrap();
        std::process::Command::new("git")
            .args(["commit", "-m", "add feature"])
            .current_dir(tmp.path())
            .output()
            .unwrap();
        std::process::Command::new("git")
            .args(["checkout", "master"])
            .current_dir(tmp.path())
            .output()
            .unwrap();

        // Create story file in current/ so move_story_to_done would work.
        let current_dir = tmp.path().join(".huskies/work/2_current");
        std::fs::create_dir_all(&current_dir).unwrap();
        std::fs::write(
            current_dir.join("50_story_test.md"),
            "---\nname: Test\n---\n",
        )
        .unwrap();

        let ctx = test_ctx(tmp.path());
        let result = tool_accept_story(&json!({"story_id": "50_story_test"}), &ctx).await;
        assert!(
            result.is_err(),
            "should refuse when feature branch has unmerged code"
        );
        let err = result.unwrap_err();
        assert!(
            err.contains("unmerged"),
            "error should mention unmerged changes: {err}"
        );
    }

    #[tokio::test]
    async fn tool_accept_story_succeeds_when_no_feature_branch() {
        let tmp = tempfile::tempdir().unwrap();
        setup_git_repo_in(tmp.path());

        // Create story file in current/ (no feature branch).
        let current_dir = tmp.path().join(".huskies/work/2_current");
        std::fs::create_dir_all(&current_dir).unwrap();
        let content = "---\nname: No Branch\n---\n";
        std::fs::write(current_dir.join("51_story_no_branch.md"), content).unwrap();
        crate::db::ensure_content_store();
        crate::db::write_item_with_content(
            "51_story_no_branch",
            "2_current",
            content,
            crate::db::ItemMeta::named("No Branch"),
        );

        let ctx = test_ctx(tmp.path());
        let result = tool_accept_story(&json!({"story_id": "51_story_no_branch"}), &ctx).await;
        assert!(
            result.is_ok(),
            "should succeed when no feature branch: {result:?}"
        );
    }

    #[tokio::test]
    async fn delete_story_cancels_pending_timer() {
        use chrono::Utc;

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

        // Create a story file in the backlog.
        let backlog = root.join(".huskies/work/1_backlog");
        fs::create_dir_all(&backlog).unwrap();
        fs::write(
            backlog.join("478_story_rate_limit_repro.md"),
            "---\nname: \"Rate Limit Repro\"\n---\n",
        )
        .unwrap();

        let ctx = test_ctx(root);

        // Schedule a rate-limit retry timer for the story (simulates the
        // auto-scheduler that fires after a rate-limit event).
        let future_time = Utc::now() + chrono::Duration::minutes(5);
        ctx.timer_store
            .add("478_story_rate_limit_repro".to_string(), future_time)
            .unwrap();

        // Sanity: timer is present before deletion.
        assert_eq!(ctx.timer_store.list().len(), 1);

        // Delete the story.
        let result =
            tool_delete_story(&json!({"story_id": "478_story_rate_limit_repro"}), &ctx).await;
        assert!(result.is_ok(), "delete_story failed: {result:?}");

        // Timer must be gone — fast-forwarding past the scheduled time should
        // return no entries.
        assert!(
            ctx.timer_store.list().is_empty(),
            "timer was not cancelled by delete_story"
        );
        let far_future = Utc::now() + chrono::Duration::hours(1);
        let due = ctx.timer_store.take_due(far_future);
        assert!(
            due.is_empty(),
            "take_due returned a timer for the deleted story: {due:?}"
        );

        // Filesystem shadow must also be gone.
        assert!(
            !backlog.join("478_story_rate_limit_repro.md").exists(),
            "filesystem shadow was not removed"
        );
    }
}
