//! Acceptance-criterion file edits: check / add / remove / edit by index.

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,
};

/// Toggle an acceptance criterion checkbox (`- [ ]` → `- [x]`) by its 0-based index among unchecked items.
#[allow(clippy::string_slice)] // indent_len = line.len() - trimmed.len(); trim() returns a sub-slice → valid boundary
pub fn check_criterion_in_file(
    project_root: &Path,
    story_id: &str,
    criterion_index: usize,
) -> Result<(), String> {
    let contents = read_story_content(project_root, story_id)?;

    let mut unchecked_count: usize = 0;
    let mut found = false;
    let new_lines: Vec<String> = contents
        .lines()
        .map(|line| {
            let trimmed = line.trim();
            if let Some(rest) = trimmed.strip_prefix("- [ ] ") {
                if unchecked_count == criterion_index {
                    unchecked_count += 1;
                    found = true;
                    let indent_len = line.len() - trimmed.len();
                    let indent = &line[..indent_len];
                    return format!("{indent}- [x] {rest}");
                }
                unchecked_count += 1;
            }
            line.to_string()
        })
        .collect();

    if !found {
        return Err(format!(
            "Criterion index {criterion_index} out of range. Story '{story_id}' has \
             {unchecked_count} unchecked criteria (indices 0..{}).",
            unchecked_count.saturating_sub(1)
        ));
    }

    let mut new_str = new_lines.join("\n");
    if contents.ends_with('\n') {
        new_str.push('\n');
    }

    // 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, &new_str, None);

    Ok(())
}

/// Remove an acceptance criterion from a story by its 0-based index (counting all criteria,
/// both checked and unchecked). Returns an error if the index is out of range.
pub fn remove_criterion_from_file(
    project_root: &Path,
    story_id: &str,
    criterion_index: usize,
) -> Result<(), String> {
    let contents = read_story_content(project_root, story_id)?;

    let mut count: usize = 0;
    let mut found = false;
    let new_lines: Vec<String> = contents
        .lines()
        .filter(|line| {
            let trimmed = line.trim();
            if trimmed.starts_with("- [ ] ") || trimmed.starts_with("- [x] ") {
                if count == criterion_index {
                    count += 1;
                    found = true;
                    return false;
                }
                count += 1;
            }
            true
        })
        .map(|s| s.to_string())
        .collect();

    if !found {
        return Err(format!(
            "Criterion index {criterion_index} out of range. Story '{story_id}' has \
             {count} criteria (indices 0..{}).",
            count.saturating_sub(1)
        ));
    }

    let mut new_str = new_lines.join("\n");
    if contents.ends_with('\n') {
        new_str.push('\n');
    }

    let stage = story_stage(story_id).unwrap_or_else(|| "2_current".to_string());
    write_story_content(project_root, story_id, &stage, &new_str, None);

    Ok(())
}

/// Edit the text of an existing acceptance criterion without changing its checked state.
///
/// Finds the criterion at `criterion_index` (0-based, counting all criteria regardless
/// of checked state) and replaces its text with `new_text`.
#[allow(clippy::string_slice)] // indent_len = line.len() - trimmed.len(); trim() returns a sub-slice → valid boundary
pub fn edit_criterion_in_file(
    project_root: &Path,
    story_id: &str,
    criterion_index: usize,
    new_text: &str,
) -> Result<(), String> {
    let contents = read_story_content(project_root, story_id)?;

    let mut count: usize = 0;
    let mut found = false;
    let new_lines: Vec<String> = contents
        .lines()
        .map(|line| {
            let trimmed = line.trim();
            let prefix = if trimmed.starts_with("- [ ] ") {
                Some("- [ ] ")
            } else if trimmed.starts_with("- [x] ") {
                Some("- [x] ")
            } else {
                None
            };
            if let Some(p) = prefix {
                if count == criterion_index {
                    count += 1;
                    found = true;
                    let indent_len = line.len() - trimmed.len();
                    let indent = &line[..indent_len];
                    return format!("{indent}{p}{new_text}");
                }
                count += 1;
            }
            line.to_string()
        })
        .collect();

    if !found {
        return Err(format!(
            "Criterion index {criterion_index} out of range. Story '{story_id}' has \
             {count} criteria (indices 0..{}).",
            count.saturating_sub(1)
        ));
    }

    let mut new_str = new_lines.join("\n");
    if contents.ends_with('\n') {
        new_str.push('\n');
    }

    let stage = story_stage(story_id).unwrap_or_else(|| "2_current".to_string());
    write_story_content(project_root, story_id, &stage, &new_str, None);

    Ok(())
}

