//! update_story_in_file: replaces sections / writes front matter on existing stories.

use serde_json::Value;
use std::collections::HashMap;
use std::path::Path;

#[allow(unused_imports)]
use super::super::{
    create_section_content, next_item_number, read_story_content, replace_section_content,
    slugify_name, story_stage, write_story_content,
};

use crate::io::story_metadata::set_front_matter_field;

fn json_value_to_yaml_scalar(value: &Value) -> String {
    match value {
        Value::Bool(b) => b.to_string(),
        Value::Number(n) => n.to_string(),
        Value::Array(arr) => {
            let items: Vec<String> = arr.iter().map(|v| v.to_string()).collect();
            format!("[{}]", items.join(", "))
        }
        Value::String(s) => yaml_encode_str(s),
        // Null and Object are not meaningful as YAML scalars; store as quoted strings.
        other => format!(
            "\"{}\"",
            other
                .to_string()
                .replace('"', "\\\"")
                .replace('\n', " ")
                .replace('\r', "")
        ),
    }
}

/// Encode a plain string as a YAML scalar.
///
/// Booleans (`true`/`false`), integers, and inline sequences (`[...]`) are
/// written unquoted.  Everything else is quoted to avoid ambiguity.
fn yaml_encode_str(s: &str) -> String {
    match s {
        "true" | "false" => s.to_string(),
        s if s.parse::<i64>().is_ok() => s.to_string(),
        s if s.parse::<f64>().is_ok() => s.to_string(),
        // YAML inline sequences like [490] or [490, 491] — write unquoted so
        // serde_yaml can deserialise them as Vec<u32>.
        s if s.starts_with('[') && s.ends_with(']') => s.to_string(),
        s => format!(
            "\"{}\"",
            s.replace('"', "\\\"").replace('\n', " ").replace('\r', "")
        ),
    }
}

