//! Edge-case integration tests — exercises parser, RefResolver, Store, and element logic
//! against purpose-built fixture files that stress corner cases.

use std::collections::HashMap;
use std::path::{Path, PathBuf};
use chrono::{Datelike, NaiveDate};
use indexmap::IndexMap;
use mps::{elements::*, parser, ref_resolver::RefResolver, store::Store};

// ── Helpers ──────────────────────────────────────────────────────────────────

fn fixtures_dir() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR")).join("tests").join("fixtures")
}

fn fixture_path(name: &str) -> PathBuf {
    fixtures_dir().join(name)
}

fn parse_fixture(name: &str) -> IndexMap<String, Element> {
    parser::parse_file(&fixture_path(name)).expect("fixture parse failed")
}

fn non_root(els: &IndexMap<String, Element>) -> Vec<&Element> {
    els.values().filter(|e| !e.is_mps_group() && !e.is_unknown()).collect()
}

fn fixture_store() -> (tempfile::TempDir, Store) {
    let dir = tempfile::tempdir().unwrap();
    for entry in std::fs::read_dir(fixtures_dir()).unwrap() {
        let entry = entry.unwrap();
        let dst = dir.path().join(entry.file_name());
        std::fs::copy(entry.path(), dst).unwrap();
    }
    let store = Store::new(dir.path());
    (dir, store)
}

fn make_date(y: i32, m: u32, d: u32) -> NaiveDate {
    NaiveDate::from_ymd_opt(y, m, d).unwrap()
}

// ═══════════════════════════════════════════════════════════════════════════
// ITERATION 1: Empty bodies and empty-bracket elements
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn test_empty_body_task_is_parsed() {
    // @task[]{} — empty body, empty brackets
    let els = parse_fixture("20260301.1000000100.mps");
    assert!(els.values().any(|e| e.kind() == ElementKind::Task),
        "should parse task with empty body");
}

#[test]
fn test_empty_body_body_str_is_empty_or_whitespace() {
    let els = parse_fixture("20260301.1000000100.mps");
    let task = els.values().find(|e| e.kind() == ElementKind::Task).unwrap();
    assert!(task.body_str().trim().is_empty(), "empty-body task body should be empty");
}

#[test]
fn test_empty_body_log_has_duration() {
    // @log[start: 09:00, end: 10:00]{} — duration should still be computed
    let els = parse_fixture("20260301.1000000100.mps");
    let log = els.values().find(|e| e.kind() == ElementKind::Log).unwrap();
    if let Element::Log { data, .. } = log {
        assert_eq!(data.duration_minutes(), Some(60), "09:00–10:00 = 60 min");
        assert_eq!(data.duration_str(), Some("1h".to_string()));
    } else {
        panic!("expected Log");
    }
}

#[test]
fn test_empty_body_reminder_at_noon() {
    let els = parse_fixture("20260301.1000000100.mps");
    let rem = els.values().find(|e| e.kind() == ElementKind::Reminder).unwrap();
    if let Element::Reminder { data, .. } = rem {
        assert_eq!(data.at.as_deref(), Some("noon"));
    } else {
        panic!("expected Reminder");
    }
}

#[test]
fn test_empty_body_note_has_no_tags() {
    let els = parse_fixture("20260301.1000000100.mps");
    let note = els.values().find(|e| e.kind() == ElementKind::Note).unwrap();
    assert!(note.tags().is_empty(), "bare @note has no tags");
}

#[test]
fn test_empty_bracket_task_raw_args_is_empty() {
    // @task[]{} — empty bracket → raw_args should be ""
    let els = parse_fixture("20260301.1000000100.mps");
    let task = els.values().find(|e| e.kind() == ElementKind::Task).unwrap();
    assert_eq!(task.raw_args(), "", "empty bracket → raw_args is empty string");
}

// ═══════════════════════════════════════════════════════════════════════════
// ITERATION 2: Deep nesting — 4 levels
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn test_deep_nesting_4_levels_parsed() {
    let els = parse_fixture("20260302.1000000101.mps");
    // Structure: root_mps → mps.1 → mps.1.1 → mps.1.1.1 → task.1.1.1.1
    // base_ref = 20260302
    assert!(els.contains_key("20260302.1"),         "level-1 mps block");
    assert!(els.contains_key("20260302.1.1"),       "level-2 mps block");
    assert!(els.contains_key("20260302.1.1.1"),     "level-3 mps block");
    assert!(els.contains_key("20260302.1.1.1.1"),   "level-4 task");
    assert!(els.contains_key("20260302.1.1.1.2"),   "level-4 note");
    assert!(els.contains_key("20260302.1.1.2"),     "level-3 task");
    assert!(els.contains_key("20260302.1.2"),       "level-2 task");
}

#[test]
fn test_deep_nesting_top_level_tasks() {
    let els = parse_fixture("20260302.1000000101.mps");
    let top_task = els.get("20260302.2").expect("top-level task at .2");
    assert_eq!(top_task.kind(), ElementKind::Task);
    assert!(top_task.body_str().contains("Top level outer"));
}

#[test]
fn test_deep_nesting_ref_resolver_4_levels() {
    let els = parse_fixture("20260302.1000000101.mps");
    let r = RefResolver::new(&els);
    // mps-1 is the outermost @mps block
    assert_eq!(r.to_human("20260302.1"), Some("mps-1"));
    // task inside mps-1 at position 2 = mps-1.2
    assert_eq!(r.to_human("20260302.1.2"), Some("mps-1.2"));
    // mps block at position 1 inside mps-1 = mps-1.1 (mps-2 if top-level counting, but it's nested)
    // Actually mps blocks are counted as "mps-N" at top level but nested as parent.N
    assert_eq!(r.to_human("20260302.1.1"), Some("mps-1.1"));
    assert_eq!(r.to_human("20260302.1.1.1"), Some("mps-1.1.1"));
    assert_eq!(r.to_human("20260302.1.1.1.1"), Some("mps-1.1.1.1"));
}

#[test]
fn test_deep_nesting_5_levels() {
    let els = parse_fixture("20260313.1000000112.mps");
    // root → mps.1 → mps.1.1 → mps.1.1.1 → mps.1.1.1.1 → task.1.1.1.1.1
    assert!(els.contains_key("20260313.1.1.1.1.1"), "5-level task ref");
    let deep = els.get("20260313.1.1.1.1.1").unwrap();
    assert!(deep.body_str().contains("Five levels deep"));
}

#[test]
fn test_deep_nesting_ref_resolver_5_levels() {
    let els = parse_fixture("20260313.1000000112.mps");
    let r = RefResolver::new(&els);
    assert_eq!(r.to_human("20260313.1.1.1.1.1"), Some("mps-1.1.1.1.1"));
    // Roundtrip
    assert_eq!(r.to_epoch("mps-1.1.1.1.1"), Some("20260313.1.1.1.1.1"));
}

// ═══════════════════════════════════════════════════════════════════════════
// ITERATION 3: Many same-type elements — counter correctness
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn test_many_same_type_5_tasks() {
    let els = parse_fixture("20260303.1000000102.mps");
    let task_count = els.values().filter(|e| e.kind() == ElementKind::Task).count();
    assert_eq!(task_count, 5, "should have exactly 5 tasks");
}

#[test]
fn test_many_same_type_3_notes() {
    let els = parse_fixture("20260303.1000000102.mps");
    let note_count = els.values().filter(|e| e.kind() == ElementKind::Note).count();
    assert_eq!(note_count, 3);
}

#[test]
fn test_many_same_type_ref_resolver_counters() {
    let els = parse_fixture("20260303.1000000102.mps");
    let r = RefResolver::new(&els);
    assert_eq!(r.to_epoch("task-5"), Some("20260303.5"), "5th task maps to .5");
    assert_eq!(r.to_epoch("note-3"), Some("20260303.8"), "3rd note maps to .8 (after 5 tasks)");
    assert_eq!(r.to_epoch("log-2"), Some("20260303.10"), "2nd log maps to .10");
}

#[test]
fn test_many_same_type_done_count() {
    let els = parse_fixture("20260303.1000000102.mps");
    let done = els.values()
        .filter_map(|e| if let Element::Task { data, .. } = e { Some(data) } else { None })
        .filter(|d| d.is_done())
        .count();
    assert_eq!(done, 2, "tasks 4 and 5 are done");
}

// ═══════════════════════════════════════════════════════════════════════════
// ITERATION 4: Tags with hyphens, underscores, and numbers
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn test_hyphen_tags_parsed_correctly() {
    let els = parse_fixture("20260304.1000000103.mps");
    let task = els.values().find(|e| e.kind() == ElementKind::Task).unwrap();
    let tags = task.tags();
    assert!(tags.contains(&"mps-dev".to_string()), "hyphenated tag mps-dev");
    assert!(tags.contains(&"work_item".to_string()), "underscore tag work_item");
    assert!(tags.contains(&"v2".to_string()), "alphanumeric tag v2");
}

#[test]
fn test_hyphen_tag_in_log() {
    let els = parse_fixture("20260304.1000000103.mps");
    let log = els.values().find(|e| e.kind() == ElementKind::Log).unwrap();
    assert!(log.tags().contains(&"feature-123".to_string()), "feature-123 tag");
    if let Element::Log { data, .. } = log {
        assert_eq!(data.duration_str(), Some("2h30m".to_string()), "09:00–11:30 = 2h30m");
    }
}

#[test]
fn test_hyphen_tag_in_reminder() {
    let els = parse_fixture("20260304.1000000103.mps");
    let rem = els.values().find(|e| e.kind() == ElementKind::Reminder).unwrap();
    assert!(rem.tags().contains(&"daily-standup".to_string()), "hyphenated reminder tag");
    if let Element::Reminder { data, .. } = rem {
        assert_eq!(data.at.as_deref(), Some("3pm"));
    }
}

#[test]
fn test_hyphen_tags_in_store_search() {
    let (_dir, store) = fixture_store();
    let results = store.search("", None, Some("mps-dev"), None).unwrap();
    assert!(!results.is_empty(), "search by hyphenated tag should find results");
}

// ═══════════════════════════════════════════════════════════════════════════
// ITERATION 5: Log duration edge cases
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn test_log_near_full_day_duration() {
    let els = parse_fixture("20260305.1000000104.mps");
    // 00:00 to 23:59 = 1439 min = 23h59m
    let log = els.values()
        .filter_map(|e| if let Element::Log { data, .. } = e { Some(data) } else { None })
        .find(|d| d.start.as_deref() == Some("00:00"))
        .unwrap();
    assert_eq!(log.duration_minutes(), Some(1439));
    assert_eq!(log.duration_str(), Some("23h59m".to_string()));
}

#[test]
fn test_log_zero_duration() {
    let els = parse_fixture("20260305.1000000104.mps");
    let log = els.values()
        .filter_map(|e| if let Element::Log { data, .. } = e { Some(data) } else { None })
        .find(|d| d.start.as_deref() == Some("08:00") && d.end.as_deref() == Some("08:00"))
        .unwrap();
    assert_eq!(log.duration_minutes(), Some(0));
    assert_eq!(log.duration_str(), None, "zero duration → no duration_str");
}

#[test]
fn test_log_only_start_no_duration() {
    let els = parse_fixture("20260305.1000000104.mps");
    let log = els.values()
        .filter_map(|e| if let Element::Log { data, .. } = e { Some(data) } else { None })
        .find(|d| d.start.is_some() && d.end.is_none())
        .unwrap();
    assert_eq!(log.duration_minutes(), None);
    assert_eq!(log.duration_str(), None);
}

#[test]
fn test_log_only_end_no_duration() {
    let els = parse_fixture("20260305.1000000104.mps");
    let log = els.values()
        .filter_map(|e| if let Element::Log { data, .. } = e { Some(data) } else { None })
        .find(|d| d.start.is_none() && d.end.is_some())
        .unwrap();
    assert_eq!(log.duration_minutes(), None);
}

#[test]
fn test_log_45_minutes() {
    let els = parse_fixture("20260305.1000000104.mps");
    // 09:30 to 10:15 = 45 min
    let log = els.values()
        .filter_map(|e| if let Element::Log { data, .. } = e { Some(data) } else { None })
        .find(|d| d.start.as_deref() == Some("09:30"))
        .unwrap();
    assert_eq!(log.duration_minutes(), Some(45));
    assert_eq!(log.duration_str(), Some("0h45m".to_string()));
}

#[test]
fn test_log_no_times_no_duration() {
    let els = parse_fixture("20260305.1000000104.mps");
    let log = els.values()
        .filter_map(|e| if let Element::Log { data, .. } = e { Some(data) } else { None })
        .find(|d| d.start.is_none() && d.end.is_none() && d.tags.contains(&"work".to_string()))
        .unwrap();
    assert_eq!(log.duration_minutes(), None);
}

// ═══════════════════════════════════════════════════════════════════════════
// ITERATION 6: Mixed unknown elements
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn test_unknown_elements_are_parsed_as_unknown() {
    let els = parse_fixture("20260306.1000000105.mps");
    let unknowns: Vec<_> = els.values().filter(|e| e.is_unknown()).collect();
    assert_eq!(unknowns.len(), 4, "widget, gadget, sprocket, thingamajig — 4 unknowns");
}

#[test]
fn test_unknown_elements_counted_correctly() {
    let els = parse_fixture("20260306.1000000105.mps");
    let unknowns: Vec<_> = els.values().filter(|e| e.is_unknown()).collect();
    // widget, gadget, sprocket, thingamajig
    assert_eq!(unknowns.len(), 4);
    let signs: Vec<&str> = unknowns.iter().map(|e| e.sign()).collect();
    assert!(signs.contains(&"widget"));
    assert!(signs.contains(&"gadget"));
    assert!(signs.contains(&"thingamajig"));
}

