use std::path::{Path, PathBuf};
use std::process::Command;

use crate::io::story_metadata::{clear_front_matter_field, write_rejection_notes};
use crate::slog;

pub(super) fn item_type_from_id(item_id: &str) -> &'static str {
    // New format: {digits}_{type}_{slug}
    let after_num = item_id.trim_start_matches(|c: char| c.is_ascii_digit());
    if after_num.starts_with("_bug_") {
        "bug"
    } else if after_num.starts_with("_spike_") {
        "spike"
    } else {
        "story"
    }
}

/// Return the source directory path for a work item (always work/1_backlog/).
fn item_source_dir(project_root: &Path, _item_id: &str) -> PathBuf {
    project_root.join(".storkit").join("work").join("1_backlog")
}

/// Return the done directory path for a work item (always work/5_done/).
fn item_archive_dir(project_root: &Path, _item_id: &str) -> PathBuf {
    project_root.join(".storkit").join("work").join("5_done")
}

/// Move a work item (story, bug, or spike) from `work/1_backlog/` to `work/2_current/`.
///
/// Idempotent: if the item is already in `2_current/`, returns Ok without committing.
/// If the item is not found in `1_backlog/`, logs a warning and returns Ok.
pub fn move_story_to_current(project_root: &Path, story_id: &str) -> Result<(), String> {
    let sk = project_root.join(".storkit").join("work");
    let current_dir = sk.join("2_current");
    let current_path = current_dir.join(format!("{story_id}.md"));

    if current_path.exists() {
        // Already in 2_current/ — idempotent, nothing to do.
        return Ok(());
    }

    let source_dir = item_source_dir(project_root, story_id);
    let source_path = source_dir.join(format!("{story_id}.md"));

    if !source_path.exists() {
        slog!(
            "[lifecycle] Work item '{story_id}' not found in {}; skipping move to 2_current/",
            source_dir.display()
        );
        return Ok(());
    }

    std::fs::create_dir_all(&current_dir)
        .map_err(|e| format!("Failed to create work/2_current/ directory: {e}"))?;

    std::fs::rename(&source_path, &current_path)
        .map_err(|e| format!("Failed to move '{story_id}' to 2_current/: {e}"))?;

    slog!(
        "[lifecycle] Moved '{story_id}' from {} to work/2_current/",
        source_dir.display()
    );

    Ok(())
}

/// Check whether a feature branch `feature/story-{story_id}` exists and has
/// commits that are not yet on master.  Returns `true` when there is unmerged
/// work, `false` when there is no branch or all its commits are already
/// reachable from master.
pub fn feature_branch_has_unmerged_changes(project_root: &Path, story_id: &str) -> bool {
    let branch = format!("feature/story-{story_id}");

    // Check if the branch exists.
    let branch_check = Command::new("git")
        .args(["rev-parse", "--verify", &branch])
        .current_dir(project_root)
        .output();
    match branch_check {
        Ok(out) if out.status.success() => {}
        _ => return false, // No feature branch → nothing to merge.
    }

    // Check if the branch has commits not reachable from master.
    let log = Command::new("git")
        .args(["log", &format!("master..{branch}"), "--oneline"])
        .current_dir(project_root)
        .output();
    match log {
        Ok(out) => {
            let stdout = String::from_utf8_lossy(&out.stdout);
            !stdout.trim().is_empty()
        }
        Err(_) => false,
    }
}