/// Update the user story text and/or description in a story.
///
/// At least one of `user_story` or `description` must be provided.
/// Replaces the content of the corresponding `##` section in place.
pub fn update_story_in_file(
    project_root: &Path,
    story_id: &str,
    user_story: Option<&str>,
    description: Option<&str>,
    front_matter: Option<&HashMap<String, Value>>,
) -> Result<(), String> {
    let has_front_matter_updates = front_matter.map(|m| !m.is_empty()).unwrap_or(false);
    if user_story.is_none() && description.is_none() && !has_front_matter_updates {
        return Err(
            "At least one of 'user_story', 'description', or 'front_matter' must be provided."
                .to_string(),
        );
    }

    let mut contents = read_story_content(project_root, story_id)?;

    if let Some(fields) = front_matter {
        if fields.contains_key("acceptance_criteria") {
            return Err(
                "'acceptance_criteria' is a reserved field managed via the story body \
                 (use add_criterion / remove_criterion / edit_criterion instead)."
                    .to_string(),
            );
        }
        for (key, value) in fields {
            let yaml_value = json_value_to_yaml_scalar(value);
            contents = set_front_matter_field(&contents, key, &yaml_value);
        }
    }

    if let Some(us) = user_story {
        contents = match replace_section_content(&contents, "User Story", us) {
            Ok(updated) => updated,
            Err(_) => {
                create_section_content(&contents, "User Story", us, Some("Acceptance Criteria"))
            }
        };
    }
    if let Some(desc) = description {
        contents = match replace_section_content(&contents, "Description", desc) {
            Ok(updated) => updated,
            Err(_) => {
                create_section_content(&contents, "Description", desc, Some("Acceptance Criteria"))
            }
        };
    }

    // Write back to content store.
    let stage = story_stage(story_id).unwrap_or_else(|| "2_current".to_string());
    write_story_content(project_root, story_id, &stage, &contents);

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::io::story_metadata::parse_front_matter;
    use std::fs;

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

    #[allow(dead_code)]
    fn story_with_criteria(n: usize) -> String {
        let mut s = "---\nname: Test Story\n---\n\n## Acceptance Criteria\n\n".to_string();
        for i in 0..n {
            s.push_str(&format!("- [ ] Criterion {i}\n"));
        }
        s
    }

    /// Helper to set up a story in the filesystem and content store for tests
    /// that use check/add criterion.
    #[allow(dead_code)]
    fn setup_story_in_fs(root: &std::path::Path, story_id: &str, content: &str) {
        let current = root.join(".huskies/work/2_current");
        fs::create_dir_all(&current).unwrap();
        fs::write(current.join(format!("{story_id}.md")), content).unwrap();
        // Also write to the global content store so read_story_content picks up this
        // content even when a previous test has left a stale entry for the same ID.
        crate::db::ensure_content_store();
        crate::db::write_content(story_id, content);
    }

    // --- create_story integration tests ---

    #[test]
    fn update_story_acceptance_criteria_in_front_matter_returns_error() {
        // Bug 625: passing acceptance_criteria via front_matter must return an
        // explicit error rather than silently dropping the value.
        let tmp = tempfile::tempdir().unwrap();
        setup_story_in_fs(
            tmp.path(),
            "101_test",
            "---\nname: T\n---\n\n## Acceptance Criteria\n\n- [ ] Existing\n",
        );

        let mut fields = HashMap::new();
        fields.insert(
            "acceptance_criteria".to_string(),
            serde_json::json!(["crit 1", "crit 2"]),
        );
        let result = update_story_in_file(tmp.path(), "101_test", None, None, Some(&fields));
        assert!(result.is_err(), "should fail with reserved-field error");
        let err = result.unwrap_err();
        assert!(
            err.contains("acceptance_criteria"),
            "error should name the reserved field: {err}"
        );
    }

    // ── remove_criterion_from_file tests ──────────────────────────────────────

    #[test]
    fn update_story_replaces_user_story_section() {
        let tmp = tempfile::tempdir().unwrap();
        let content = "---\nname: T\n---\n\n## User Story\n\nOld text\n\n## Acceptance Criteria\n\n- [ ] AC\n";
        setup_story_in_fs(tmp.path(), "20_test", content);

        update_story_in_file(
            tmp.path(),
            "20_test",
            Some("New user story text"),
            None,
            None,
        )
        .unwrap();

        let result = read_story_content(tmp.path(), "20_test").unwrap();
        assert!(
            result.contains("New user story text"),
            "new text should be present"
        );
        assert!(!result.contains("Old text"), "old text should be replaced");
        assert!(
            result.contains("## Acceptance Criteria"),
            "other sections preserved"
        );
    }

    #[test]
    fn update_story_replaces_description_section() {
        let tmp = tempfile::tempdir().unwrap();
        let content = "---\nname: T\n---\n\n## Description\n\nOld description\n\n## Acceptance Criteria\n\n- [ ] AC\n";
        setup_story_in_fs(tmp.path(), "21_test", content);

        update_story_in_file(tmp.path(), "21_test", None, Some("New description"), None).unwrap();

        let result = read_story_content(tmp.path(), "21_test").unwrap();
        assert!(
            result.contains("New description"),
            "new description present"
        );
        assert!(
            !result.contains("Old description"),
            "old description replaced"
        );
    }

    #[test]
    fn update_story_no_args_returns_error() {
        let tmp = tempfile::tempdir().unwrap();
        setup_story_in_fs(tmp.path(), "22_test", "---\nname: T\n---\n");

        let result = update_story_in_file(tmp.path(), "22_test", None, None, None);
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("At least one"));
    }

    #[test]
    fn update_story_creates_user_story_section_if_missing() {
        let tmp = tempfile::tempdir().unwrap();
        // Story with no ## User Story section but has ## Acceptance Criteria.
        let content = "---\nname: T\n---\n\n## Acceptance Criteria\n\n- [ ] AC\n";
        setup_story_in_fs(tmp.path(), "23_test", content);

        let result =
            update_story_in_file(tmp.path(), "23_test", Some("New user story"), None, None);
        assert!(
            result.is_ok(),
            "should succeed when section is missing: {result:?}"
        );

        let updated = read_story_content(tmp.path(), "23_test").unwrap();
        assert!(
            updated.contains("## User Story"),
            "section should be created"
        );
        assert!(updated.contains("New user story"), "text should be present");
        // Section should appear before Acceptance Criteria.
        let pos_us = updated.find("## User Story").unwrap();
        let pos_ac = updated.find("## Acceptance Criteria").unwrap();
        assert!(
            pos_us < pos_ac,
            "User Story should be before Acceptance Criteria"
        );
    }

    #[test]
    fn update_story_creates_description_section_if_missing() {
        let tmp = tempfile::tempdir().unwrap();
        // Story with no ## Description section but has ## Acceptance Criteria.
        let content = "---\nname: T\n---\n\n## User Story\n\nAs a user...\n\n## Acceptance Criteria\n\n- [ ] AC\n";
        setup_story_in_fs(tmp.path(), "32_test", content);

        let result = update_story_in_file(
            tmp.path(),
            "32_test",
            None,
            Some("New description text"),
            None,
        );
        assert!(
            result.is_ok(),
            "should succeed when section is missing: {result:?}"
        );

        let updated = read_story_content(tmp.path(), "32_test").unwrap();
        assert!(
            updated.contains("## Description"),
            "section should be created"
        );
        assert!(
            updated.contains("New description text"),
            "text should be present"
        );
        // Section should appear before Acceptance Criteria.
        let pos_desc = updated.find("## Description").unwrap();
        let pos_ac = updated.find("## Acceptance Criteria").unwrap();
        assert!(
            pos_desc < pos_ac,
            "Description should be before Acceptance Criteria"
        );
    }

    #[test]
    fn update_story_creates_description_section_no_ac_section() {
        let tmp = tempfile::tempdir().unwrap();
        // Story with no ## Description and no ## Acceptance Criteria.
        let content = "---\nname: T\n---\n\nSome content here.\n";
        setup_story_in_fs(tmp.path(), "33_test", content);

        let result = update_story_in_file(
            tmp.path(),
            "33_test",
            None,
            Some("Appended description"),
            None,
        );
        assert!(
            result.is_ok(),
            "should succeed even with no Acceptance Criteria: {result:?}"
        );

        let updated = read_story_content(tmp.path(), "33_test").unwrap();
        assert!(
            updated.contains("## Description"),
            "section should be created"
        );
        assert!(
            updated.contains("Appended description"),
            "text should be present"
        );
    }

    #[test]
    fn update_story_sets_agent_front_matter_field() {
        let tmp = tempfile::tempdir().unwrap();
        setup_story_in_fs(
            tmp.path(),
            "24_test",
            "---\nname: T\n---\n\n## User Story\n\nSome story\n",
        );

        let mut fields = HashMap::new();
        fields.insert("agent".to_string(), Value::String("dev".to_string()));
        update_story_in_file(tmp.path(), "24_test", None, None, Some(&fields)).unwrap();

        let result = read_story_content(tmp.path(), "24_test").unwrap();
        assert!(
            result.contains("agent: \"dev\""),
            "agent field should be set"
        );
        assert!(result.contains("name: T"), "name field preserved");
    }

    #[test]
    fn update_story_sets_arbitrary_front_matter_fields() {
        let tmp = tempfile::tempdir().unwrap();
        setup_story_in_fs(
            tmp.path(),
            "25_test",
            "---\nname: T\n---\n\n## User Story\n\nSome story\n",
        );

        let mut fields = HashMap::new();
        fields.insert("qa".to_string(), Value::String("human".to_string()));
        fields.insert("priority".to_string(), Value::String("high".to_string()));
        update_story_in_file(tmp.path(), "25_test", None, None, Some(&fields)).unwrap();

        let result = read_story_content(tmp.path(), "25_test").unwrap();
        assert!(result.contains("qa: \"human\""), "qa field should be set");
        assert!(
            result.contains("priority: \"high\""),
            "priority field should be set"
        );
        assert!(result.contains("name: T"), "name field preserved");
    }

    #[test]
    fn update_story_front_matter_only_no_section_required() {
        let tmp = tempfile::tempdir().unwrap();
        setup_story_in_fs(
            tmp.path(),
            "26_test",
            "---\nname: T\n---\n\nNo sections here.\n",
        );

        let mut fields = HashMap::new();
        fields.insert("agent".to_string(), Value::String("dev".to_string()));
        let result = update_story_in_file(tmp.path(), "26_test", None, None, Some(&fields));
        assert!(
            result.is_ok(),
            "front-matter-only update should not require body sections"
        );

        let contents = read_story_content(tmp.path(), "26_test").unwrap();
        assert!(contents.contains("agent: \"dev\""));
    }

    #[test]
    fn update_story_bool_front_matter_written_unquoted() {
        let tmp = tempfile::tempdir().unwrap();
        setup_story_in_fs(tmp.path(), "27_test", "---\nname: T\n---\n\nNo sections.\n");

        // String "false" still works (backwards compatibility).
        let mut fields = HashMap::new();
        fields.insert("blocked".to_string(), Value::String("false".to_string()));
        update_story_in_file(tmp.path(), "27_test", None, None, Some(&fields)).unwrap();

        let result = read_story_content(tmp.path(), "27_test").unwrap();
        assert!(
            result.contains("blocked: false"),
            "bool should be unquoted: {result}"
        );
        assert!(
            !result.contains("blocked: \"false\""),
            "bool must not be quoted: {result}"
        );
    }

    #[test]
    fn update_story_integer_front_matter_written_unquoted() {
        let tmp = tempfile::tempdir().unwrap();
        setup_story_in_fs(tmp.path(), "28_test", "---\nname: T\n---\n\nNo sections.\n");

        // String "0" still works (backwards compatibility).
        let mut fields = HashMap::new();
        fields.insert("retry_count".to_string(), Value::String("0".to_string()));
        update_story_in_file(tmp.path(), "28_test", None, None, Some(&fields)).unwrap();

        let result = read_story_content(tmp.path(), "28_test").unwrap();
        assert!(
            result.contains("retry_count: 0"),
            "integer should be unquoted: {result}"
        );
        assert!(
            !result.contains("retry_count: \"0\""),
            "integer must not be quoted: {result}"
        );
    }

    #[test]
    fn update_story_bool_front_matter_parseable_after_write() {
        let tmp = tempfile::tempdir().unwrap();
        setup_story_in_fs(
            tmp.path(),
            "29_test",
            "---\nname: My Story\n---\n\nNo sections.\n",
        );

        let mut fields = HashMap::new();
        fields.insert("blocked".to_string(), Value::String("false".to_string()));
        update_story_in_file(tmp.path(), "29_test", None, None, Some(&fields)).unwrap();

        let contents = read_story_content(tmp.path(), "29_test").unwrap();
        let meta = parse_front_matter(&contents).expect("front matter should parse");
        assert_eq!(
            meta.name.as_deref(),
            Some("My Story"),
            "name preserved after writing bool field"
        );
    }

    // ── Bug 493 regression tests ──────────────────────────────────────────────

    #[test]
    fn update_story_depends_on_stored_as_yaml_array_not_quoted_string() {
        let tmp = tempfile::tempdir().unwrap();
        setup_story_in_fs(tmp.path(), "30_test", "---\nname: T\n---\n\nNo sections.\n");

        // String "[490]" still works (backwards compatibility).
        let mut fields = HashMap::new();
        fields.insert("depends_on".to_string(), Value::String("[490]".to_string()));
        update_story_in_file(tmp.path(), "30_test", None, None, Some(&fields)).unwrap();

        let result = read_story_content(tmp.path(), "30_test").unwrap();
        assert!(
            result.contains("depends_on: [490]"),
            "should be unquoted array: {result}"
        );
        assert!(
            !result.contains("depends_on: \"[490]\""),
            "must not be quoted: {result}"
        );

        let meta = parse_front_matter(&result).expect("front matter should parse");
        assert_eq!(meta.depends_on, Some(vec![490]));
    }

    #[test]
    fn update_story_native_bool_written_unquoted() {
        let tmp = tempfile::tempdir().unwrap();
        setup_story_in_fs(tmp.path(), "31_test", "---\nname: T\n---\n\nNo sections.\n");

        let mut fields = HashMap::new();
        fields.insert("blocked".to_string(), Value::Bool(false));
        update_story_in_file(tmp.path(), "31_test", None, None, Some(&fields)).unwrap();

        let result = read_story_content(tmp.path(), "31_test").unwrap();
        assert!(
            result.contains("blocked: false"),
            "native bool false should be unquoted: {result}"
        );
        assert!(
            !result.contains("blocked: \"false\""),
            "must not be quoted: {result}"
        );
    }

    #[test]
    fn update_story_native_bool_true_written_unquoted() {
        let tmp = tempfile::tempdir().unwrap();
        setup_story_in_fs(tmp.path(), "32_test", "---\nname: T\n---\n\nNo sections.\n");

        let mut fields = HashMap::new();
        fields.insert("blocked".to_string(), Value::Bool(true));
        update_story_in_file(tmp.path(), "32_test", None, None, Some(&fields)).unwrap();

        let result = read_story_content(tmp.path(), "32_test").unwrap();
        assert!(
            result.contains("blocked: true"),
            "native bool true should be unquoted: {result}"
        );
        assert!(
            !result.contains("blocked: \"true\""),
            "must not be quoted: {result}"
        );
    }

    #[test]
    fn update_story_native_integer_written_unquoted() {
        let tmp = tempfile::tempdir().unwrap();
        setup_story_in_fs(
            tmp.path(),
            "33b_test",
            "---\nname: T\n---\n\nNo sections.\n",
        );

        let mut fields = HashMap::new();
        fields.insert("retry_count".to_string(), serde_json::json!(3));
        update_story_in_file(tmp.path(), "33b_test", None, None, Some(&fields)).unwrap();

        let result = read_story_content(tmp.path(), "33b_test").unwrap();
        assert!(
            result.contains("retry_count: 3"),
            "native integer should be unquoted: {result}"
        );
        assert!(
            !result.contains("retry_count: \"3\""),
            "must not be quoted: {result}"
        );
    }

    #[test]
    fn update_story_native_array_written_as_yaml_sequence() {
        let tmp = tempfile::tempdir().unwrap();
        setup_story_in_fs(tmp.path(), "34_test", "---\nname: T\n---\n\nNo sections.\n");

        let mut fields = HashMap::new();
        fields.insert("depends_on".to_string(), serde_json::json!([490, 491]));
        update_story_in_file(tmp.path(), "34_test", None, None, Some(&fields)).unwrap();

        let result = read_story_content(tmp.path(), "34_test").unwrap();
        assert!(
            result.contains("depends_on: [490, 491]"),
            "native array should be YAML sequence: {result}"
        );
        assert!(
            !result.contains("depends_on: \"["),
            "must not be quoted: {result}"
        );

        let meta = parse_front_matter(&result).expect("front matter should parse");
        assert_eq!(meta.depends_on, Some(vec![490, 491]));
    }

    #[test]
    fn update_story_native_bool_parseable_after_write() {
        let tmp = tempfile::tempdir().unwrap();
        setup_story_in_fs(
            tmp.path(),
            "35_test",
            "---\nname: My Story\n---\n\nNo sections.\n",
        );

        let mut fields = HashMap::new();
        fields.insert("blocked".to_string(), Value::Bool(false));
        update_story_in_file(tmp.path(), "35_test", None, None, Some(&fields)).unwrap();

        let contents = read_story_content(tmp.path(), "35_test").unwrap();
        let meta = parse_front_matter(&contents).expect("front matter should parse");
        assert_eq!(
            meta.name.as_deref(),
            Some("My Story"),
            "name preserved after writing native bool"
        );
    }

    #[test]
    fn update_story_depends_on_multi_element_array() {
        let tmp = tempfile::tempdir().unwrap();
        setup_story_in_fs(tmp.path(), "31_test", "---\nname: T\n---\n\nNo sections.\n");

        // String "[490, 491]" still works (backwards compatibility).
        let mut fields = HashMap::new();
        fields.insert(
            "depends_on".to_string(),
            Value::String("[490, 491]".to_string()),
        );
        update_story_in_file(tmp.path(), "31_test", None, None, Some(&fields)).unwrap();

        let result = read_story_content(tmp.path(), "31_test").unwrap();
        let meta = parse_front_matter(&result).expect("front matter should parse");
        assert_eq!(meta.depends_on, Some(vec![490, 491]));
    }
}