#[test]
fn test_unknown_elements_excluded_from_non_root() {
    let els = parse_fixture("20260306.1000000105.mps");
    let visible = non_root(&els);
    // Only task, note, log are visible — 3 known elements
    assert_eq!(visible.len(), 3);
    assert!(visible.iter().all(|e| !e.is_unknown()));
}

#[test]
fn test_unknown_elements_ref_resolver_skips_them() {
    let els = parse_fixture("20260306.1000000105.mps");
    let r = RefResolver::new(&els);
    // widget is at .1, task at .2 → task should be task-1 (not task-2)
    assert!(r.to_human("20260306.1").is_none(), "widget (.1) skipped");
    assert_eq!(r.to_human("20260306.2"), Some("task-1"), "task at .2 becomes task-1");
    assert!(r.to_human("20260306.3").is_none(), "gadget (.3) skipped");
    assert_eq!(r.to_human("20260306.4"), Some("note-1"), "note at .4 becomes note-1");
    assert!(r.to_human("20260306.5").is_none(), "sprocket (.5) skipped");
    assert_eq!(r.to_human("20260306.6"), Some("log-1"), "log at .6 becomes log-1");
    assert!(r.to_human("20260306.7").is_none(), "thingamajig (.7) skipped");
}

#[test]
fn test_unknown_elements_excluded_from_store_search() {
    let (_dir, store) = fixture_store();
    // Search for "Unknown" which appears in the unknown elements' body
    let results = store.search("Unknown element", None, None, None).unwrap();
    assert!(results.iter().all(|r| !r.element.is_unknown()),
        "search should never return unknown elements");
}

#[test]
fn test_sprocket_has_own_raw_args() {
    let els = parse_fixture("20260306.1000000105.mps");
    let sprocket = els.values().find(|e| e.sign() == "sprocket").unwrap();
    assert_eq!(sprocket.raw_args(), "cog, status: spinning");
}

// ═══════════════════════════════════════════════════════════════════════════
// ITERATION 7: Unicode and special chars in body
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn test_unicode_body_preserved() {
    let els = parse_fixture("20260307.1000000106.mps");
    let note = els.values().find(|e| e.kind() == ElementKind::Note).unwrap();
    assert!(note.body_str().contains("日本語テキスト"), "CJK text preserved");
    assert!(note.body_str().contains("🎯"), "emoji preserved");
    assert!(note.body_str().contains("مرحبا"), "Arabic text preserved");
}

#[test]
fn test_at_sign_in_body_not_parsed_as_element() {
    let els = parse_fixture("20260307.1000000106.mps");
    // @john.doe and @example.com in body should NOT create unknown elements
    // (because they lack the { that at_regexp requires)
    let unknowns: Vec<_> = els.values().filter(|e| e.is_unknown()).collect();
    assert!(unknowns.is_empty(), "@ without {{ should not create elements");
    // Total: root mps + note + task + log = 4 elements
    assert_eq!(els.len(), 4);
}

#[test]
fn test_dollar_in_body_does_not_break_parsing() {
    let els = parse_fixture("20260307.1000000106.mps");
    let log = els.values().find(|e| e.kind() == ElementKind::Log).unwrap();
    assert!(log.body_str().contains("$50.00"), "dollar sign preserved in body");
    assert!(log.body_str().contains("$1,200"), "dollar sign with comma preserved");
}

#[test]
fn test_unicode_search() {
    let (_dir, store) = fixture_store();
    let results = store.search("日本語", None, None, None).unwrap();
    assert!(!results.is_empty(), "should find CJK query");
    assert!(results[0].element.body_str().contains("日本語テキスト"));
}

#[test]
fn test_special_chars_in_body_dont_affect_count() {
    let els = parse_fixture("20260307.1000000106.mps");
    // root mps + note + task + log = 4 elements
    assert_eq!(els.len(), 4, "special body chars should not create extra elements");
}

#[test]
fn test_rewrite_element_with_dollar_in_body() {
    // Ensure rewrite_element works when the log element has $ in its body
    let (dir, store) = fixture_store();
    let date = make_date(2026, 3, 7);
    let mut new_attrs = HashMap::new();
    new_attrs.insert("start".to_string(), "10:00".to_string());
    // The log at 20260307 already has start:10:00 end:11:00 — update end time
    new_attrs.insert("end".to_string(), "12:00".to_string());
    // Should not panic even though body has $ signs
    let result = store.rewrite_element("log-1", &new_attrs, date);
    assert!(result.is_ok(), "rewrite with $ in body should not error");
    drop(dir);
}

// ═══════════════════════════════════════════════════════════════════════════
// ITERATION 8: Duplicate raw_args — which element gets rewritten?
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn test_duplicate_raw_args_file_parses_all_tasks() {
    let els = parse_fixture("20260308.1000000107.mps");
    let tasks: Vec<_> = els.values().filter(|e| e.kind() == ElementKind::Task).collect();
    assert_eq!(tasks.len(), 5, "5 tasks total");
}

#[test]
fn test_duplicate_raw_args_ref_resolver_assigns_unique_refs() {
    let els = parse_fixture("20260308.1000000107.mps");
    let r = RefResolver::new(&els);
    // All 5 tasks should get unique human refs
    assert_eq!(r.to_epoch("task-1"), Some("20260308.1"));
    assert_eq!(r.to_epoch("task-2"), Some("20260308.2"));
    assert_eq!(r.to_epoch("task-3"), Some("20260308.3"));
    assert_eq!(r.to_epoch("task-4"), Some("20260308.4"));
    assert_eq!(r.to_epoch("task-5"), Some("20260308.5"));
}

#[test]
fn test_rewrite_first_of_duplicate_raw_args() {
    // task-1 and task-2 both have raw_args "work, status: open".
    // Rewriting task-2 (epoch 20260308.2) by epoch ref should update the SECOND occurrence.
    let (dir, store) = fixture_store();
    let date = make_date(2026, 3, 8);
    let mut new_attrs = HashMap::new();
    new_attrs.insert("status".to_string(), "done".to_string());

    // Update by epoch ref (unambiguous)
    let ok = store.rewrite_element("20260308.1", &new_attrs, date).unwrap();
    assert!(ok, "rewrite by epoch ref should succeed");

    // Read back and verify: task-1 body should now show status:done
    let file = store.find_file(date).unwrap();
    let content = std::fs::read_to_string(&file).unwrap();
    // The first task body is "First open work task"
    // After rewrite, "First open work task" should be in a done task context
    assert!(content.contains("status: done"), "first task now done");
    drop(dir);
}

// ═══════════════════════════════════════════════════════════════════════════
// ITERATION 9: Complex nested structures and RefResolver
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn test_complex_nested_all_elements_parsed() {
    let els = parse_fixture("20260309.1000000108.mps");
    // root mps + mps[project-alpha] + 2 tasks in mps + mps[sub-milestone] + task in sub + note in sub
    // + note[retrospective] + log = 9 elements
    assert!(els.len() >= 8, "should have at least 8 elements, got {}", els.len());
}

#[test]
fn test_complex_nested_ref_resolver_3_levels() {
    let els = parse_fixture("20260309.1000000108.mps");
    let r = RefResolver::new(&els);
    // project-alpha mps block = mps-1
    assert_eq!(r.to_human("20260309.1"), Some("mps-1"));
    // First task inside mps-1 = mps-1.1
    assert_eq!(r.to_human("20260309.1.1"), Some("mps-1.1"));
    // sub-milestone mps block = mps-1.3
    assert_eq!(r.to_human("20260309.1.3"), Some("mps-1.3"));
    // task inside sub-milestone = mps-1.3.1
    assert_eq!(r.to_human("20260309.1.3.1"), Some("mps-1.3.1"));
    // note inside sub-milestone = mps-1.3.2
    assert_eq!(r.to_human("20260309.1.3.2"), Some("mps-1.3.2"));
}

#[test]
fn test_complex_nested_top_level_elements() {
    let els = parse_fixture("20260309.1000000108.mps");
    let r = RefResolver::new(&els);
    // note[retrospective] is after the mps block → note-1
    assert_eq!(r.to_human("20260309.2"), Some("note-1"));
    // log is after note → log-1
    assert_eq!(r.to_human("20260309.3"), Some("log-1"));
}

#[test]
fn test_complex_nested_log_full_day() {
    let els = parse_fixture("20260309.1000000108.mps");
    let log = els.values().find(|e| e.kind() == ElementKind::Log).unwrap();
    if let Element::Log { data, .. } = log {
        // 09:00 to 17:30 = 510 minutes = 8h30m
        assert_eq!(data.duration_minutes(), Some(510));
        assert_eq!(data.duration_str(), Some("8h30m".to_string()));
    }
}

#[test]
fn test_complex_nested_rewrite_nested_task_by_human_ref() {
    let (dir, store) = fixture_store();
    let date = make_date(2026, 3, 9);
    let mut new_attrs = HashMap::new();
    new_attrs.insert("status".to_string(), "done".to_string());
    // mps-1.3.1 is the task inside the sub-milestone
    let ok = store.rewrite_element("mps-1.3.1", &new_attrs, date).unwrap();
    assert!(ok, "should rewrite a 3-level-deep task by human ref");
    let file = store.find_file(date).unwrap();
    let content = std::fs::read_to_string(&file).unwrap();
    assert!(content.contains("Write API integration tests"), "body preserved");
    drop(dir);
}

// ═══════════════════════════════════════════════════════════════════════════
// ITERATION 10: Epoch-less filenames
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn test_epoch_less_file_included_in_all_files() {
    let (_dir, store) = fixture_store();
    let files = store.all_files().unwrap();
    let has_epoch_less = files.iter().any(|p| {
        p.file_name().and_then(|n| n.to_str()) == Some("20260310.mps")
    });
    assert!(has_epoch_less, "epoch-less file should be in all_files()");
}

#[test]
fn test_epoch_less_file_found_by_find_file() {
    let (_dir, store) = fixture_store();
    let date = make_date(2026, 3, 10);
    let found = store.find_file(date);
    assert!(found.is_some(), "find_file should find epoch-less file");
    assert!(found.unwrap().to_str().unwrap().ends_with("20260310.mps"));
}

#[test]
fn test_epoch_less_file_parses_elements() {
    let els = parse_fixture("20260310.mps");
    // base_ref extracted from basename: "20260310.mps"[..8] = "20260310" → 20260310
    assert!(els.values().any(|e| e.kind() == ElementKind::Task));
    assert!(els.values().any(|e| e.kind() == ElementKind::Note));
}

#[test]
fn test_epoch_less_file_date_in_all_file_dates() {
    let (_dir, store) = fixture_store();
    let dates = store.all_file_dates().unwrap();
    let d = make_date(2026, 3, 10);
    assert!(dates.contains(&d), "epoch-less file date appears in all_file_dates");
}

#[test]
fn test_epoch_less_file_parse_date() {
    let (_dir, store) = fixture_store();
    let date = make_date(2026, 3, 10);
    let els = store.parse_date(date).unwrap();
    assert!(els.values().any(|e| e.kind() == ElementKind::Task));
}

// ═══════════════════════════════════════════════════════════════════════════
// ITERATION 11: Whitespace in args — trimming robustness
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn test_whitespace_args_task_tag_trimmed() {
    let els = parse_fixture("20260311.1000000110.mps");
    // @task[  work  ,   status: open  ]
    let tasks: Vec<_> = els.values().filter(|e| e.kind() == ElementKind::Task).collect();
    assert!(tasks.iter().any(|t| t.tags().contains(&"work".to_string())),
        "tag 'work' should be trimmed of spaces");
}

#[test]
fn test_whitespace_args_task_status_trimmed() {
    let els = parse_fixture("20260311.1000000110.mps");
    // @task[  work  ,   status: open  ] — status should be parsed as "open"
    let tasks: Vec<_> = els.values()
        .filter_map(|e| if let Element::Task { data, .. } = e { Some(data) } else { None })
        .collect();
    assert!(tasks.iter().any(|d| d.is_open()), "status: open with spaces should parse correctly");
}

#[test]
fn test_whitespace_only_args_treated_as_no_args() {
    let els = parse_fixture("20260311.1000000110.mps");
    // @note[   ] — whitespace-only args should yield no tags, no attrs
    let note = els.values().find(|e| e.kind() == ElementKind::Note).unwrap();
    assert!(note.tags().is_empty(), "whitespace-only args → no tags");
}

#[test]
fn test_whitespace_log_times_trimmed() {
    let els = parse_fixture("20260311.1000000110.mps");
    // @log[ start: 09:00 , end: 10:00 ] — times should still parse
    let log = els.values().find(|e| e.kind() == ElementKind::Log).unwrap();
    if let Element::Log { data, .. } = log {
        assert_eq!(data.duration_minutes(), Some(60), "09:00–10:00 = 60 min even with spaces");
    }
}

#[test]
fn test_whitespace_args_reversed_order() {
    let els = parse_fixture("20260311.1000000110.mps");
    // @task[ status: done , work ] — status first, then tag
    let done_task = els.values()
        .filter_map(|e| if let Element::Task { data, .. } = e { Some(data) } else { None })
        .find(|d| d.is_done());
    assert!(done_task.is_some(), "status: done before tag should parse correctly");
    let done_data = done_task.unwrap();
    assert!(done_data.tags.contains(&"work".to_string()), "tag after status attr");
}

// ═══════════════════════════════════════════════════════════════════════════
// ITERATION 12: @ signs in body without { — not parsed as elements
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn test_at_in_body_without_brace_not_parsed() {
    let els = parse_fixture("20260312.1000000111.mps");
    // Body contains @john.doe, @example.com, @deprecated, etc. — none should become elements
    let unknowns: Vec<_> = els.values().filter(|e| e.is_unknown()).collect();
    assert!(unknowns.is_empty(), "@ without {{ must not create Unknown elements");
    // root mps + note + task + log = 4 total
    assert_eq!(els.len(), 4, "only 3 real elements + root mps");
}