/// Move a story from `work/2_current/` to `work/5_done/` and auto-commit.
///
/// * If the story is in `2_current/`, it is moved to `5_done/` and committed.
/// * If the story is in `4_merge/`, it is moved to `5_done/` and committed.
/// * If the story is already in `5_done/` or `6_archived/`, this is a no-op (idempotent).
/// * If the story is not found in `2_current/`, `4_merge/`, `5_done/`, or `6_archived/`, an error is returned.
pub fn move_story_to_done(project_root: &Path, story_id: &str) -> Result<(), String> {
    let sk = project_root.join(".storkit").join("work");
    let current_path = sk.join("2_current").join(format!("{story_id}.md"));
    let merge_path = sk.join("4_merge").join(format!("{story_id}.md"));
    let done_dir = sk.join("5_done");
    let done_path = done_dir.join(format!("{story_id}.md"));
    let archived_path = sk.join("6_archived").join(format!("{story_id}.md"));

    if done_path.exists() || archived_path.exists() {
        // Already in done or archived — idempotent, nothing to do.
        return Ok(());
    }

    // Check 2_current/ first, then 4_merge/
    let source_path = if current_path.exists() {
        current_path.clone()
    } else if merge_path.exists() {
        merge_path.clone()
    } else {
        return Err(format!(
            "Story '{story_id}' not found in work/2_current/ or work/4_merge/. Cannot accept story."
        ));
    };

    std::fs::create_dir_all(&done_dir)
        .map_err(|e| format!("Failed to create work/5_done/ directory: {e}"))?;
    std::fs::rename(&source_path, &done_path)
        .map_err(|e| format!("Failed to move story '{story_id}' to 5_done/: {e}"))?;

    // Strip stale pipeline fields from front matter now that the story is done.
    for field in &["merge_failure", "retry_count", "blocked"] {
        if let Err(e) = clear_front_matter_field(&done_path, field) {
            slog!("[lifecycle] Warning: could not clear {field} from '{story_id}': {e}");
        }
    }

    let from_dir = if source_path == current_path {
        "work/2_current/"
    } else {
        "work/4_merge/"
    };
    slog!("[lifecycle] Moved story '{story_id}' from {from_dir} to work/5_done/");

    Ok(())
}

/// Move a story/bug from `work/2_current/` or `work/3_qa/` to `work/4_merge/`.
///
/// This stages a work item as ready for the mergemaster to pick up and merge into master.
/// Idempotent: if already in `4_merge/`, returns Ok without committing.
pub fn move_story_to_merge(project_root: &Path, story_id: &str) -> Result<(), String> {
    let sk = project_root.join(".storkit").join("work");
    let current_path = sk.join("2_current").join(format!("{story_id}.md"));
    let qa_path = sk.join("3_qa").join(format!("{story_id}.md"));
    let merge_dir = sk.join("4_merge");
    let merge_path = merge_dir.join(format!("{story_id}.md"));

    if merge_path.exists() {
        // Already in 4_merge/ — idempotent, nothing to do.
        return Ok(());
    }

    // Accept from 2_current/ (manual trigger) or 3_qa/ (pipeline advancement from QA stage).
    let source_path = if current_path.exists() {
        current_path.clone()
    } else if qa_path.exists() {
        qa_path.clone()
    } else {
        return Err(format!(
            "Work item '{story_id}' not found in work/2_current/ or work/3_qa/. Cannot move to 4_merge/."
        ));
    };

    std::fs::create_dir_all(&merge_dir)
        .map_err(|e| format!("Failed to create work/4_merge/ directory: {e}"))?;
    std::fs::rename(&source_path, &merge_path)
        .map_err(|e| format!("Failed to move '{story_id}' to 4_merge/: {e}"))?;

    let from_dir = if source_path == current_path {
        "work/2_current/"
    } else {
        "work/3_qa/"
    };
    // Reset retry count and blocked for the new stage.
    if let Err(e) = clear_front_matter_field(&merge_path, "retry_count") {
        slog!("[lifecycle] Warning: could not clear retry_count for '{story_id}': {e}");
    }
    if let Err(e) = clear_front_matter_field(&merge_path, "blocked") {
        slog!("[lifecycle] Warning: could not clear blocked for '{story_id}': {e}");
    }

    slog!("[lifecycle] Moved '{story_id}' from {from_dir} to work/4_merge/");

    Ok(())
}

/// Move a story/bug from `work/2_current/` to `work/3_qa/` and auto-commit.
///
/// This stages a work item for QA review before merging to master.
/// Idempotent: if already in `3_qa/`, returns Ok without committing.
pub fn move_story_to_qa(project_root: &Path, story_id: &str) -> Result<(), String> {
    let sk = project_root.join(".storkit").join("work");
    let current_path = sk.join("2_current").join(format!("{story_id}.md"));
    let qa_dir = sk.join("3_qa");
    let qa_path = qa_dir.join(format!("{story_id}.md"));

    if qa_path.exists() {
        // Already in 3_qa/ — idempotent, nothing to do.
        return Ok(());
    }

    if !current_path.exists() {
        return Err(format!(
            "Work item '{story_id}' not found in work/2_current/. Cannot move to 3_qa/."
        ));
    }

    std::fs::create_dir_all(&qa_dir)
        .map_err(|e| format!("Failed to create work/3_qa/ directory: {e}"))?;
    std::fs::rename(&current_path, &qa_path)
        .map_err(|e| format!("Failed to move '{story_id}' to 3_qa/: {e}"))?;

    // Reset retry count for the new stage.
    if let Err(e) = clear_front_matter_field(&qa_path, "retry_count") {
        slog!("[lifecycle] Warning: could not clear retry_count for '{story_id}': {e}");
    }
    if let Err(e) = clear_front_matter_field(&qa_path, "blocked") {
        slog!("[lifecycle] Warning: could not clear blocked for '{story_id}': {e}");
    }

    slog!("[lifecycle] Moved '{story_id}' from work/2_current/ to work/3_qa/");

    Ok(())
}