/// Add a new acceptance criterion to a story.
///
/// Appends `- [ ] {criterion}` after the last existing criterion line in the
/// "## Acceptance Criteria" section.
pub fn add_criterion_to_file(
    project_root: &Path,
    story_id: &str,
    criterion: &str,
) -> Result<(), String> {
    let contents = read_story_content(project_root, story_id)?;

    let lines: Vec<&str> = contents.lines().collect();
    let mut in_ac_section = false;
    let mut ac_section_start: Option<usize> = None;
    let mut last_criterion_line: Option<usize> = None;

    for (i, line) in lines.iter().enumerate() {
        let trimmed = line.trim();
        if trimmed == "## Acceptance Criteria" {
            in_ac_section = true;
            ac_section_start = Some(i);
            continue;
        }
        if in_ac_section {
            if trimmed.starts_with("## ") {
                break;
            }
            if trimmed.starts_with("- [ ] ") || trimmed.starts_with("- [x] ") {
                last_criterion_line = Some(i);
            }
        }
    }

    let mut new_lines: Vec<String> = lines.iter().map(|s| s.to_string()).collect();

    let insert_after = if let Some(idx) = last_criterion_line.or(ac_section_start) {
        idx
    } else {
        // No ## Acceptance Criteria section — create one on demand.
        if new_lines.last().map(|l| !l.is_empty()).unwrap_or(false) {
            new_lines.push(String::new());
        }
        new_lines.push("## Acceptance Criteria".to_string());
        new_lines.push(String::new());
        new_lines.len() - 1
    };

    new_lines.insert(insert_after + 1, format!("- [ ] {criterion}"));

    let mut new_str = new_lines.join("\n");
    if contents.ends_with('\n') {
        new_str.push('\n');
    }

    // 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, &new_str, None);

    Ok(())
}