#[test]
fn test_at_in_body_body_preserved() {
    let els = parse_fixture("20260312.1000000111.mps");
    let note = els.values().find(|e| e.kind() == ElementKind::Note).unwrap();
    assert!(note.body_str().contains("@john.doe"), "@john.doe preserved in body");
    assert!(note.body_str().contains("@deprecated"), "@deprecated preserved");
}

#[test]
fn test_at_in_body_url_not_parsed_as_element() {
    let els = parse_fixture("20260312.1000000111.mps");
    let note = els.values().find(|e| e.kind() == ElementKind::Note).unwrap();
    assert!(note.body_str().contains("https://github.com"), "URL preserved in body");
}

// ═══════════════════════════════════════════════════════════════════════════
// Cross-fixture store integration
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn test_store_all_files_includes_new_fixtures() {
    let (_dir, store) = fixture_store();
    let files = store.all_files().unwrap();
    // Original 8 + new 13 fixtures = 21 total (some may be epoch-less)
    assert!(files.len() >= 18, "all fixture files should be found, got {}", files.len());
}

#[test]
fn test_store_all_file_dates_includes_new_dates() {
    let (_dir, store) = fixture_store();
    let dates = store.all_file_dates().unwrap();
    let new_dates = [
        make_date(2026, 3, 1),
        make_date(2026, 3, 2),
        make_date(2026, 3, 10), // epoch-less
        make_date(2026, 3, 12),
    ];
    for d in &new_dates {
        assert!(dates.contains(d), "date {} should be in all_file_dates", d);
    }
}

#[test]
fn test_store_search_across_all_fixtures_unicode() {
    let (_dir, store) = fixture_store();
    let results = store.search("Unicode", None, None, None).unwrap();
    assert!(!results.is_empty(), "should find Unicode in notes");
}

#[test]
fn test_store_search_tag_filter_hyphenated() {
    let (_dir, store) = fixture_store();
    let results = store.search("", None, Some("feature-123"), None).unwrap();
    assert!(!results.is_empty(), "should find elements with hyphenated tag feature-123");
}

#[test]
fn test_store_tag_filter_excludes_unknowns_from_mixed_file() {
    let (_dir, store) = fixture_store();
    let date = make_date(2026, 3, 6);
    let els = store.parse_date(date).unwrap();
    let visible: Vec<_> = els.values().filter(|e| !e.is_unknown() && !e.is_mps_group()).collect();
    assert_eq!(visible.len(), 3, "only task, note, log visible from mixed-unknown file");
}

#[test]
fn test_store_stats_empty_body_elements_counted() {
    let (_dir, store) = fixture_store();
    let date = make_date(2026, 3, 1);
    let els = store.parse_date(date).unwrap();
    let task_count = els.values().filter(|e| e.kind() == ElementKind::Task).count();
    let log_count  = els.values().filter(|e| e.kind() == ElementKind::Log).count();
    assert_eq!(task_count, 1, "empty-body task counted");
    assert_eq!(log_count, 1, "empty-body log counted");
}

#[test]
fn test_files_since_includes_new_edge_case_dates() {
    let (_dir, store) = fixture_store();
    let since = make_date(2026, 3, 1);
    let files = store.files_since(since).unwrap();
    // Should include all edge case fixtures from March 2026
    assert!(files.len() >= 10, "files_since 2026-03-01 should include all March edge cases");
}

// ═══════════════════════════════════════════════════════════════════════════
// ITERATION 13: rewrite_element robustness
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn test_rewrite_empty_body_task_to_done() {
    let (dir, store) = fixture_store();
    let date = make_date(2026, 3, 1);
    let mut attrs = HashMap::new();
    attrs.insert("status".to_string(), "done".to_string());
    let ok = store.rewrite_element("task-1", &attrs, date).unwrap();
    assert!(ok, "should rewrite task with empty body");
    let file = store.find_file(date).unwrap();
    let content = std::fs::read_to_string(&file).unwrap();
    assert!(content.contains("status: done"), "status written into args");
    drop(dir);
}

#[test]
fn test_rewrite_log_add_start_time() {
    // @log[work]{ Log one - no times } → add start time
    let (dir, store) = fixture_store();
    let date = make_date(2026, 3, 3);
    let mut attrs = HashMap::new();
    attrs.insert("start".to_string(), "08:00".to_string());
    // log-1 is the first log (no times)
    let ok = store.rewrite_element("log-1", &attrs, date).unwrap();
    assert!(ok, "should add start time to a no-time log");
    let file = store.find_file(date).unwrap();
    let content = std::fs::read_to_string(&file).unwrap();
    assert!(content.contains("start: 08:00"), "start time added");
    drop(dir);
}

#[test]
fn test_rewrite_log_update_end_time() {
    // @log[work, start: 09:00, end: 10:00]{ Log two } → update end to 11:00
    let (dir, store) = fixture_store();
    let date = make_date(2026, 3, 3);
    let mut attrs = HashMap::new();
    attrs.insert("end".to_string(), "11:00".to_string());
    // log-2 is the timed log
    let ok = store.rewrite_element("log-2", &attrs, date).unwrap();
    assert!(ok, "should update end time");
    let file = store.find_file(date).unwrap();
    let content = std::fs::read_to_string(&file).unwrap();
    assert!(content.contains("end: 11:00"), "end time updated to 11:00");
    drop(dir);
}

#[test]
fn test_rewrite_preserves_tags_from_original() {
    // @task[work]{ Task one } → mark done, tag 'work' should still be there
    let (dir, store) = fixture_store();
    let date = make_date(2026, 3, 3);
    let mut attrs = HashMap::new();
    attrs.insert("status".to_string(), "done".to_string());
    store.rewrite_element("task-1", &attrs, date).unwrap();
    let file = store.find_file(date).unwrap();
    let content = std::fs::read_to_string(&file).unwrap();
    // The rewritten line should have both status: done and tag work
    assert!(content.contains("work"), "tag 'work' preserved after rewrite");
    drop(dir);
}

#[test]
fn test_rewrite_nonexistent_human_ref_returns_false() {
    let (_dir, store) = fixture_store();
    let date = make_date(2026, 3, 1);
    let attrs = HashMap::new();
    let ok = store.rewrite_element("task-999", &attrs, date).unwrap();
    assert!(!ok, "nonexistent human ref returns false");
}

#[test]
fn test_rewrite_nonexistent_epoch_ref_returns_false() {
    let (_dir, store) = fixture_store();
    let date = make_date(2026, 3, 1);
    let attrs = HashMap::new();
    let ok = store.rewrite_element("20260301.9999", &attrs, date).unwrap();
    assert!(!ok, "nonexistent epoch ref returns false");
}

// ═══════════════════════════════════════════════════════════════════════════
// ITERATION 14: split_args edge cases
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn test_split_args_time_with_colon_value() {
    use mps::elements::split_args;
    // "at: 10:30" — first colon splits key="at", val="10:30"
    let p = split_args("at: 10:30");
    assert_eq!(p.attrs.get("at").map(|s| s.as_str()), Some("10:30"));
    assert!(p.tags.is_empty());
}

#[test]
fn test_split_args_time_with_seconds() {
    use mps::elements::split_args;
    let p = split_args("start: 09:00:00");
    assert_eq!(p.attrs.get("start").map(|s| s.as_str()), Some("09:00:00"));
}

#[test]
fn test_split_args_multiple_time_attrs() {
    use mps::elements::split_args;
    let p = split_args("work, start: 09:00, end: 17:30, backend");
    assert_eq!(p.tags, vec!["work", "backend"]);
    assert_eq!(p.attrs.get("start").map(|s| s.as_str()), Some("09:00"));
    assert_eq!(p.attrs.get("end").map(|s| s.as_str()), Some("17:30"));
}

#[test]
fn test_split_args_empty_value_after_colon() {
    use mps::elements::split_args;
    // "key:" — empty value
    let p = split_args("key:");
    assert_eq!(p.attrs.get("key").map(|s| s.as_str()), Some(""));
    assert!(p.tags.is_empty());
}

#[test]
fn test_split_args_single_tag_no_comma() {
    use mps::elements::split_args;
    let p = split_args("work");
    assert_eq!(p.tags, vec!["work"]);
    assert!(p.attrs.is_empty());
}

#[test]
fn test_split_args_hyphenated_tag() {
    use mps::elements::split_args;
    let p = split_args("mps-dev, work_item");
    assert_eq!(p.tags, vec!["mps-dev", "work_item"]);
}

// ═══════════════════════════════════════════════════════════════════════════
// ITERATION 15: Duplicate raw_args — rewriting the SECOND of two identical-args tasks
// This is the critical regression: naive first-match regex updates the wrong element.
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn test_rewrite_second_duplicate_raw_args_updates_correct_element() {
    // @task[work, status: open]{ First open work task }   ← task-1 / epoch .1
    // @task[work, status: open]{ Second open work task }  ← task-2 / epoch .2
    // Updating task-2 must mark "Second" task done, NOT "First".
    let (dir, store) = fixture_store();
    let date = make_date(2026, 3, 8);
    let mut attrs = HashMap::new();
    attrs.insert("status".to_string(), "done".to_string());

    let ok = store.rewrite_element("task-2", &attrs, date).unwrap();
    assert!(ok, "rewrite task-2 should succeed");

    let file = store.find_file(date).unwrap();
    let content = std::fs::read_to_string(&file).unwrap();

    // The FIRST task should remain open
    assert!(
        content.contains("First open work task"),
        "first task body should still be present"
    );
    // The SECOND task should now be done
    assert!(
        content.contains("Second open work task"),
        "second task body should still be present"
    );

    // Parse the rewritten file and verify only task-2 changed status
    let els = parser::parse_file(&file).unwrap();
    let r = RefResolver::new(&els);
    let task1_epoch = r.to_epoch("task-1").unwrap();
    let task2_epoch = r.to_epoch("task-2").unwrap();

    let task1 = els.get(task1_epoch).unwrap();
    let task2 = els.get(task2_epoch).unwrap();

    // After rewrite: task-1 must still be OPEN, task-2 must be DONE
    if let Element::Task { data: d1, .. } = task1 {
        assert!(d1.is_open(), "task-1 should remain open after rewriting task-2");
    }
    if let Element::Task { data: d2, .. } = task2 {
        assert!(d2.is_done(), "task-2 should be done after rewrite");
    }
    drop(dir);
}

#[test]
fn test_rewrite_fourth_of_four_same_args_tasks() {
    // backend tasks: task-3 and task-4 both have "backend, status: done"
    // Updating task-4 by epoch ref should only change the 4th task
    let (dir, store) = fixture_store();
    let date = make_date(2026, 3, 8);
    let mut attrs = HashMap::new();
    attrs.insert("status".to_string(), "open".to_string()); // flip back to open

    // task-4 is epoch 20260308.4
    let ok = store.rewrite_element("20260308.4", &attrs, date).unwrap();
    assert!(ok);

    let file = store.find_file(date).unwrap();
    let els = parser::parse_file(&file).unwrap();

    // task-3 at .3 should still be done
    let task3 = els.get("20260308.3").unwrap();
    if let Element::Task { data, .. } = task3 {
        assert!(data.is_done(), "task-3 (20260308.3) must remain done");
    }
    // task-4 at .4 should now be open
    let task4 = els.get("20260308.4").unwrap();
    if let Element::Task { data, .. } = task4 {
        assert!(data.is_open(), "task-4 (20260308.4) should now be open");
    }
    drop(dir);
}

// ═══════════════════════════════════════════════════════════════════════════
// ITERATION 16: Multiple files for the same date
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn test_multiple_files_same_date_find_files_returns_both() {
    let (_dir, store) = fixture_store();
    let date = make_date(2026, 3, 15);
    let files = store.find_files(date);
    assert_eq!(files.len(), 2, "both files for 2026-03-15 should be found");
}

#[test]
fn test_multiple_files_same_date_find_file_returns_first() {
    let (_dir, store) = fixture_store();
    let date = make_date(2026, 3, 15);
    let file = store.find_file(date).expect("should find a file");
    // First sorted filename: 20260315.1000000200.mps < 20260315.1000000201.mps
    assert!(file.to_str().unwrap().contains("1000000200"),
        "find_file should return the first (earliest epoch) file");
}

#[test]
fn test_multiple_files_same_date_parse_date_uses_first() {
    let (_dir, store) = fixture_store();
    let date = make_date(2026, 3, 15);
    let els = store.parse_date(date).unwrap();
    // First file has morning task and note only
    let task = els.values().find(|e| e.kind() == ElementKind::Task).unwrap();
    assert!(task.body_str().contains("morning work"),
        "parse_date should parse the FIRST file for the date");
}

#[test]
fn test_multiple_files_same_date_all_file_dates_deduplicates() {
    let (_dir, store) = fixture_store();
    let dates = store.all_file_dates().unwrap();
    let march_15_count = dates.iter().filter(|&&d| d == make_date(2026, 3, 15)).count();
    assert_eq!(march_15_count, 1, "2026-03-15 should appear only once in all_file_dates");
}

#[test]
fn test_multiple_files_same_date_all_files_includes_both() {
    let (_dir, store) = fixture_store();
    let all = store.all_files().unwrap();
    let march_15_files: Vec<_> = all.iter()
        .filter(|p| p.file_name().and_then(|n| n.to_str())
            .map(|n| n.starts_with("20260315"))
            .unwrap_or(false))
        .collect();
    assert_eq!(march_15_files.len(), 2, "all_files includes both files for 2026-03-15");
}

// ═══════════════════════════════════════════════════════════════════════════
// ITERATION 17: files_since boundary conditions
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn test_files_since_includes_exact_boundary_date() {
    let (_dir, store) = fixture_store();
    // 20260315 files exist; since 2026-03-15 should include them (inclusive boundary)
    let since = make_date(2026, 3, 15);
    let files = store.files_since(since).unwrap();
    let has_march15 = files.iter().any(|p| {
        p.file_name().and_then(|n| n.to_str())
            .map(|n| n.starts_with("20260315"))
            .unwrap_or(false)
    });
    assert!(has_march15, "files_since should include files on the boundary date");
}