/// Move a story from `work/3_qa/` back to `work/2_current/` and write rejection notes.
///
/// Used when a human reviewer rejects a story during manual QA.
/// Clears the `review_hold` front matter field and appends rejection notes to the story file.
pub fn reject_story_from_qa(
    project_root: &Path,
    story_id: &str,
    notes: &str,
) -> Result<(), String> {
    let sk = project_root.join(".storkit").join("work");
    let qa_path = sk.join("3_qa").join(format!("{story_id}.md"));
    let current_dir = sk.join("2_current");
    let current_path = current_dir.join(format!("{story_id}.md"));

    if current_path.exists() {
        return Ok(()); // Already in 2_current — idempotent.
    }

    if !qa_path.exists() {
        return Err(format!(
            "Work item '{story_id}' not found in work/3_qa/. Cannot reject."
        ));
    }

    std::fs::create_dir_all(&current_dir)
        .map_err(|e| format!("Failed to create work/2_current/ directory: {e}"))?;
    std::fs::rename(&qa_path, &current_path)
        .map_err(|e| format!("Failed to move '{story_id}' from 3_qa/ to 2_current/: {e}"))?;

    // Clear review_hold since the story is going back for rework.
    if let Err(e) = clear_front_matter_field(&current_path, "review_hold") {
        slog!("[lifecycle] Warning: could not clear review_hold from '{story_id}': {e}");
    }

    // Write rejection notes into the story file so the coder can see what needs fixing.
    if !notes.is_empty()
        && let Err(e) = write_rejection_notes(&current_path, notes)
    {
        slog!("[lifecycle] Warning: could not write rejection notes to '{story_id}': {e}");
    }

    slog!("[lifecycle] Rejected '{story_id}' from work/3_qa/ back to work/2_current/");

    Ok(())
}

/// Move any work item to an arbitrary pipeline stage by searching all stages.
///
/// Accepts `target_stage` as one of: `backlog`, `current`, `qa`, `merge`, `done`.
/// Idempotent: if the item is already in the target stage, returns Ok.
/// Returns `(from_stage, to_stage)` on success.
pub fn move_story_to_stage(
    project_root: &Path,
    story_id: &str,
    target_stage: &str,
) -> Result<(String, String), String> {
    let stage_dirs: &[(&str, &str)] = &[
        ("backlog", "1_backlog"),
        ("current", "2_current"),
        ("qa", "3_qa"),
        ("merge", "4_merge"),
        ("done", "5_done"),
    ];

    let target_dir_name = stage_dirs
        .iter()
        .find(|(name, _)| *name == target_stage)
        .map(|(_, dir)| *dir)
        .ok_or_else(|| {
            format!(
                "Invalid target_stage '{target_stage}'. Must be one of: backlog, current, qa, merge, done"
            )
        })?;

    let sk = project_root.join(".storkit").join("work");
    let target_dir = sk.join(target_dir_name);
    let target_path = target_dir.join(format!("{story_id}.md"));

    if target_path.exists() {
        return Ok((target_stage.to_string(), target_stage.to_string()));
    }

    // Search all named stages plus the archive stage.
    let search_dirs: &[(&str, &str)] = &[
        ("backlog", "1_backlog"),
        ("current", "2_current"),
        ("qa", "3_qa"),
        ("merge", "4_merge"),
        ("done", "5_done"),
        ("archived", "6_archived"),
    ];

    let mut found_path: Option<std::path::PathBuf> = None;
    let mut from_stage = "";
    for (stage_name, dir_name) in search_dirs {
        let candidate = sk.join(dir_name).join(format!("{story_id}.md"));
        if candidate.exists() {
            found_path = Some(candidate);
            from_stage = stage_name;
            break;
        }
    }

    let source_path =
        found_path.ok_or_else(|| format!("Work item '{story_id}' not found in any pipeline stage."))?;

    std::fs::create_dir_all(&target_dir)
        .map_err(|e| format!("Failed to create work/{target_dir_name}/ directory: {e}"))?;
    std::fs::rename(&source_path, &target_path)
        .map_err(|e| format!("Failed to move '{story_id}' to work/{target_dir_name}/: {e}"))?;

    slog!(
        "[lifecycle] Moved '{story_id}' from work/{from_stage}/ to work/{target_dir_name}/"
    );

    Ok((from_stage.to_string(), target_stage.to_string()))
}