/// Encode a JSON value as a YAML scalar string.
///
/// Native JSON types map to native YAML types:
/// - Bool → unquoted `true`/`false`
/// - Integer → unquoted integer
/// - Float → unquoted float
/// - Array → unquoted inline sequence (e.g. `[490, 491]`)
/// - String → quoted unless it looks like a bool, integer, or inline sequence
#[cfg(test)]
mod tests {
    use super::*;
    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(crate::db::ContentKey::Story(story_id), content);
    }

    // --- create_story integration tests ---

    #[test]
    fn check_criterion_marks_first_unchecked() {
        let tmp = tempfile::tempdir().unwrap();
        setup_git_repo(tmp.path());
        setup_story_in_fs(tmp.path(), "1_test", &story_with_criteria(3));

        check_criterion_in_file(tmp.path(), "1_test", 0).unwrap();

        // Read the updated content.
        let contents = read_story_content(tmp.path(), "1_test").unwrap();
        assert!(
            contents.contains("- [x] Criterion 0"),
            "first should be checked"
        );
        assert!(
            contents.contains("- [ ] Criterion 1"),
            "second should stay unchecked"
        );
        assert!(
            contents.contains("- [ ] Criterion 2"),
            "third should stay unchecked"
        );
    }

    #[test]
    fn check_criterion_marks_second_unchecked() {
        let tmp = tempfile::tempdir().unwrap();
        setup_git_repo(tmp.path());
        setup_story_in_fs(tmp.path(), "2_test", &story_with_criteria(3));

        check_criterion_in_file(tmp.path(), "2_test", 1).unwrap();

        let contents = read_story_content(tmp.path(), "2_test").unwrap();
        assert!(
            contents.contains("- [ ] Criterion 0"),
            "first should stay unchecked"
        );
        assert!(
            contents.contains("- [x] Criterion 1"),
            "second should be checked"
        );
        assert!(
            contents.contains("- [ ] Criterion 2"),
            "third should stay unchecked"
        );
    }

    #[test]
    fn check_criterion_out_of_range_returns_error() {
        let tmp = tempfile::tempdir().unwrap();
        setup_git_repo(tmp.path());
        setup_story_in_fs(tmp.path(), "3_test", &story_with_criteria(2));

        let result = check_criterion_in_file(tmp.path(), "3_test", 5);
        assert!(result.is_err(), "should fail for out-of-range index");
        assert!(result.unwrap_err().contains("out of range"));
    }

    // ── add_criterion_to_file tests ───────────────────────────────────────────

    #[allow(dead_code)]
    fn story_with_ac_section(criteria: &[&str]) -> String {
        let mut s =
            "---\nname: Test\n---\n\n## User Story\n\nAs a user...\n\n## Acceptance Criteria\n\n"
                .to_string();
        for c in criteria {
            s.push_str(&format!("- [ ] {c}\n"));
        }
        s.push_str("\n## Out of Scope\n\n- N/A\n");
        s
    }

    #[test]
    fn add_criterion_appends_after_last_criterion() {
        let tmp = tempfile::tempdir().unwrap();
        setup_story_in_fs(
            tmp.path(),
            "10_test",
            &story_with_ac_section(&["First", "Second"]),
        );

        add_criterion_to_file(tmp.path(), "10_test", "Third").unwrap();

        let contents = read_story_content(tmp.path(), "10_test").unwrap();
        assert!(contents.contains("- [ ] First\n"));
        assert!(contents.contains("- [ ] Second\n"));
        assert!(contents.contains("- [ ] Third\n"));
        let pos_second = contents.find("- [ ] Second").unwrap();
        let pos_third = contents.find("- [ ] Third").unwrap();
        assert!(pos_third > pos_second, "Third should appear after Second");
    }

    #[test]
    fn add_criterion_to_empty_section() {
        let tmp = tempfile::tempdir().unwrap();
        let content =
            "---\nname: Test\n---\n\n## Acceptance Criteria\n\n## Out of Scope\n\n- N/A\n";
        setup_story_in_fs(tmp.path(), "11_test", content);

        add_criterion_to_file(tmp.path(), "11_test", "New AC").unwrap();

        let contents = read_story_content(tmp.path(), "11_test").unwrap();
        assert!(
            contents.contains("- [ ] New AC\n"),
            "criterion should be present"
        );
    }

    #[test]
    fn add_criterion_creates_section_when_missing() {
        // Bug 625: adding a criterion to a story that has no ## Acceptance Criteria
        // section (e.g. a spike converted to a story) must auto-create the section.
        let tmp = tempfile::tempdir().unwrap();
        setup_story_in_fs(
            tmp.path(),
            "12_test",
            "---\nname: Test\n---\n\nNo AC section here.\n",
        );

        let result = add_criterion_to_file(tmp.path(), "12_test", "X");
        assert!(
            result.is_ok(),
            "should succeed by creating the section: {result:?}"
        );

        let contents = read_story_content(tmp.path(), "12_test").unwrap();
        assert!(
            contents.contains("## Acceptance Criteria"),
            "section should be created"
        );
        assert!(contents.contains("- [ ] X"), "criterion should be present");
    }

    /// Bug 625: spike-to-story conversion + AC addition end-to-end.
    ///
    /// Simulates: create spike content (no AC section), convert to story via
    /// update_story front_matter type field, then add_criterion three times,
    #[test]
    fn spike_converted_to_story_accepts_add_criterion() {
        let tmp = tempfile::tempdir().unwrap();

        // Spike content: no ## Acceptance Criteria section.
        let spike_content = "---\nname: \"My Spike\"\n---\n\n\
                             ## Question\n\n- What is the best approach?\n\n\
                             ## Hypothesis\n\n- TBD\n\n\
                             ## Findings\n\n- TBD\n\n\
                             ## Recommendation\n\n- TBD\n";
        setup_story_in_fs(tmp.path(), "100_spike_my_spike", spike_content);

        // Convert spike to story by updating the typed item_type CRDT register.
        crate::crdt_state::set_item_type(
            "100_spike_my_spike",
            Some(crate::io::story_metadata::ItemType::Story),
        );

        // Add three acceptance criteria.
        add_criterion_to_file(tmp.path(), "100_spike_my_spike", "First criterion")
            .expect("add first criterion");
        add_criterion_to_file(tmp.path(), "100_spike_my_spike", "Second criterion")
            .expect("add second criterion");
        add_criterion_to_file(tmp.path(), "100_spike_my_spike", "Third criterion")
            .expect("add third criterion");

        let contents = read_story_content(tmp.path(), "100_spike_my_spike").unwrap();
        assert!(
            contents.contains("## Acceptance Criteria"),
            "AC section should be present"
        );
        assert!(
            contents.contains("- [ ] First criterion"),
            "first AC missing"
        );
        assert!(
            contents.contains("- [ ] Second criterion"),
            "second AC missing"
        );
        assert!(
            contents.contains("- [ ] Third criterion"),
            "third AC missing"
        );
    }

    #[test]
    fn remove_criterion_removes_by_index() {
        let tmp = tempfile::tempdir().unwrap();
        setup_story_in_fs(
            tmp.path(),
            "20_remove_test",
            &story_with_ac_section(&["First", "Second", "Third"]),
        );

        remove_criterion_from_file(tmp.path(), "20_remove_test", 1).unwrap();

        let contents = read_story_content(tmp.path(), "20_remove_test").unwrap();
        assert!(contents.contains("- [ ] First"), "First should remain");
        assert!(!contents.contains("Second"), "Second should be removed");
        assert!(contents.contains("- [ ] Third"), "Third should remain");
    }

    #[test]
    fn remove_criterion_shifts_indices() {
        let tmp = tempfile::tempdir().unwrap();
        setup_story_in_fs(
            tmp.path(),
            "21_remove_test",
            &story_with_ac_section(&["A", "B", "C"]),
        );

        remove_criterion_from_file(tmp.path(), "21_remove_test", 0).unwrap();

        let contents = read_story_content(tmp.path(), "21_remove_test").unwrap();
        assert!(!contents.contains("- [ ] A"), "A should be removed");
        assert!(contents.contains("- [ ] B"), "B should remain");
        assert!(contents.contains("- [ ] C"), "C should remain");
        // B is now at index 0, C at index 1 — verify by removing B next
        remove_criterion_from_file(tmp.path(), "21_remove_test", 0).unwrap();
        let contents2 = read_story_content(tmp.path(), "21_remove_test").unwrap();
        assert!(!contents2.contains("- [ ] B"), "B should now be removed");
        assert!(contents2.contains("- [ ] C"), "C should still remain");
    }

    #[test]
    fn remove_criterion_out_of_range_returns_error() {
        let tmp = tempfile::tempdir().unwrap();
        setup_story_in_fs(
            tmp.path(),
            "22_remove_test",
            &story_with_ac_section(&["Only"]),
        );

        let result = remove_criterion_from_file(tmp.path(), "22_remove_test", 5);
        assert!(result.is_err(), "should fail for out-of-range index");
        assert!(result.unwrap_err().contains("out of range"));
    }

    // ── update_story_in_file tests ─────────────────────────────────────────────
}