#[test]
fn test_files_since_excludes_before_boundary() {
    let (_dir, store) = fixture_store();
    // since 2026-03-15 should exclude 20260314 and earlier
    let since = make_date(2026, 3, 15);
    let files = store.files_since(since).unwrap();
    let has_before = files.iter().any(|p| {
        p.file_name().and_then(|n| n.to_str())
            .map(|n| n < "20260315")
            .unwrap_or(false)
    });
    assert!(!has_before, "files_since should exclude files before boundary date");
}

#[test]
fn test_files_since_epoch_less_file_included() {
    let (_dir, store) = fixture_store();
    // 20260310.mps exists; since 2026-03-10 should include it
    let since = make_date(2026, 3, 10);
    let files = store.files_since(since).unwrap();
    let has_epoch_less = files.iter().any(|p| {
        p.file_name().and_then(|n| n.to_str()) == Some("20260310.mps")
    });
    assert!(has_epoch_less, "epoch-less file should be included in files_since");
}

// ═══════════════════════════════════════════════════════════════════════════
// ITERATION 18: Mixed-status file with all element types
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn test_mixed_status_file_all_types_present() {
    let els = parse_fixture("20260316.1000000202.mps");
    let visible = non_root(&els);
    assert!(visible.iter().any(|e| e.kind() == ElementKind::Task));
    assert!(visible.iter().any(|e| e.kind() == ElementKind::Note));
    assert!(visible.iter().any(|e| e.kind() == ElementKind::Log));
    assert!(visible.iter().any(|e| e.kind() == ElementKind::Reminder));
}

#[test]
fn test_mixed_status_bare_task_is_open() {
    let els = parse_fixture("20260316.1000000202.mps");
    // @task{ Bare task } — no status → defaults to open
    let tasks: Vec<_> = els.values()
        .filter_map(|e| if let Element::Task { data, .. } = e { Some(data) } else { None })
        .collect();
    let open_count = tasks.iter().filter(|d| d.is_open()).count();
    assert_eq!(open_count, 1, "bare task defaults to open");
}

#[test]
fn test_mixed_status_explicit_done_task() {
    let els = parse_fixture("20260316.1000000202.mps");
    let tasks: Vec<_> = els.values()
        .filter_map(|e| if let Element::Task { data, .. } = e { Some(data) } else { None })
        .collect();
    let done_count = tasks.iter().filter(|d| d.is_done()).count();
    assert_eq!(done_count, 1, "explicit status: done task");
}

#[test]
fn test_mixed_status_log_30_minutes() {
    let els = parse_fixture("20260316.1000000202.mps");
    let log = els.values().find(|e| e.kind() == ElementKind::Log).unwrap();
    if let Element::Log { data, .. } = log {
        assert_eq!(data.duration_minutes(), Some(30));
        assert_eq!(data.duration_str(), Some("0h30m".to_string()));
    }
}

#[test]
fn test_mixed_status_reminder_at_8am() {
    let els = parse_fixture("20260316.1000000202.mps");
    let rem = els.values().find(|e| e.kind() == ElementKind::Reminder).unwrap();
    if let Element::Reminder { data, .. } = rem {
        assert_eq!(data.at.as_deref(), Some("8am"));
    }
}

#[test]
fn test_mixed_status_ref_resolver_task_with_only_attr() {
    let els = parse_fixture("20260316.1000000202.mps");
    let r = RefResolver::new(&els);
    // @task[status: done] is the first element → task-1
    assert_eq!(r.to_epoch("task-1"), Some("20260316.1"));
    // @task (bare) is second → task-2
    assert_eq!(r.to_epoch("task-2"), Some("20260316.2"));
}

// ═══════════════════════════════════════════════════════════════════════════
// ITERATION 19: Store::append produces parseable output
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn test_append_task_then_parse_and_update() {
    let (dir, store) = fixture_store();
    let date = make_date(2026, 3, 20);
    // Append a task
    store.append("task", "New integration task", &["work".to_string()], &[], date).unwrap();
    // Parse it back
    let els = store.parse_date(date).unwrap();
    let r = RefResolver::new(&els);
    assert_eq!(r.to_epoch("task-1"), Some("20260320.1"), "appended task gets ref task-1");
    // Now update it to done
    let mut attrs = HashMap::new();
    attrs.insert("status".to_string(), "done".to_string());
    let ok = store.rewrite_element("task-1", &attrs, date).unwrap();
    assert!(ok, "should rewrite a just-appended task");
    // Parse again to verify
    let els2 = store.parse_date(date).unwrap();
    let task = els2.values().find(|e| e.kind() == ElementKind::Task).unwrap();
    if let Element::Task { data, .. } = task {
        assert!(data.is_done(), "task should be done after rewrite");
    }
    drop(dir);
}

#[test]
fn test_append_multiple_elements_ref_counters() {
    let (dir, store) = fixture_store();
    let date = make_date(2026, 3, 21);
    store.append("task", "Task A", &["work".to_string()], &[], date).unwrap();
    store.append("note", "Note B", &[], &[], date).unwrap();
    store.append("task", "Task C", &["work".to_string()], &[], date).unwrap();
    let els = store.parse_date(date).unwrap();
    let r = RefResolver::new(&els);
    assert_eq!(r.to_epoch("task-1"), Some("20260321.1"));
    assert_eq!(r.to_epoch("note-1"), Some("20260321.2"));
    assert_eq!(r.to_epoch("task-2"), Some("20260321.3"));
    drop(dir);
}

#[test]
fn test_append_log_with_times_duration_calculated() {
    let (dir, store) = fixture_store();
    let date = make_date(2026, 3, 22);
    store.append("log", "Two hour work session", &["work".to_string()],
                 &[("start", "09:00"), ("end", "11:00")], date).unwrap();
    let els = store.parse_date(date).unwrap();
    let log = els.values().find(|e| e.kind() == ElementKind::Log).unwrap();
    if let Element::Log { data, .. } = log {
        assert_eq!(data.duration_minutes(), Some(120));
        assert_eq!(data.duration_str(), Some("2h".to_string()));
    }
    drop(dir);
}

#[test]
fn test_append_reminder_with_at_time() {
    let (dir, store) = fixture_store();
    let date = make_date(2026, 3, 23);
    store.append("reminder", "Daily standup", &[], &[("at", "9:30")], date).unwrap();
    let els = store.parse_date(date).unwrap();
    let rem = els.values().find(|e| e.kind() == ElementKind::Reminder).unwrap();
    if let Element::Reminder { data, .. } = rem {
        assert_eq!(data.at.as_deref(), Some("9:30"));
    }
    drop(dir);
}

// ═══════════════════════════════════════════════════════════════════════════
// ITERATION 20: Rewrite preserves body exactly and file idempotency
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn test_rewrite_preserves_body_content_exactly() {
    let (dir, store) = fixture_store();
    let date = make_date(2026, 3, 7); // unicode file
    let file = store.find_file(date).unwrap();
    let before_content = std::fs::read_to_string(&file).unwrap();
    // Rewrite the task's status (it has @task[work]{ body with email etc })
    let mut attrs = HashMap::new();
    attrs.insert("status".to_string(), "done".to_string());
    let ok = store.rewrite_element("task-1", &attrs, date).unwrap();
    assert!(ok);
    let after_content = std::fs::read_to_string(&file).unwrap();
    // Body of the task should still contain the @ sign
    assert!(after_content.contains("contact@example.com"), "@ in body preserved after rewrite");
    assert!(after_content.contains("*bold*"), "markdown preserved after rewrite");
    // Only the opening line changed
    assert!(after_content.contains("status: done"), "status updated");
    // Everything else unchanged
    assert_eq!(
        before_content.replace("@task[work]{", "@task[status: done, work]{"),
        after_content,
        "only the opening line should change"
    );
    drop(dir);
}

#[test]
fn test_double_rewrite_idempotent() {
    // Rewriting the same element twice with the same value should be a no-op the second time.
    let (dir, store) = fixture_store();
    let date = make_date(2026, 3, 3); // many same-type file
    let mut attrs = HashMap::new();
    attrs.insert("status".to_string(), "done".to_string());
    // First rewrite: task-3 from open → done
    let ok1 = store.rewrite_element("task-3", &attrs, date).unwrap();
    assert!(ok1, "first rewrite should succeed");
    let file = store.find_file(date).unwrap();
    let content1 = std::fs::read_to_string(&file).unwrap();
    // Second rewrite: task-3 to done again (same state)
    let ok2 = store.rewrite_element("task-3", &attrs, date).unwrap();
    assert!(!ok2, "second rewrite with same value should be a no-op (returns false)");
    let content2 = std::fs::read_to_string(&file).unwrap();
    assert_eq!(content1, content2, "file should be unchanged after no-op rewrite");
    drop(dir);
}

#[test]
fn test_rewrite_note_adds_arbitrary_attr() {
    // Notes don't have typed_attrs. Rewiring with a custom attr should write it.
    // This is an edge case — notes don't normally have attrs.
    let (dir, store) = fixture_store();
    let date = make_date(2026, 3, 1); // has a bare @note
    let mut attrs = HashMap::new();
    attrs.insert("priority".to_string(), "high".to_string());
    let ok = store.rewrite_element("note-1", &attrs, date).unwrap();
    assert!(ok, "should rewrite note with custom attr");
    let file = store.find_file(date).unwrap();
    let content = std::fs::read_to_string(&file).unwrap();
    assert!(content.contains("priority: high"), "custom attr written to note");
    drop(dir);
}

// ═══════════════════════════════════════════════════════════════════════════
// ITERATION 21: Search correctness edge cases
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn test_search_case_insensitive() {
    let (_dir, store) = fixture_store();
    let lower = store.search("unicode", None, None, None).unwrap();
    let upper = store.search("UNICODE", None, None, None).unwrap();
    let mixed = store.search("UniCode", None, None, None).unwrap();
    assert!(!lower.is_empty(), "lowercase search should find match");
    assert_eq!(lower.len(), upper.len(), "case-insensitive: lower == UPPER");
    assert_eq!(lower.len(), mixed.len(), "case-insensitive: lower == MiXeD");
}

#[test]
fn test_search_empty_query_returns_all_known_elements() {
    // Isolated store with only the mixed-unknown fixture
    let dir = tempfile::tempdir().unwrap();
    std::fs::copy(
        fixture_path("20260306.1000000105.mps"),
        dir.path().join("20260306.1000000105.mps"),
    ).unwrap();
    let store = Store::new(dir.path());
    let results = store.search("", None, None, None).unwrap();
    // Should return task, note, log — NOT widget, gadget, sprocket, thingamajig
    assert_eq!(results.len(), 3, "search with empty query finds all 3 known elements; got {}", results.len());
    assert!(results.iter().all(|r| !r.element.is_unknown()));
}

#[test]
fn test_search_type_filter_note_only() {
    // Isolated store with only the many-same-type fixture
    let dir = tempfile::tempdir().unwrap();
    std::fs::copy(
        fixture_path("20260303.1000000102.mps"),
        dir.path().join("20260303.1000000102.mps"),
    ).unwrap();
    let store = Store::new(dir.path());
    let results = store.search("", Some("note"), None, None).unwrap();
    assert_eq!(results.len(), 3, "type=note should return all 3 notes; got {}", results.len());
    assert!(results.iter().all(|r| r.element.kind() == ElementKind::Note));
}

#[test]
fn test_search_type_filter_log_with_since() {
    let (_dir, store) = fixture_store();
    let since = make_date(2026, 3, 4);
    let results = store.search("", Some("log"), None, Some(since)).unwrap();
    assert!(!results.is_empty(), "should find logs since 2026-03-04");
    assert!(results.iter().all(|r| r.element.kind() == ElementKind::Log));
}

#[test]
fn test_search_no_results_returns_empty_vec() {
    let (_dir, store) = fixture_store();
    let results = store.search("xyzzy_impossible_string_12345", None, None, None).unwrap();
    assert!(results.is_empty(), "no match → empty vec");
}

#[test]
fn test_search_result_date_str_correct_format() {
    let (_dir, store) = fixture_store();
    let results = store.search("epoch-less", None, None, None).unwrap();
    assert!(!results.is_empty(), "should find 'epoch-less' in 20260310.mps");
    let date_str = &results[0].date_str;
    assert_eq!(date_str, "20260310", "date_str should be YYYYMMDD from filename");
}

// ═══════════════════════════════════════════════════════════════════════════
// ITERATION 22: RefResolver roundtrips across all new fixtures
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn test_ref_resolver_roundtrip_all_new_fixtures() {
    let new_fixtures = [
        "20260301.1000000100.mps",
        "20260302.1000000101.mps",
        "20260303.1000000102.mps",
        "20260304.1000000103.mps",
        "20260305.1000000104.mps",
        "20260306.1000000105.mps",
        "20260307.1000000106.mps",
        "20260308.1000000107.mps",
        "20260309.1000000108.mps",
        "20260311.1000000110.mps",
        "20260312.1000000111.mps",
        "20260313.1000000112.mps",
        "20260316.1000000202.mps",
    ];
    for fixture in &new_fixtures {
        let els = parse_fixture(fixture);
        let r = RefResolver::new(&els);
        // Iterate the elements map keys — same approach as integration_test.rs
        for epoch_ref in els.keys() {
            if let Some(human) = r.to_human(epoch_ref) {
                let back = r.to_epoch(human).unwrap_or("none");
                assert_eq!(back, epoch_ref.as_str(),
                    "{}: roundtrip failed: epoch {} → human {} → {}", fixture, epoch_ref, human, back);
            }
        }
    }
}