/// Move a bug from `work/2_current/` or `work/1_backlog/` to `work/5_done/` and auto-commit.
///
/// * If the bug is in `2_current/`, it is moved to `5_done/` and committed.
/// * If the bug is still in `1_backlog/` (never started), it is moved directly to `5_done/`.
/// * If the bug is already in `5_done/`, this is a no-op (idempotent).
/// * If the bug is not found anywhere, an error is returned.
pub fn close_bug_to_archive(project_root: &Path, bug_id: &str) -> Result<(), String> {
    let sk = project_root.join(".storkit").join("work");
    let current_path = sk.join("2_current").join(format!("{bug_id}.md"));
    let backlog_path = sk.join("1_backlog").join(format!("{bug_id}.md"));
    let archive_dir = item_archive_dir(project_root, bug_id);
    let archive_path = archive_dir.join(format!("{bug_id}.md"));

    if archive_path.exists() {
        return Ok(());
    }

    let source_path = if current_path.exists() {
        current_path.clone()
    } else if backlog_path.exists() {
        backlog_path.clone()
    } else {
        return Err(format!(
            "Bug '{bug_id}' not found in work/2_current/ or work/1_backlog/. Cannot close bug."
        ));
    };

    std::fs::create_dir_all(&archive_dir)
        .map_err(|e| format!("Failed to create work/5_done/ directory: {e}"))?;
    std::fs::rename(&source_path, &archive_path)
        .map_err(|e| format!("Failed to move bug '{bug_id}' to 5_done/: {e}"))?;

    slog!(
        "[lifecycle] Closed bug '{bug_id}' → work/5_done/"
    );

    Ok(())
}

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

    // ── move_story_to_current tests ────────────────────────────────────────────

    #[test]
    fn move_story_to_current_moves_file() {
        use std::fs;
        let tmp = tempfile::tempdir().unwrap();
        let root = tmp.path();
        let backlog = root.join(".storkit/work/1_backlog");
        let current = root.join(".storkit/work/2_current");
        fs::create_dir_all(&backlog).unwrap();
        fs::create_dir_all(&current).unwrap();
        fs::write(backlog.join("10_story_foo.md"), "test").unwrap();

        move_story_to_current(root, "10_story_foo").unwrap();

        assert!(!backlog.join("10_story_foo.md").exists());
        assert!(current.join("10_story_foo.md").exists());
    }

    #[test]
    fn move_story_to_current_is_idempotent_when_already_current() {
        use std::fs;
        let tmp = tempfile::tempdir().unwrap();
        let root = tmp.path();
        let current = root.join(".storkit/work/2_current");
        fs::create_dir_all(&current).unwrap();
        fs::write(current.join("11_story_foo.md"), "test").unwrap();

        move_story_to_current(root, "11_story_foo").unwrap();
        assert!(current.join("11_story_foo.md").exists());
    }

    #[test]
    fn move_story_to_current_noop_when_not_in_backlog() {
        let tmp = tempfile::tempdir().unwrap();
        assert!(move_story_to_current(tmp.path(), "99_missing").is_ok());
    }

    #[test]
    fn move_bug_to_current_moves_from_backlog() {
        use std::fs;
        let tmp = tempfile::tempdir().unwrap();
        let root = tmp.path();
        let backlog = root.join(".storkit/work/1_backlog");
        let current = root.join(".storkit/work/2_current");
        fs::create_dir_all(&backlog).unwrap();
        fs::create_dir_all(&current).unwrap();
        fs::write(backlog.join("1_bug_test.md"), "# Bug 1\n").unwrap();

        move_story_to_current(root, "1_bug_test").unwrap();

        assert!(!backlog.join("1_bug_test.md").exists());
        assert!(current.join("1_bug_test.md").exists());
    }

    // ── close_bug_to_archive tests ─────────────────────────────────────────────

    #[test]
    fn close_bug_moves_from_current_to_archive() {
        use std::fs;
        let tmp = tempfile::tempdir().unwrap();
        let root = tmp.path();
        let current = root.join(".storkit/work/2_current");
        fs::create_dir_all(&current).unwrap();
        fs::write(current.join("2_bug_test.md"), "# Bug 2\n").unwrap();

        close_bug_to_archive(root, "2_bug_test").unwrap();

        assert!(!current.join("2_bug_test.md").exists());
        assert!(root.join(".storkit/work/5_done/2_bug_test.md").exists());
    }

    #[test]
    fn close_bug_moves_from_backlog_when_not_started() {
        use std::fs;
        let tmp = tempfile::tempdir().unwrap();
        let root = tmp.path();
        let backlog = root.join(".storkit/work/1_backlog");
        fs::create_dir_all(&backlog).unwrap();
        fs::write(backlog.join("3_bug_test.md"), "# Bug 3\n").unwrap();

        close_bug_to_archive(root, "3_bug_test").unwrap();

        assert!(!backlog.join("3_bug_test.md").exists());
        assert!(root.join(".storkit/work/5_done/3_bug_test.md").exists());
    }

    // ── item_type_from_id tests ────────────────────────────────────────────────

    #[test]
    fn item_type_from_id_detects_types() {
        assert_eq!(item_type_from_id("1_bug_test"), "bug");
        assert_eq!(item_type_from_id("1_spike_research"), "spike");
        assert_eq!(item_type_from_id("50_story_my_story"), "story");
        assert_eq!(item_type_from_id("1_story_simple"), "story");
    }

    // ── move_story_to_merge tests ──────────────────────────────────────────────

    #[test]
    fn move_story_to_merge_moves_file() {
        use std::fs;
        let tmp = tempfile::tempdir().unwrap();
        let root = tmp.path();
        let current = root.join(".storkit/work/2_current");
        fs::create_dir_all(&current).unwrap();
        fs::write(current.join("20_story_foo.md"), "test").unwrap();

        move_story_to_merge(root, "20_story_foo").unwrap();

        assert!(!current.join("20_story_foo.md").exists());
        assert!(root.join(".storkit/work/4_merge/20_story_foo.md").exists());
    }

    #[test]
    fn move_story_to_merge_from_qa_dir() {
        use std::fs;
        let tmp = tempfile::tempdir().unwrap();
        let root = tmp.path();
        let qa_dir = root.join(".storkit/work/3_qa");
        fs::create_dir_all(&qa_dir).unwrap();
        fs::write(qa_dir.join("40_story_test.md"), "test").unwrap();

        move_story_to_merge(root, "40_story_test").unwrap();

        assert!(!qa_dir.join("40_story_test.md").exists());
        assert!(root.join(".storkit/work/4_merge/40_story_test.md").exists());
    }

    #[test]
    fn move_story_to_merge_idempotent_when_already_in_merge() {
        use std::fs;
        let tmp = tempfile::tempdir().unwrap();
        let root = tmp.path();
        let merge_dir = root.join(".storkit/work/4_merge");
        fs::create_dir_all(&merge_dir).unwrap();
        fs::write(merge_dir.join("21_story_test.md"), "test").unwrap();

        move_story_to_merge(root, "21_story_test").unwrap();
        assert!(merge_dir.join("21_story_test.md").exists());
    }

    #[test]
    fn move_story_to_merge_errors_when_not_in_current_or_qa() {
        let tmp = tempfile::tempdir().unwrap();
        let result = move_story_to_merge(tmp.path(), "99_nonexistent");
        assert!(result.unwrap_err().contains("not found in work/2_current/ or work/3_qa/"));
    }

    // ── move_story_to_qa tests ────────────────────────────────────────────────

    #[test]
    fn move_story_to_qa_moves_file() {
        use std::fs;
        let tmp = tempfile::tempdir().unwrap();
        let root = tmp.path();
        let current = root.join(".storkit/work/2_current");
        fs::create_dir_all(&current).unwrap();
        fs::write(current.join("30_story_qa.md"), "test").unwrap();

        move_story_to_qa(root, "30_story_qa").unwrap();

        assert!(!current.join("30_story_qa.md").exists());
        assert!(root.join(".storkit/work/3_qa/30_story_qa.md").exists());
    }

    #[test]
    fn move_story_to_qa_idempotent_when_already_in_qa() {
        use std::fs;
        let tmp = tempfile::tempdir().unwrap();
        let root = tmp.path();
        let qa_dir = root.join(".storkit/work/3_qa");
        fs::create_dir_all(&qa_dir).unwrap();
        fs::write(qa_dir.join("31_story_test.md"), "test").unwrap();

        move_story_to_qa(root, "31_story_test").unwrap();
        assert!(qa_dir.join("31_story_test.md").exists());
    }

    #[test]
    fn move_story_to_qa_errors_when_not_in_current() {
        let tmp = tempfile::tempdir().unwrap();
        let result = move_story_to_qa(tmp.path(), "99_nonexistent");
        assert!(result.unwrap_err().contains("not found in work/2_current/"));
    }

    // ── move_story_to_done tests ──────────────────────────────────────────

    #[test]
    fn move_story_to_done_finds_in_merge_dir() {
        use std::fs;
        let tmp = tempfile::tempdir().unwrap();
        let root = tmp.path();
        let merge_dir = root.join(".storkit/work/4_merge");
        fs::create_dir_all(&merge_dir).unwrap();
        fs::write(merge_dir.join("22_story_test.md"), "test").unwrap();

        move_story_to_done(root, "22_story_test").unwrap();

        assert!(!merge_dir.join("22_story_test.md").exists());
        assert!(root.join(".storkit/work/5_done/22_story_test.md").exists());
    }

    #[test]
    fn move_story_to_done_error_when_not_in_current_or_merge() {
        let tmp = tempfile::tempdir().unwrap();
        let result = move_story_to_done(tmp.path(), "99_nonexistent");
        assert!(result.unwrap_err().contains("4_merge"));
    }

    // ── feature_branch_has_unmerged_changes tests ────────────────────────────

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

    /// Bug 226: feature_branch_has_unmerged_changes returns true when the
    /// feature branch has commits not on master.
    #[test]
    fn feature_branch_has_unmerged_changes_detects_unmerged_code() {
        use std::fs;
        use tempfile::tempdir;

        let tmp = tempdir().unwrap();
        let repo = tmp.path();
        init_git_repo(repo);

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

        assert!(
            feature_branch_has_unmerged_changes(repo, "50_story_test"),
            "should detect unmerged changes on feature branch"
        );
    }

    /// Bug 226: feature_branch_has_unmerged_changes returns false when no
    /// feature branch exists.
    #[test]
    fn feature_branch_has_unmerged_changes_false_when_no_branch() {
        use tempfile::tempdir;

        let tmp = tempdir().unwrap();
        let repo = tmp.path();
        init_git_repo(repo);

        assert!(
            !feature_branch_has_unmerged_changes(repo, "99_nonexistent"),
            "should return false when no feature branch"
        );
    }

    // ── reject_story_from_qa tests ────────────────────────────────────────────

    #[test]
    fn reject_story_from_qa_moves_to_current() {
        use std::fs;
        let tmp = tempfile::tempdir().unwrap();
        let root = tmp.path();
        let qa_dir = root.join(".storkit/work/3_qa");
        let current_dir = root.join(".storkit/work/2_current");
        fs::create_dir_all(&qa_dir).unwrap();
        fs::create_dir_all(&current_dir).unwrap();
        fs::write(
            qa_dir.join("50_story_test.md"),
            "---\nname: Test\nreview_hold: true\n---\n# Story\n",
        )
        .unwrap();

        reject_story_from_qa(root, "50_story_test", "Button color wrong").unwrap();

        assert!(!qa_dir.join("50_story_test.md").exists());
        assert!(current_dir.join("50_story_test.md").exists());
        let contents = fs::read_to_string(current_dir.join("50_story_test.md")).unwrap();
        assert!(contents.contains("Button color wrong"));
        assert!(contents.contains("## QA Rejection Notes"));
        assert!(!contents.contains("review_hold"));
    }

    #[test]
    fn reject_story_from_qa_errors_when_not_in_qa() {
        let tmp = tempfile::tempdir().unwrap();
        let result = reject_story_from_qa(tmp.path(), "99_nonexistent", "notes");
        assert!(result.unwrap_err().contains("not found in work/3_qa/"));
    }

    #[test]
    fn reject_story_from_qa_idempotent_when_in_current() {
        use std::fs;
        let tmp = tempfile::tempdir().unwrap();
        let root = tmp.path();
        let current_dir = root.join(".storkit/work/2_current");
        fs::create_dir_all(&current_dir).unwrap();
        fs::write(current_dir.join("51_story_test.md"), "---\nname: Test\n---\n# Story\n").unwrap();

        reject_story_from_qa(root, "51_story_test", "notes").unwrap();
        assert!(current_dir.join("51_story_test.md").exists());
    }

    // ── move_story_to_stage tests ─────────────────────────────────

    #[test]
    fn move_story_to_stage_moves_from_backlog_to_current() {
        use std::fs;
        let tmp = tempfile::tempdir().unwrap();
        let root = tmp.path();
        let backlog = root.join(".storkit/work/1_backlog");
        let current = root.join(".storkit/work/2_current");
        fs::create_dir_all(&backlog).unwrap();
        fs::create_dir_all(&current).unwrap();
        fs::write(backlog.join("60_story_move.md"), "test").unwrap();

        let (from, to) = move_story_to_stage(root, "60_story_move", "current").unwrap();

        assert_eq!(from, "backlog");
        assert_eq!(to, "current");
        assert!(!backlog.join("60_story_move.md").exists());
        assert!(current.join("60_story_move.md").exists());
    }

    #[test]
    fn move_story_to_stage_moves_from_current_to_backlog() {
        use std::fs;
        let tmp = tempfile::tempdir().unwrap();
        let root = tmp.path();
        let current = root.join(".storkit/work/2_current");
        let backlog = root.join(".storkit/work/1_backlog");
        fs::create_dir_all(&current).unwrap();
        fs::create_dir_all(&backlog).unwrap();
        fs::write(current.join("61_story_back.md"), "test").unwrap();

        let (from, to) = move_story_to_stage(root, "61_story_back", "backlog").unwrap();

        assert_eq!(from, "current");
        assert_eq!(to, "backlog");
        assert!(!current.join("61_story_back.md").exists());
        assert!(backlog.join("61_story_back.md").exists());
    }

    #[test]
    fn move_story_to_stage_idempotent_when_already_in_target() {
        use std::fs;
        let tmp = tempfile::tempdir().unwrap();
        let root = tmp.path();
        let current = root.join(".storkit/work/2_current");
        fs::create_dir_all(&current).unwrap();
        fs::write(current.join("62_story_idem.md"), "test").unwrap();

        let (from, to) = move_story_to_stage(root, "62_story_idem", "current").unwrap();

        assert_eq!(from, "current");
        assert_eq!(to, "current");
        assert!(current.join("62_story_idem.md").exists());
    }

    #[test]
    fn move_story_to_stage_invalid_target_returns_error() {
        let tmp = tempfile::tempdir().unwrap();
        let result = move_story_to_stage(tmp.path(), "1_story_test", "invalid");
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("Invalid target_stage"));
    }

    #[test]
    fn move_story_to_stage_not_found_returns_error() {
        let tmp = tempfile::tempdir().unwrap();
        let result = move_story_to_stage(tmp.path(), "99_story_ghost", "current");
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("not found in any pipeline stage"));
    }

    #[test]
    fn move_story_to_stage_finds_in_qa_dir() {
        use std::fs;
        let tmp = tempfile::tempdir().unwrap();
        let root = tmp.path();
        let qa_dir = root.join(".storkit/work/3_qa");
        let backlog = root.join(".storkit/work/1_backlog");
        fs::create_dir_all(&qa_dir).unwrap();
        fs::create_dir_all(&backlog).unwrap();
        fs::write(qa_dir.join("63_story_qa.md"), "test").unwrap();

        let (from, to) = move_story_to_stage(root, "63_story_qa", "backlog").unwrap();

        assert_eq!(from, "qa");
        assert_eq!(to, "backlog");
        assert!(!qa_dir.join("63_story_qa.md").exists());
        assert!(backlog.join("63_story_qa.md").exists());
    }
}