#[test]
fn test_ref_resolver_epoch_less_file_roundtrip() {
    let els = parse_fixture("20260310.mps");
    let r = RefResolver::new(&els);
    // base_ref = 20260310 (parsed from "20260310.mps"[..8])
    assert_eq!(r.to_human("20260310.1"), Some("task-1"));
    assert_eq!(r.to_human("20260310.2"), Some("note-1"));
    assert_eq!(r.to_human("20260310.3"), Some("log-1"));
    assert_eq!(r.to_epoch("task-1"), Some("20260310.1"));
}

// ═══════════════════════════════════════════════════════════════════════════
// ITERATION 23: export command excludes unknown elements
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn test_export_excludes_unknown_elements() {
    // The mixed-unknown fixture has 4 unknowns. Export should not include them.
    let dir = tempfile::tempdir().unwrap();
    std::fs::copy(
        fixture_path("20260306.1000000105.mps"),
        dir.path().join("20260306.1000000105.mps"),
    ).unwrap();
    let store = Store::new(dir.path());
    let date = make_date(2026, 3, 6);
    let els = store.parse_date(date).unwrap();
    // Simulate what export does: skip mps_group and unknown
    let exported: Vec<_> = els.values()
        .filter(|e| !e.is_mps_group() && !e.is_unknown())
        .collect();
    assert_eq!(exported.len(), 3, "export should have 3 known elements, not 7");
}

#[test]
fn test_export_includes_all_element_types() {
    let dir = tempfile::tempdir().unwrap();
    std::fs::copy(
        fixture_path("20260316.1000000202.mps"),
        dir.path().join("20260316.1000000202.mps"),
    ).unwrap();
    let store = Store::new(dir.path());
    let date = make_date(2026, 3, 16);
    let els = store.parse_date(date).unwrap();
    let exported: Vec<_> = els.values()
        .filter(|e| !e.is_mps_group() && !e.is_unknown())
        .collect();
    // task×2, note×1, log×1, reminder×1 = 5
    assert_eq!(exported.len(), 5);
}

// ═══════════════════════════════════════════════════════════════════════════
// ITERATION 24: Tags command with status filter
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn test_tags_status_filter_open_tasks_only() {
    let dir = tempfile::tempdir().unwrap();
    std::fs::copy(
        fixture_path("20260303.1000000102.mps"),
        dir.path().join("20260303.1000000102.mps"),
    ).unwrap();
    let store = Store::new(dir.path());
    let date = make_date(2026, 3, 3);
    let els = store.parse_date(date).unwrap();
    // Status filter: only open tasks have tags counted
    // Open tasks: task1 (work), task2 (work), task3 (work) → "work" appears 3 times
    // Done tasks: task4 (work), task5 (work) → NOT counted
    let mut counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
    for (_, el) in &els {
        if el.is_mps_group() || el.is_unknown() { continue; }
        match el {
            Element::Task { data, .. } if data.is_open() => {
                for tag in el.tags() { *counts.entry(tag.clone()).or_insert(0) += 1; }
            }
            _ => {}
        }
    }
    assert_eq!(counts.get("work").copied().unwrap_or(0), 3,
        "open task status filter: only 3 open tasks have 'work' tag");
}

#[test]
fn test_tags_type_filter_log_only() {
    let dir = tempfile::tempdir().unwrap();
    std::fs::copy(
        fixture_path("20260304.1000000103.mps"),
        dir.path().join("20260304.1000000103.mps"),
    ).unwrap();
    let store = Store::new(dir.path());
    let date = make_date(2026, 3, 4);
    let els = store.parse_date(date).unwrap();
    // Type filter: only log tags counted
    let mut counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
    for (_, el) in &els {
        if el.kind() != ElementKind::Log { continue; }
        for tag in el.tags() { *counts.entry(tag.clone()).or_insert(0) += 1; }
    }
    // Log has tag "feature-123"
    assert!(counts.contains_key("feature-123"), "log tag feature-123 counted");
    // Tags from tasks and notes should NOT be counted
    assert!(!counts.contains_key("mps-dev"), "task tag mps-dev not counted with log filter");
}

// ═══════════════════════════════════════════════════════════════════════════
// ITERATION 25: Parser with body containing special chars that could confuse regex
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn test_parser_body_with_regex_metacharacters() {
    // Body has characters that are regex special: . * + ? [ ] ( ) ^ $ |
    // Note: literal } CANNOT appear in a body — it would close the element early.
    // This is a known parser limitation (matches Ruby behaviour).
    let dir = tempfile::tempdir().unwrap();
    let content = "@task[work]{\n  Fix regex: (a|b).* and [xyz]+ and ^start and end$ and a|b\n}";
    let path = dir.path().join("20260320.1000000300.mps");
    std::fs::write(&path, content).unwrap();
    let els = parser::parse_file(&path).unwrap();
    let task = els.values().find(|e| e.kind() == ElementKind::Task).unwrap();
    assert!(task.body_str().contains("(a|b).*"), "regex parens/dot/star preserved");
    assert!(task.body_str().contains("[xyz]+"), "regex brackets/plus preserved");
    assert!(task.body_str().contains("^start"), "regex caret preserved");
    assert!(task.body_str().contains("end$"), "regex dollar preserved");
    assert!(task.body_str().contains("a|b"), "regex pipe preserved");
}

#[test]
fn test_rewrite_element_body_with_regex_metacharacters() {
    // The rewrite should not break when element has regex metacharacters in body.
    // No literal } allowed in body (known parser limitation).
    let dir = tempfile::tempdir().unwrap();
    let content = "@task[work]{\n  Fix regex: (a|b).* and [xyz]+ and ^start$ and a|b\n}\n";
    let path = dir.path().join("20260320.1000000300.mps");
    std::fs::write(&path, content).unwrap();
    let store = Store::new(dir.path());
    let date = make_date(2026, 3, 20);
    let mut attrs = HashMap::new();
    attrs.insert("status".to_string(), "done".to_string());
    let ok = store.rewrite_element("task-1", &attrs, date).unwrap();
    assert!(ok, "rewrite should succeed with regex chars in body");
    let after = std::fs::read_to_string(&path).unwrap();
    assert!(after.contains("(a|b).*"), "regex chars in body preserved after rewrite");
    assert!(after.contains("[xyz]+"), "bracket chars preserved after rewrite");
    assert!(after.contains("status: done"), "status updated");
    drop(dir);
}

#[test]
fn test_rewrite_element_tag_with_regex_metacharacters_in_args() {
    // Args with special chars that need regex escaping (e.g., . in tag names)
    // Note: tags can't contain . in practice, but raw_args might have other special chars
    let dir = tempfile::tempdir().unwrap();
    // Use a reminder with "at: 3:30pm" — the colon in time gets regex-escaped
    let content = "@reminder[at: 3:30pm]{\n  afternoon meeting\n}\n";
    let path = dir.path().join("20260321.1000000301.mps");
    std::fs::write(&path, content).unwrap();
    let store = Store::new(dir.path());
    let date = make_date(2026, 3, 21);
    let mut attrs = HashMap::new();
    attrs.insert("at".to_string(), "4:00pm".to_string());
    let ok = store.rewrite_element("reminder-1", &attrs, date).unwrap();
    assert!(ok, "rewrite reminder with colon in time should succeed");
    let after = std::fs::read_to_string(&path).unwrap();
    assert!(after.contains("at: 4:00pm"), "time updated correctly");
    assert!(after.contains("afternoon meeting"), "body preserved");
    drop(dir);
}

// ═══════════════════════════════════════════════════════════════════════════
// ITERATION 26: all_file_dates sorted and unique across epoch-less + epoch files
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn test_all_file_dates_is_sorted() {
    let (_dir, store) = fixture_store();
    let dates = store.all_file_dates().unwrap();
    let sorted_copy = {
        let mut v = dates.clone();
        v.sort();
        v
    };
    assert_eq!(dates, sorted_copy, "all_file_dates should be sorted ascending");
}

#[test]
fn test_all_file_dates_no_duplicates() {
    let (_dir, store) = fixture_store();
    let dates = store.all_file_dates().unwrap();
    let unique: std::collections::HashSet<_> = dates.iter().collect();
    assert_eq!(dates.len(), unique.len(), "all_file_dates should have no duplicates");
}

#[test]
fn test_all_file_dates_epoch_less_file_at_correct_position() {
    let (_dir, store) = fixture_store();
    let dates = store.all_file_dates().unwrap();
    let march_10 = make_date(2026, 3, 10);
    let march_9_pos = dates.iter().position(|&d| d < march_10);
    let march_10_pos = dates.iter().position(|&d| d == march_10).unwrap();
    let march_11_pos = dates.iter().position(|&d| d > march_10);
    // march_10 must come after all dates < it and before all dates > it
    if let (Some(before), Some(after)) = (march_9_pos, march_11_pos) {
        assert!(before < march_10_pos, "epoch-less date sorted correctly after earlier dates");
        assert!(march_10_pos < after, "epoch-less date sorted correctly before later dates");
    }
}

// ═══════════════════════════════════════════════════════════════════════════
// ITERATION 27: element count assertions across the full fixture set
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn test_store_total_element_counts_across_all_fixtures() {
    let (_dir, store) = fixture_store();
    let all_files = store.all_files().unwrap();
    let mut total_tasks = 0usize;
    let mut total_notes = 0usize;
    let mut total_logs  = 0usize;
    let mut total_unknowns = 0usize;
    for file in &all_files {
        let els = parser::parse_file(file).unwrap();
        for (_, el) in &els {
            match el.kind() {
                ElementKind::Task    => total_tasks += 1,
                ElementKind::Note    => total_notes += 1,
                ElementKind::Log     => total_logs += 1,
                ElementKind::Unknown => total_unknowns += 1,
                _ => {}
            }
        }
    }
    // Verify unknowns exist (from the mixed-unknown fixture)
    assert!(total_unknowns >= 4, "should have at least 4 unknown elements from the mixed fixture");
    // Verify reasonable element counts
    assert!(total_tasks >= 20, "should have many tasks across all fixtures; got {}", total_tasks);
    assert!(total_notes >= 10, "should have many notes across all fixtures; got {}", total_notes);
    assert!(total_logs >= 10, "should have many logs across all fixtures; got {}", total_logs);
}

// ═══════════════════════════════════════════════════════════════════════════
// ITERATION 28: Task-in-task nesting — non-@mps parent containers
// The nested_test.mps pattern: @task contains child @tasks and @notes.
// ═══════════════════════════════════════════════════════════════════════════

fn parse_task_in_task() -> IndexMap<String, Element> {
    parse_fixture("20260501.1800000001.mps")
}

#[test]
fn test_task_in_task_total_element_count() {
    // root mps wrapper + outer task + 3 child tasks + 3 child notes = 8
    let els = parse_task_in_task();
    assert_eq!(els.len(), 8, "expected 8 elements, got {}", els.len());
}

#[test]
fn test_task_in_task_all_ref_paths_present() {
    let els = parse_task_in_task();
    assert!(els.contains_key("20260501"),       "root mps wrapper");
    assert!(els.contains_key("20260501.1"),     "outer task");
    assert!(els.contains_key("20260501.1.1"),   "child task: bugs");
    assert!(els.contains_key("20260501.1.2"),   "child task: meetings");
    assert!(els.contains_key("20260501.1.3"),   "child task: dev works");
    assert!(els.contains_key("20260501.1.3.1"), "nested note: items not done");
    assert!(els.contains_key("20260501.1.3.2"), "nested note: peoples support");
    assert!(els.contains_key("20260501.1.3.3"), "nested note: say no");
}

#[test]
fn test_task_in_task_outer_task_type_and_status() {
    let els = parse_task_in_task();
    let outer = els.get("20260501.1").unwrap();
    assert_eq!(outer.kind(), ElementKind::Task, "outer element is a Task");
    if let Element::Task { data, .. } = outer {
        assert!(data.is_open(), "outer task has status: open");
        assert!(data.tags.is_empty(), "outer task has no tags (status: open is an attr)");
    } else { panic!("expected Task"); }
}

#[test]
fn test_task_in_task_inner_tasks_default_open() {
    let els = parse_task_in_task();
    for key in &["20260501.1.1", "20260501.1.2", "20260501.1.3"] {
        let el = els.get(*key).expect(key);
        assert_eq!(el.kind(), ElementKind::Task, "{} should be Task", key);
        if let Element::Task { data, .. } = el {
            assert!(data.is_open(), "{} should default to open (no args)", key);
        }
    }
}

#[test]
fn test_task_in_task_nested_notes_are_notes() {
    let els = parse_task_in_task();
    for key in &["20260501.1.3.1", "20260501.1.3.2", "20260501.1.3.3"] {
        let el = els.get(*key).expect(key);
        assert_eq!(el.kind(), ElementKind::Note, "{} should be Note", key);
    }
}

#[test]
fn test_task_in_task_leaf_bodies_are_clean() {
    let els = parse_task_in_task();
    assert!(els.get("20260501.1.1").unwrap().body_str().trim().contains("Several bugs to be fixed"));
    assert!(els.get("20260501.1.2").unwrap().body_str().trim().contains("Several meetings to be attended"));
    assert!(els.get("20260501.1.3.1").unwrap().body_str().trim().contains("Check for items not done"));
    assert!(els.get("20260501.1.3.2").unwrap().body_str().trim().contains("Check for peoples support"));
    assert!(els.get("20260501.1.3.3").unwrap().body_str().trim().contains("Say No to not aligned with my goal"));
}

#[test]
fn test_task_in_task_outer_body_contains_child_syntax() {
    // The outer task's body_str is the raw text between its braces,
    // which includes the literal "@task{...}" source of its children.
    let els = parse_task_in_task();
    let body = els.get("20260501.1").unwrap().body_str();
    assert!(body.contains("These items to be done by monday"), "own text present");
    assert!(body.contains("@task{"), "child @task syntax embedded in body");
}

#[test]
fn test_task_in_task_dev_task_body_contains_note_syntax() {
    let els = parse_task_in_task();
    let body = els.get("20260501.1.3").unwrap().body_str();
    assert!(body.contains("Several dev works to be done"), "own text present");
    assert!(body.contains("@note{"), "child @note syntax embedded in body");
}

#[test]
fn test_task_in_task_resolver_top_level_human_ref() {
    let els = parse_task_in_task();
    let r = RefResolver::new(&els);
    assert_eq!(r.to_human("20260501.1"), Some("task-1"), "outer task → task-1");
    assert_eq!(r.to_epoch("task-1"),     Some("20260501.1"), "roundtrip");
}

#[test]
fn test_task_in_task_resolver_child_tasks() {
    let els = parse_task_in_task();
    let r = RefResolver::new(&els);
    assert_eq!(r.to_human("20260501.1.1"), Some("task-1.1"), "bugs task");
    assert_eq!(r.to_human("20260501.1.2"), Some("task-1.2"), "meetings task");
    assert_eq!(r.to_human("20260501.1.3"), Some("task-1.3"), "dev task");
    assert_eq!(r.to_epoch("task-1.1"), Some("20260501.1.1"));
    assert_eq!(r.to_epoch("task-1.2"), Some("20260501.1.2"));
    assert_eq!(r.to_epoch("task-1.3"), Some("20260501.1.3"));
}

#[test]
fn test_task_in_task_resolver_deep_notes() {
    let els = parse_task_in_task();
    let r = RefResolver::new(&els);
    assert_eq!(r.to_human("20260501.1.3.1"), Some("task-1.3.1"));
    assert_eq!(r.to_human("20260501.1.3.2"), Some("task-1.3.2"));
    assert_eq!(r.to_human("20260501.1.3.3"), Some("task-1.3.3"));
    assert_eq!(r.to_epoch("task-1.3.1"), Some("20260501.1.3.1"));
    assert_eq!(r.to_epoch("task-1.3.2"), Some("20260501.1.3.2"));
    assert_eq!(r.to_epoch("task-1.3.3"), Some("20260501.1.3.3"));
}

#[test]
fn test_task_in_task_resolver_full_roundtrip() {
    let els = parse_task_in_task();
    let r = RefResolver::new(&els);
    let pairs = [
        ("20260501.1",     "task-1"),
        ("20260501.1.1",   "task-1.1"),
        ("20260501.1.2",   "task-1.2"),
        ("20260501.1.3",   "task-1.3"),
        ("20260501.1.3.1", "task-1.3.1"),
        ("20260501.1.3.2", "task-1.3.2"),
        ("20260501.1.3.3", "task-1.3.3"),
    ];
    for (epoch, human) in &pairs {
        assert_eq!(r.to_human(epoch), Some(*human),  "epoch→human: {}", epoch);
        assert_eq!(r.to_epoch(human), Some(*epoch), "human→epoch: {}", human);
    }
}

#[test]
fn test_task_in_task_type_counter_only_counts_toplevel() {
    // The RefResolver "task" type counter is 1 — only the outer task at depth 1.
    // The 3 inner tasks are nested and get dotted refs (task-1.1 etc.), not new task-N slots.
    let els = parse_task_in_task();
    let r = RefResolver::new(&els);
    assert_eq!(r.to_human("20260501.1"), Some("task-1"), "outer task → task-1");
    assert!(r.to_epoch("task-2").is_none(), "no task-2 — inner tasks are not top-level");
    assert!(r.to_epoch("task-3").is_none(), "no task-3");
    assert!(r.to_epoch("task-4").is_none(), "no task-4");
    // Confirm inner tasks are accessible only via dotted refs
    assert!(r.to_epoch("task-1.1").is_some(), "task-1.1 is the bugs child task");
}

#[test]
fn test_task_in_task_rewrite_child_by_human_ref() {
    let (dir, store) = fixture_store();
    let date = make_date(2026, 5, 1);
    let mut attrs = HashMap::new();
    attrs.insert("status".to_string(), "done".to_string());
    let ok = store.rewrite_element("task-1.2", &attrs, date).unwrap();
    assert!(ok, "rewrite task-1.2 (meetings) to done");
    let content = std::fs::read_to_string(store.find_file(date).unwrap()).unwrap();
    assert!(content.contains("status: done"),                       "status written");
    assert!(content.contains("Several meetings to be attended"),    "body preserved");
    assert!(content.contains("Several bugs to be fixed"),           "sibling task unchanged");
    drop(dir);
}

#[test]
fn test_task_in_task_rewrite_deep_child_by_human_ref() {
    let (dir, store) = fixture_store();
    let date = make_date(2026, 5, 1);
    let mut attrs = HashMap::new();
    attrs.insert("status".to_string(), "done".to_string());
    let ok = store.rewrite_element("task-1.3", &attrs, date).unwrap();
    assert!(ok, "rewrite task-1.3 (dev task, level 2) to done");
    let content = std::fs::read_to_string(store.find_file(date).unwrap()).unwrap();
    assert!(content.contains("Several dev works to be done"), "dev task body preserved");
    drop(dir);
}

#[test]
fn test_task_in_task_rewrite_outer_task_by_human_ref() {
    let (dir, store) = fixture_store();
    let date = make_date(2026, 5, 1);
    let mut attrs = HashMap::new();
    attrs.insert("status".to_string(), "done".to_string());
    let ok = store.rewrite_element("task-1", &attrs, date).unwrap();
    assert!(ok, "rewrite outer task-1 to done");
    let content = std::fs::read_to_string(store.find_file(date).unwrap()).unwrap();
    assert!(content.contains("status: done"),           "outer task now done");
    assert!(content.contains("Several bugs to be fixed"), "inner task body still present");
    drop(dir);
}

#[test]
fn test_task_in_task_task_count() {
    let els = parse_task_in_task();
    let tasks = els.values().filter(|e| e.kind() == ElementKind::Task).count();
    assert_eq!(tasks, 4, "4 tasks: 1 outer + 3 nested");
}

#[test]
fn test_task_in_task_note_count() {
    let els = parse_task_in_task();
    let notes = els.values().filter(|e| e.kind() == ElementKind::Note).count();
    assert_eq!(notes, 3, "3 notes, all nested inside the dev task");
}

#[test]
fn test_task_in_task_no_logs_or_reminders() {
    let els = parse_task_in_task();
    assert_eq!(els.values().filter(|e| e.kind() == ElementKind::Log).count(), 0);
    assert_eq!(els.values().filter(|e| e.kind() == ElementKind::Reminder).count(), 0);
}

// ═══════════════════════════════════════════════════════════════════════════
// ITERATION 29: Tagged @mps blocks and task-as-parent (note-in-task)
// Tests @mps[tags]{} capturing and non-mps elements as parent containers.
// ═══════════════════════════════════════════════════════════════════════════

fn parse_tagged_mps_fixture() -> IndexMap<String, Element> {
    parse_fixture("20260502.1800000002.mps")
}

#[test]
fn test_tagged_mps_total_element_count() {
    // root mps + sprint mps + task in sprint + note in sprint + done-task in sprint
    // + top-level done task + note in done task + bare note = 8
    let els = parse_tagged_mps_fixture();
    assert_eq!(els.len(), 8, "expected 8 elements, got {}", els.len());
}

#[test]
fn test_tagged_mps_all_ref_paths_present() {
    let els = parse_tagged_mps_fixture();
    assert!(els.contains_key("20260502"),     "root mps");
    assert!(els.contains_key("20260502.1"),   "sprint mps block");
    assert!(els.contains_key("20260502.1.1"), "task inside sprint");
    assert!(els.contains_key("20260502.1.2"), "note inside sprint");
    assert!(els.contains_key("20260502.1.3"), "done task inside sprint");
    assert!(els.contains_key("20260502.2"),   "top-level done task");
    assert!(els.contains_key("20260502.2.1"), "note inside done task");
    assert!(els.contains_key("20260502.3"),   "bare top-level note");
}

#[test]
fn test_tagged_mps_block_captures_tags() {
    let els = parse_tagged_mps_fixture();
    let mps_block = els.get("20260502.1").unwrap();
    assert!(mps_block.is_mps_group(), "sprint block is MpsGroup");
    assert!(mps_block.tags().contains(&"sprint-42".to_string()), "sprint-42 tag");
    assert!(mps_block.tags().contains(&"current".to_string()), "current tag");
}

#[test]
fn test_tagged_mps_block_raw_args() {
    let els = parse_tagged_mps_fixture();
    let mps_block = els.get("20260502.1").unwrap();
    assert_eq!(mps_block.raw_args(), "sprint-42, current", "raw_args preserved");
}

#[test]
fn test_tagged_mps_resolver_sprint_block() {
    let els = parse_tagged_mps_fixture();
    let r = RefResolver::new(&els);
    assert_eq!(r.to_human("20260502.1"), Some("mps-1"), "sprint mps → mps-1");
    assert_eq!(r.to_epoch("mps-1"),      Some("20260502.1"), "roundtrip");
}

#[test]
fn test_tagged_mps_children_refs() {
    let els = parse_tagged_mps_fixture();
    let r = RefResolver::new(&els);
    assert_eq!(r.to_human("20260502.1.1"), Some("mps-1.1"), "task inside sprint");
    assert_eq!(r.to_human("20260502.1.2"), Some("mps-1.2"), "note inside sprint");
    assert_eq!(r.to_human("20260502.1.3"), Some("mps-1.3"), "done task inside sprint");
    assert_eq!(r.to_epoch("mps-1.1"), Some("20260502.1.1"));
    assert_eq!(r.to_epoch("mps-1.2"), Some("20260502.1.2"));
    assert_eq!(r.to_epoch("mps-1.3"), Some("20260502.1.3"));
}

#[test]
fn test_tagged_mps_child_types() {
    let els = parse_tagged_mps_fixture();
    assert_eq!(els.get("20260502.1.1").unwrap().kind(), ElementKind::Task, ".1.1 is Task");
    assert_eq!(els.get("20260502.1.2").unwrap().kind(), ElementKind::Note, ".1.2 is Note");
    assert_eq!(els.get("20260502.1.3").unwrap().kind(), ElementKind::Task, ".1.3 is Task");
}

#[test]
fn test_tagged_mps_child_statuses() {
    let els = parse_tagged_mps_fixture();
    if let Element::Task { data, .. } = els.get("20260502.1.1").unwrap() {
        assert!(data.is_open(), "sprint planning task is open");
    }
    if let Element::Task { data, .. } = els.get("20260502.1.3").unwrap() {
        assert!(data.is_done(), "backend task is done");
        assert!(data.tags.contains(&"backend".to_string()), "backend tag preserved");
    }
}

#[test]
fn test_tagged_mps_note_has_review_tag() {
    let els = parse_tagged_mps_fixture();
    let note = els.get("20260502.1.2").unwrap();
    assert!(note.tags().contains(&"review".to_string()), "note has review tag");
}

#[test]
fn test_note_in_task_ref_path() {
    let els = parse_tagged_mps_fixture();
    let note = els.get("20260502.2.1").expect("note inside done task");
    assert_eq!(note.kind(), ElementKind::Note, "it is a note");
}

#[test]
fn test_note_in_task_resolver() {
    let els = parse_tagged_mps_fixture();
    let r = RefResolver::new(&els);
    // top-level done task → task-1 (first Task at depth 1)
    assert_eq!(r.to_human("20260502.2"),   Some("task-1"),   "top-level done task → task-1");
    // note nested inside it → task-1.1 (not note-1)
    assert_eq!(r.to_human("20260502.2.1"), Some("task-1.1"), "note in task → task-1.1");
    assert_eq!(r.to_epoch("task-1"),       Some("20260502.2"), "roundtrip task-1");
    assert_eq!(r.to_epoch("task-1.1"),     Some("20260502.2.1"), "roundtrip task-1.1");
}

#[test]
fn test_bare_top_level_note_gets_note_counter() {
    // @note{ Bare top-level note } is at depth 1 → note-1.
    // The notes inside the mps block (mps-1.2) and inside the task (task-1.1)
    // are at depth 2 and do NOT consume the depth-1 note counter.
    let els = parse_tagged_mps_fixture();
    let r = RefResolver::new(&els);
    assert_eq!(r.to_human("20260502.3"), Some("note-1"), "bare top-level note → note-1");
    assert_eq!(r.to_epoch("note-1"),     Some("20260502.3"), "roundtrip");
}

#[test]
fn test_nested_notes_do_not_inflate_note_counter() {
    // Only the bare note at depth 1 gets a note-N ref.
    // The note inside the mps block is mps-1.2 and the note inside the task is task-1.1.
    let els = parse_tagged_mps_fixture();
    let r = RefResolver::new(&els);
    assert!(r.to_epoch("note-2").is_none(), "no note-2 — only one top-level note");
}

#[test]
fn test_tagged_mps_done_task_top_level_ref() {
    let els = parse_tagged_mps_fixture();
    let done = els.get("20260502.2").unwrap();
    if let Element::Task { data, .. } = done {
        assert!(data.is_done(), "top-level done task");
    }
}

#[test]
fn test_tagged_mps_done_task_body_contains_note_syntax() {
    let els = parse_tagged_mps_fixture();
    let body = els.get("20260502.2").unwrap().body_str();
    assert!(body.contains("Top level done task"), "own text");
    assert!(body.contains("@note{"), "nested note syntax in body");
}

#[test]
fn test_tagged_mps_resolver_full_roundtrip() {
    let els = parse_tagged_mps_fixture();
    let r = RefResolver::new(&els);
    let pairs = [
        ("20260502.1",   "mps-1"),
        ("20260502.1.1", "mps-1.1"),
        ("20260502.1.2", "mps-1.2"),
        ("20260502.1.3", "mps-1.3"),
        ("20260502.2",   "task-1"),
        ("20260502.2.1", "task-1.1"),
        ("20260502.3",   "note-1"),
    ];
    for (epoch, human) in &pairs {
        assert_eq!(r.to_human(epoch), Some(*human),  "epoch→human: {}", epoch);
        assert_eq!(r.to_epoch(human), Some(*epoch), "human→epoch: {}", human);
    }
}

// ═══════════════════════════════════════════════════════════════════════════
// ITERATION 30: character element type
// ═══════════════════════════════════════════════════════════════════════════

const CHAR_FILE: &str = "20260601.1900000001.mps";

fn parse_char_fixture() -> IndexMap<String, Element> {
    parse_fixture(CHAR_FILE)
}

fn char_elements(els: &IndexMap<String, Element>) -> Vec<&Element> {
    els.values().filter(|e| e.kind() == ElementKind::Character).collect()
}

// ── Parser correctness ────────────────────────────────────────────────────

#[test]
fn test_character_parser_produces_character_kind() {
    let els = parse_char_fixture();
    assert!(els.values().any(|e| e.kind() == ElementKind::Character),
        "fixture must contain at least one Character element");
}

#[test]
fn test_character_parser_name_and_tags() {
    let els = parse_char_fixture();
    let el = char_elements(&els).into_iter()
        .find(|e| if let Element::Character { data, .. } = e { data.name.as_deref() == Some("Mahfuz Vai") } else { false })
        .expect("Mahfuz Vai character not found");
    if let Element::Character { data, .. } = el {
        assert_eq!(data.name.as_deref(), Some("Mahfuz Vai"));
        assert!(data.tags.contains(&"mahfuz-vai".to_string()));
        assert!(data.tags.contains(&"favor".to_string()));
    }
}

#[test]
fn test_character_parser_tags_only_no_name() {
    let els = parse_char_fixture();
    // The second character has [favor, colleague] — no name
    let nameless: Vec<_> = char_elements(&els).into_iter()
        .filter(|e| if let Element::Character { data, .. } = e { data.name.is_none() } else { false })
        .collect();
    assert!(!nameless.is_empty(), "fixture must have at least one nameless character");
    let el = nameless[0];
    if let Element::Character { data, .. } = el {
        assert!(data.tags.contains(&"colleague".to_string()) || data.tags.contains(&"favor".to_string()));
    }
}

#[test]
fn test_character_parser_bare_braces_no_args() {
    let els = parse_char_fixture();
    // The anonymous character has no args at all
    let bare = char_elements(&els).into_iter()
        .find(|e| e.raw_args().is_empty())
        .expect("bare character (no args) not found");
    if let Element::Character { data, .. } = bare {
        assert!(data.name.is_none());
        assert!(data.tags.is_empty());
    }
}

#[test]
fn test_character_parser_name_with_spaces_preserved() {
    let els = parse_char_fixture();
    let el = char_elements(&els).into_iter()
        .find(|e| if let Element::Character { data, .. } = e { data.name.as_deref() == Some("Dr. Alice Smith") } else { false })
        .expect("Dr. Alice Smith character not found");
    if let Element::Character { data, .. } = el {
        assert_eq!(data.name.as_deref(), Some("Dr. Alice Smith"));
        assert!(data.tags.contains(&"mentor".to_string()));
    }
}

#[test]
fn test_character_not_parsed_as_unknown() {
    let els = parse_char_fixture();
    for el in els.values() {
        if el.sign() == "character" {
            assert!(!el.is_unknown(), "character elements must not be Unknown");
        }
    }
}

#[test]
fn test_character_kind_is_character() {
    let els = parse_char_fixture();
    for el in els.values().filter(|e| e.sign() == "character") {
        assert_eq!(el.kind(), ElementKind::Character);
    }
}

#[test]
fn test_character_is_not_is_mps_group() {
    let els = parse_char_fixture();
    for el in char_elements(&els) {
        assert!(!el.is_mps_group());
    }
}

// ── ElementKind + Element trait ───────────────────────────────────────────

#[test]
fn test_element_kind_character_display() {
    assert_eq!(format!("{}", ElementKind::Character), "character");
}

#[test]
fn test_element_from_parts_character_not_unknown() {
    let el = Element::from_parts(
        "character",
        "name: Mahfuz Vai, mahfuz-vai".to_string(),
        vec![],
        "He helped me".to_string(),
    );
    assert!(!el.is_unknown());
    assert_eq!(el.kind(), ElementKind::Character);
    assert_eq!(el.sign(), "character");
}

#[test]
fn test_element_character_tags_accessible() {
    let el = Element::from_parts(
        "character",
        "name: Mahfuz Vai, mahfuz-vai, favor".to_string(),
        vec![],
        "body".to_string(),
    );
    assert!(el.tags().contains(&"mahfuz-vai".to_string()));
    assert!(el.tags().contains(&"favor".to_string()));
}

#[test]
fn test_element_character_typed_attrs_with_name() {
    let el = Element::from_parts(
        "character",
        "name: Mahfuz Vai".to_string(),
        vec![],
        "body".to_string(),
    );
    let attrs = el.typed_attrs();
    assert_eq!(attrs.len(), 1);
    assert_eq!(attrs[0].0, "name");
    assert_eq!(attrs[0].1, "Mahfuz Vai");
}

#[test]
fn test_element_character_typed_attrs_without_name() {
    let el = Element::from_parts(
        "character",
        "mahfuz-vai".to_string(),
        vec![],
        "body".to_string(),
    );
    assert!(el.typed_attrs().is_empty());
}

#[test]
fn test_element_character_body_str() {
    let el = Element::from_parts(
        "character",
        "name: Mahfuz Vai".to_string(),
        vec![],
        "He helped me today".to_string(),
    );
    assert_eq!(el.body_str(), "He helped me today");
}

// ── Filtering ─────────────────────────────────────────────────────────────

#[test]
fn test_type_filter_character_only() {
    use mps::commands::{visible, DisplayOpts};
    let els = parse_char_fixture();
    let opts = DisplayOpts {
        type_filter:   Some("character".to_string()),
        tag_filter:    None,
        status_filter: None,
        name_filter:   None,
    };
    for el in els.values().filter(|e| !e.is_mps_group() && !e.is_unknown()) {
        if el.kind() == ElementKind::Character {
            assert!(visible(el, &opts), "character should pass type filter");
        } else {
            assert!(!visible(el, &opts), "non-character should fail character type filter");
        }
    }
}

#[test]
fn test_type_filter_task_excludes_character() {
    use mps::commands::{visible, DisplayOpts};
    let els = parse_char_fixture();
    let opts = DisplayOpts {
        type_filter:   Some("task".to_string()),
        tag_filter:    None,
        status_filter: None,
        name_filter:   None,
    };
    for el in char_elements(&els) {
        assert!(!visible(el, &opts), "character must not pass task type filter");
    }
}

#[test]
fn test_tag_filter_matches_character() {
    use mps::commands::{visible, DisplayOpts};
    let els = parse_char_fixture();
    let opts = DisplayOpts {
        type_filter:   None,
        tag_filter:    Some("mahfuz-vai".to_string()),
        status_filter: None,
        name_filter:   None,
    };
    let matched = els.values()
        .filter(|e| !e.is_mps_group() && !e.is_unknown() && visible(e, &opts))
        .count();
    assert!(matched >= 1, "at least one character should match tag mahfuz-vai");
}

#[test]
fn test_name_filter_matches_character() {
    use mps::commands::{visible, DisplayOpts};
    let els = parse_char_fixture();
    let opts = DisplayOpts {
        type_filter:   None,
        tag_filter:    None,
        status_filter: None,
        name_filter:   Some("Mahfuz Vai".to_string()),
    };
    let matched = els.values()
        .filter(|e| !e.is_mps_group() && !e.is_unknown() && visible(e, &opts))
        .count();
    assert_eq!(matched, 1, "exactly one character named Mahfuz Vai");
}

#[test]
fn test_name_filter_case_insensitive() {
    use mps::commands::{visible, DisplayOpts};
    let els = parse_char_fixture();
    let opts = DisplayOpts {
        type_filter:   None,
        tag_filter:    None,
        status_filter: None,
        name_filter:   Some("mahfuz vai".to_string()),
    };
    let matched = els.values()
        .filter(|e| !e.is_mps_group() && !e.is_unknown() && visible(e, &opts))
        .count();
    assert_eq!(matched, 1, "name filter should be case-insensitive");
}

#[test]
fn test_name_filter_excludes_nameless_character() {
    use mps::commands::{visible, DisplayOpts};
    let els = parse_char_fixture();
    let opts = DisplayOpts {
        type_filter:   None,
        tag_filter:    None,
        status_filter: None,
        name_filter:   Some("Mahfuz Vai".to_string()),
    };
    // The nameless character (bare braces) must not match
    let nameless = els.values()
        .find(|e| e.kind() == ElementKind::Character && e.raw_args().is_empty())
        .expect("bare character must exist");
    assert!(!visible(nameless, &opts));
}

#[test]
fn test_name_filter_excludes_task() {
    use mps::commands::{visible, DisplayOpts};
    let els = parse_char_fixture();
    let opts = DisplayOpts {
        type_filter:   None,
        tag_filter:    None,
        status_filter: None,
        name_filter:   Some("Mahfuz Vai".to_string()),
    };
    let task = els.values().find(|e| e.kind() == ElementKind::Task).expect("task in fixture");
    assert!(!visible(task, &opts), "task must not pass name filter");
}

#[test]
fn test_status_filter_excludes_character() {
    use mps::commands::{visible, DisplayOpts};
    let els = parse_char_fixture();
    let opts = DisplayOpts {
        type_filter:   None,
        tag_filter:    None,
        status_filter: Some("open".to_string()),
        name_filter:   None,
    };
    for el in char_elements(&els) {
        assert!(!visible(el, &opts), "character must not pass status filter");
    }
}

// ── Display ───────────────────────────────────────────────────────────────

#[test]
fn test_type_badge_character_contains_bracket_text() {
    use mps::commands::type_badge;
    let badge = type_badge(&ElementKind::Character);
    // Strip ANSI codes by checking the raw string contains "character"
    assert!(badge.contains("character"), "badge must contain 'character'");
}

#[test]
fn test_element_extra_character_with_name() {
    use mps::commands::element_extra;
    let el = Element::from_parts(
        "character",
        "name: Mahfuz Vai".to_string(),
        vec![],
        "body".to_string(),
    );
    let extra = element_extra(&el);
    assert!(extra.contains("Mahfuz Vai"), "element_extra must include the name");
}

#[test]
fn test_element_extra_character_without_name() {
    use mps::commands::element_extra;
    let el = Element::from_parts(
        "character",
        "mahfuz-vai".to_string(),
        vec![],
        "body".to_string(),
    );
    let extra = element_extra(&el);
    assert!(extra.is_empty(), "element_extra must be empty when name absent");
}

// ── Append roundtrip ──────────────────────────────────────────────────────

#[test]
fn test_character_append_roundtrip_name_and_tags() {
    let (_dir, store) = fixture_store();
    let date = make_date(2026, 6, 30);
    store.append(
        "character",
        "He did me a great favor",
        &["mahfuz-vai".to_string(), "favor".to_string()],
        &[("name", "Mahfuz Vai")],
        date,
    ).unwrap();
    let els = store.parse_date(date).unwrap();
    let el = els.values()
        .find(|e| e.kind() == ElementKind::Character)
        .expect("character not found after append");
    if let Element::Character { data, .. } = el {
        assert_eq!(data.name.as_deref(), Some("Mahfuz Vai"));
        assert!(data.tags.contains(&"mahfuz-vai".to_string()));
        assert!(data.tags.contains(&"favor".to_string()));
    }
    assert_eq!(el.body_str().trim(), "He did me a great favor");
}

#[test]
fn test_character_append_roundtrip_no_name() {
    let (_dir, store) = fixture_store();
    let date = make_date(2026, 6, 30);
    store.append("character", "Anonymous observation", &["anonymous".to_string()], &[], date).unwrap();
    let els = store.parse_date(date).unwrap();
    let el = els.values()
        .find(|e| e.kind() == ElementKind::Character)
        .expect("character not found after append");
    if let Element::Character { data, .. } = el {
        assert!(data.name.is_none());
        assert!(data.tags.contains(&"anonymous".to_string()));
    }
}

#[test]
fn test_character_rewrite_name_attr() {
    let (_dir, store) = fixture_store();
    let date = make_date(2026, 6, 30);
    store.append("character", "Knew them before", &[], &[], date).unwrap();
    let els = store.parse_date(date).unwrap();
    let (ref_key, _) = els.iter()
        .find(|(_, e)| e.kind() == ElementKind::Character)
        .expect("character not found");
    let ref_key = ref_key.clone();

    let mut new_attrs = HashMap::new();
    new_attrs.insert("name".to_string(), "Late Name".to_string());
    let ok = store.rewrite_element(&ref_key, &new_attrs, date).unwrap();
    assert!(ok, "rewrite_element must succeed");

    let els2 = store.parse_date(date).unwrap();
    let el = els2.values().find(|e| e.kind() == ElementKind::Character).unwrap();
    if let Element::Character { data, .. } = el {
        assert_eq!(data.name.as_deref(), Some("Late Name"));
    }
}

// ── Export ────────────────────────────────────────────────────────────────

#[test]
fn test_export_character_kind_field() {
    // build_record is private; test via store + parse
    let els = parse_char_fixture();
    for el in char_elements(&els) {
        assert_eq!(el.sign(), "character");
    }
}

#[test]
fn test_character_stats_counting() {
    let els = parse_char_fixture();
    let char_count = els.values().filter(|e| e.kind() == ElementKind::Character).count();
    assert_eq!(char_count, 4, "fixture has exactly 4 character elements");
    let task_count = els.values().filter(|e| e.kind() == ElementKind::Task).count();
    assert_eq!(task_count, 1, "fixture has exactly 1 task element");
}

// ═══════════════════════════════════════════════════════════════════════════
// ITERATION 31: Pre-release robustness sweep
// ═══════════════════════════════════════════════════════════════════════════

// ── Config ────────────────────────────────────────────────────────────────

#[test]
fn test_config_init_creates_file_with_defaults() {
    use mps::config::Config;
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join(".mps_config.yaml");
    assert!(!path.exists());
    Config::init(&path).unwrap();
    assert!(path.exists());
    let cfg = Config::load(&path).unwrap();
    assert_eq!(cfg.git_remote, "origin");
    assert_eq!(cfg.git_branch, "master");
    assert_eq!(cfg.default_command, "open");
    assert!(cfg.type_aliases.is_empty());
    assert!(cfg.command_aliases.is_empty());
}

#[test]
fn test_config_init_is_idempotent() {
    use mps::config::Config;
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join(".mps_config.yaml");
    Config::init(&path).unwrap();
    let content_first = std::fs::read_to_string(&path).unwrap();
    Config::init(&path).unwrap(); // second call must not overwrite
    let content_second = std::fs::read_to_string(&path).unwrap();
    assert_eq!(content_first, content_second);
}

#[test]
fn test_config_load_nonexistent_returns_error() {
    use mps::config::Config;
    use mps::error::MpsError;
    let path = std::path::Path::new("/tmp/mps_does_not_exist_xyz.yaml");
    let err = Config::load(path).unwrap_err();
    assert!(matches!(err, MpsError::ConfigNotFound(_)));
}

#[test]
fn test_config_ensure_dirs_creates_directories() {
    use mps::config::Config;
    let dir = tempfile::tempdir().unwrap();
    let mps_dir = dir.path().join("mps_dir");
    let storage_dir = mps_dir.join("storage");
    let log_file = mps_dir.join("mps.log");
    let cfg = Config {
        mps_dir: mps_dir.clone(),
        storage_dir: storage_dir.clone(),
        log_file: log_file.clone(),
        git_remote: "origin".into(),
        git_branch: "master".into(),
        default_command: "open".into(),
        type_aliases: std::collections::HashMap::new(),
        command_aliases: std::collections::HashMap::new(),
    };
    assert!(!mps_dir.exists());
    cfg.ensure_dirs().unwrap();
    assert!(mps_dir.exists());
    assert!(storage_dir.exists());
    assert!(log_file.exists());
}

// ── Date parsing ──────────────────────────────────────────────────────────

#[test]
fn test_date_parse_rejects_garbage_string() {
    use mps::date_parse::parse_date;
    assert!(parse_date("not-a-date").is_err());
    assert!(parse_date("99999999").is_err());
    assert!(parse_date("the beginning of time").is_err());
}

#[test]
fn test_date_parse_rejects_empty_string() {
    use mps::date_parse::parse_date;
    assert!(parse_date("").is_err());
}

#[test]
fn test_date_parse_absolute_formats_accepted() {
    use mps::date_parse::parse_date;
    let d1 = parse_date("20260421").unwrap();
    let d2 = parse_date("2026-04-21").unwrap();
    assert_eq!(d1, d2);
    assert_eq!(d1.format("%Y-%m-%d").to_string(), "2026-04-21");
}

// ── ElementKind round-trip ────────────────────────────────────────────────

#[test]
fn test_element_kind_all_known_signs_round_trip() {
    for sign in &["task", "note", "log", "reminder", "mps", "character"] {
        let kind = ElementKind::from_sign(sign);
        assert_ne!(kind, ElementKind::Unknown, "sign '{}' must map to a known kind", sign);
        assert_eq!(kind.to_string(), *sign, "kind→sign must be identity for '{}'", sign);
    }
}

#[test]
fn test_element_kind_unknown_sign_maps_to_unknown() {
    let kind = ElementKind::from_sign("sprocket");
    assert_eq!(kind, ElementKind::Unknown);
}

// ── Append valid/invalid types ────────────────────────────────────────────

#[test]
fn test_store_append_all_valid_element_types() {
    let (_dir, store) = fixture_store();
    let date = make_date(2026, 9, 1);
    for kind in &["task", "note", "log", "reminder", "character"] {
        store.append(kind, &format!("body for {}", kind), &[], &[], date)
            .unwrap_or_else(|e| panic!("append '{}' failed: {}", kind, e));
    }
    let els = store.parse_date(date).unwrap();
    let kinds: Vec<&str> = els.values()
        .filter(|e| !e.is_mps_group() && !e.is_unknown())
        .map(|e| e.sign())
        .collect();
    assert!(kinds.contains(&"task"));
    assert!(kinds.contains(&"note"));
    assert!(kinds.contains(&"log"));
    assert!(kinds.contains(&"reminder"));
    assert!(kinds.contains(&"character"));
}

// ── DisplayOpts / visible() ───────────────────────────────────────────────

#[test]
fn test_visible_with_type_and_tag_filter_combined() {
    use mps::commands::{visible, DisplayOpts};
    let els = parse_char_fixture();
    let opts = DisplayOpts {
        type_filter:   Some("character".into()),
        tag_filter:    Some("favor".into()),
        status_filter: None,
        name_filter:   None,
    };
    let matching: Vec<_> = els.values().filter(|e| visible(e, &opts)).collect();
    // Fixture has two character elements with "favor" tag
    assert_eq!(matching.len(), 2);
    assert!(matching.iter().all(|e| e.sign() == "character"));
    assert!(matching.iter().all(|e| e.tags().iter().any(|t| t == "favor")));
}

#[test]
fn test_visible_name_filter_excludes_non_character() {
    use mps::commands::{visible, DisplayOpts};
    let opts = DisplayOpts {
        type_filter:   None,
        tag_filter:    None,
        status_filter: None,
        name_filter:   Some("anyone".into()),
    };
    // A task should be excluded when name_filter is set
    let task_raw = "@task[work]{\n  Fix bug\n}\n";
    let els = parser::parse_str(task_raw);
    let all_excluded = els.values().all(|e| !visible(e, &opts));
    assert!(all_excluded, "name_filter must exclude non-character elements");
}

#[test]
fn test_visible_status_filter_excludes_non_task() {
    use mps::commands::{visible, DisplayOpts};
    let opts = DisplayOpts {
        type_filter:   None,
        tag_filter:    None,
        status_filter: Some("open".into()),
        name_filter:   None,
    };
    let raw = "@note{\n  Just a note\n}\n";
    let els = parser::parse_str(raw);
    let all_excluded = els.values().all(|e| !visible(e, &opts));
    assert!(all_excluded, "status_filter must exclude non-task elements");
}

// ── Character edge cases ──────────────────────────────────────────────────

#[test]
fn test_character_name_with_apostrophe() {
    let raw = "@character[name: O'Brien, colleague]{\n  Helped with the migration.\n}\n";
    let els = parser::parse_str(raw);
    let chars: Vec<_> = els.values().filter(|e| e.sign() == "character").collect();
    assert_eq!(chars.len(), 1);
    if let Element::Character { data, .. } = chars[0] {
        assert_eq!(data.name.as_deref(), Some("O'Brien"));
        assert!(data.tags.iter().any(|t| t == "colleague"));
    } else {
        panic!("expected Character element");
    }
}

#[test]
fn test_character_name_with_comma_inside_quotes_parsed_as_tag() {
    // Without quoting, "Smith, Jr" splits on comma — name gets "Smith" and "Jr" becomes a tag
    let raw = "@character[name: Smith, jr, trusted]{\n  Body\n}\n";
    let els = parser::parse_str(raw);
    let chars: Vec<_> = els.values().filter(|e| e.sign() == "character").collect();
    assert_eq!(chars.len(), 1);
    if let Element::Character { data, .. } = chars[0] {
        // After the colon, the raw_args parser splits on comma, so name ends at first comma
        assert_eq!(data.name.as_deref(), Some("Smith"));
    } else {
        panic!("expected Character element");
    }
}

#[test]
fn test_character_rewrite_roundtrip_preserves_body() {
    let (_dir, store) = fixture_store();
    let date = make_date(2026, 9, 10);
    store.append("character", "A trusted advisor.", &["mentor".into()], &[("name", "Dr. Kay")], date).unwrap();
    let els = store.parse_date(date).unwrap();
    let (ref_key, _) = els.iter().find(|(_, e)| e.sign() == "character").unwrap();
    let ref_key = ref_key.clone();

    let mut attrs = HashMap::new();
    attrs.insert("name".to_string(), "Dr. Kay".to_string());
    attrs.insert("extra".to_string(), "added".to_string());
    let updated = store.rewrite_element(&ref_key, &attrs, date).unwrap();
    assert!(updated);

    let els2 = store.parse_date(date).unwrap();
    let char_el = els2.values().find(|e| e.sign() == "character").unwrap();
    assert!(char_el.body_str().contains("A trusted advisor."));
}

// ── Export ────────────────────────────────────────────────────────────────

#[test]
fn test_export_csv_character_row_has_name_field() {
    let (_dir, store) = fixture_store();
    let date = make_date(2026, 9, 15);
    store.append("character", "Met at the conference.", &["networking".into()], &[("name", "Sam Lee")], date).unwrap();

    let els = store.parse_date(date).unwrap();
    let char_el = els.values().find(|e| e.sign() == "character").unwrap();
    if let Element::Character { data, .. } = char_el {
        assert_eq!(data.name.as_deref(), Some("Sam Lee"));
    } else {
        panic!("expected Character element");
    }
    // typed_attrs must contain name for CSV export
    let attrs = char_el.typed_attrs();
    assert!(attrs.iter().any(|(k, v)| k == "name" && v == "Sam Lee"),
        "typed_attrs must expose name for export; got {:?}", attrs);
}

#[test]
fn test_export_log_typed_attrs_start_end() {
    let (_dir, store) = fixture_store();
    let date = make_date(2026, 9, 16);
    store.append("log", "Deep work.", &[], &[("start", "09:00"), ("end", "11:30")], date).unwrap();

    let els = store.parse_date(date).unwrap();
    let log_el = els.values().find(|e| e.sign() == "log").unwrap();
    let attrs = log_el.typed_attrs();
    let starts: Vec<_> = attrs.iter().filter(|(k, _)| k == "start").collect();
    let ends: Vec<_> = attrs.iter().filter(|(k, _)| k == "end").collect();
    assert!(!starts.is_empty(), "log typed_attrs must contain start");
    assert!(!ends.is_empty(), "log typed_attrs must contain end");
}

// ── Store rewrite edge cases ──────────────────────────────────────────────

#[test]
fn test_rewrite_sets_status_open_to_done() {
    let (_dir, store) = fixture_store();
    let date = make_date(2026, 9, 20);
    store.append("task", "A task to complete.", &["work".into()], &[], date).unwrap();

    let els = store.parse_date(date).unwrap();
    let (ref_key, task) = els.iter().find(|(_, e)| e.sign() == "task").unwrap();
    if let Element::Task { data, .. } = task {
        assert!(!data.is_done(), "task must start as open");
    }
    let ref_key = ref_key.clone();
    let mut attrs = HashMap::new();
    attrs.insert("status".to_string(), "done".to_string());
    let updated = store.rewrite_element(&ref_key, &attrs, date).unwrap();
    assert!(updated);

    let els2 = store.parse_date(date).unwrap();
    let task2 = els2.values().find(|e| e.sign() == "task").unwrap();
    if let Element::Task { data, .. } = task2 {
        assert!(data.is_done(), "task must be done after rewrite");
    } else {
        panic!("expected Task element");
    }
}

#[test]
fn test_rewrite_reminder_at_time() {
    let (_dir, store) = fixture_store();
    let date = make_date(2026, 9, 21);
    store.append("reminder", "Call the client.", &[], &[("at", "3pm")], date).unwrap();

    let els = store.parse_date(date).unwrap();
    let (ref_key, _) = els.iter().find(|(_, e)| e.sign() == "reminder").unwrap();
    let ref_key = ref_key.clone();

    let mut attrs = HashMap::new();
    attrs.insert("at".to_string(), "5pm".to_string());
    let updated = store.rewrite_element(&ref_key, &attrs, date).unwrap();
    assert!(updated);

    let els2 = store.parse_date(date).unwrap();
    let rem = els2.values().find(|e| e.sign() == "reminder").unwrap();
    if let Element::Reminder { data, .. } = rem {
        assert_eq!(data.at.as_deref(), Some("5pm"));
    } else {
        panic!("expected Reminder element");
    }
}

// ── Ref resolver ──────────────────────────────────────────────────────────

#[test]
fn test_ref_resolver_character_gets_own_counter() {
    let raw = "@character[name: Alice]{\n  Body A\n}\n@character[name: Bob]{\n  Body B\n}\n@task{\n  A task\n}\n";
    let els = parser::parse_str(raw);
    let resolver = RefResolver::new(&els);

    let char_refs: Vec<&str> = els.keys()
        .filter(|k| els[*k].sign() == "character")
        .map(|k| resolver.to_human(k).unwrap_or("?"))
        .collect();
    assert!(char_refs.iter().any(|r| r.starts_with("character-1")), "first character must be character-1");
    assert!(char_refs.iter().any(|r| r.starts_with("character-2")), "second character must be character-2");

    let task_refs: Vec<&str> = els.keys()
        .filter(|k| els[*k].sign() == "task")
        .map(|k| resolver.to_human(k).unwrap_or("?"))
        .collect();
    assert!(task_refs.iter().any(|r| r.starts_with("task-1")));
}

#[test]
fn test_ref_resolver_to_epoch_roundtrip_for_character() {
    let raw = "@character[name: Carol]{\n  Notes on Carol\n}\n";
    let els = parser::parse_str(raw);
    let resolver = RefResolver::new(&els);

    let epoch_key = els.keys().find(|k| els[*k].sign() == "character").unwrap();
    let human = resolver.to_human(epoch_key).unwrap();
    let back = resolver.to_epoch(human).unwrap();
    assert_eq!(back, epoch_key);
}
