//! Lifecycle → Vault canonical note 回写。
//!
//! 负责把 `MemoryRecord` 按 `docs/OBSIDIAN_SCHEMA.md` 的 frontmatter 契约渲染成单个 Markdown
//! note,落入 `50-Memory-Ledger/Extracted/<record_id>.md`。
//!
//! 幂等 + body 保护:frontmatter 里保存上次写入时的 body hash (`spool_body_hash`),
//! 再次回写时若磁盘上 body 的实际 hash 与 stored 不一致,视为用户手改,保留用户 body
//! 只重写 frontmatter;一致则按 record 重新渲染 body。archive 不删文件,仅打 archived 标记。

use crate::domain::{MemoryLifecycleState, MemoryRecord, MemoryScope, MemorySourceKind};
use crate::lifecycle_store::LedgerEntry;
use anyhow::{Context, Result, bail};
use std::collections::BTreeMap;
use std::fs;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};

pub const MEMORY_LEDGER_DIR: &str = "50-Memory-Ledger/Extracted";
pub const MEMORY_LEDGER_COMPILED_DIR: &str = "50-Memory-Ledger/Compiled";
pub const NOTE_VERSION: &str = "memory-note.v1";
pub const BODY_HASH_KEY: &str = "spool_body_hash";
pub const VERSION_KEY: &str = "spool_version";

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WriteStatus {
    Created,
    UpdatedAll,
    UpdatedPreserveBody,
    Unchanged,
}

#[derive(Debug, Clone)]
pub struct VaultWriteResult {
    pub path: PathBuf,
    pub status: WriteStatus,
    pub body_user_edited: bool,
}

pub fn memory_note_path(vault_root: &Path, record_id: &str) -> PathBuf {
    vault_root
        .join(MEMORY_LEDGER_DIR)
        .join(format!("{record_id}.md"))
}

/// 根据 memory_type 分发目标子目录: `knowledge` (Karpathy wiki compiled 页) 走
/// `Compiled/`,其他碎片记忆走 `Extracted/`。保持 Extracted 为默认以兼容现有
/// 测试与 MCP 路径引用。
pub fn memory_note_path_for(vault_root: &Path, record_id: &str, memory_type: &str) -> PathBuf {
    let dir = if memory_type == "knowledge" {
        MEMORY_LEDGER_COMPILED_DIR
    } else {
        MEMORY_LEDGER_DIR
    };
    vault_root.join(dir).join(format!("{record_id}.md"))
}

pub fn write_memory_note(
    vault_root: &Path,
    record_id: &str,
    record: &MemoryRecord,
) -> Result<VaultWriteResult> {
    if record_id.is_empty() {
        bail!("record_id must not be empty");
    }
    let path = memory_note_path_for(vault_root, record_id, &record.memory_type);
    let existing = read_existing_note(&path)?;

    let desired_body = render_body(record);
    let (final_body, base_status, body_user_edited) = match &existing {
        None => (desired_body, WriteStatus::Created, false),
        Some(existing) => {
            let current_hash = body_hash(&existing.body);
            let user_edited = existing
                .stored_body_hash
                .as_deref()
                .map(|stored| stored != current_hash)
                .unwrap_or(false);
            if user_edited {
                (
                    existing.body.clone(),
                    WriteStatus::UpdatedPreserveBody,
                    true,
                )
            } else {
                (desired_body, WriteStatus::UpdatedAll, false)
            }
        }
    };

    let fm = render_frontmatter(record_id, record, &final_body);
    let desired_content = format_note(&fm, &final_body)?;

    let status = if let Some(existing) = &existing {
        if existing.raw_content == desired_content {
            WriteStatus::Unchanged
        } else {
            base_status
        }
    } else {
        base_status
    };

    if status != WriteStatus::Unchanged {
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent).with_context(|| {
                format!(
                    "failed to create memory note parent dir {}",
                    parent.display()
                )
            })?;
        }
        fs::write(&path, &desired_content)
            .with_context(|| format!("failed to write memory note {}", path.display()))?;
    }

    Ok(VaultWriteResult {
        path,
        status,
        body_user_edited,
    })
}

pub fn archive_memory_note(vault_root: &Path, record_id: &str) -> Result<Option<VaultWriteResult>> {
    // 尝试两个目录:先 Extracted (原始碎片),再 Compiled (knowledge 综合页)。
    // archive 时拿不到 memory_type,用路径 fallback 覆盖两种情况。
    let extracted = memory_note_path(vault_root, record_id);
    let compiled = vault_root
        .join(MEMORY_LEDGER_COMPILED_DIR)
        .join(format!("{record_id}.md"));
    let path = if extracted.exists() {
        extracted
    } else if compiled.exists() {
        compiled
    } else {
        return Ok(None);
    };
    let existing = read_existing_note(&path)?.expect("path.exists guarded");
    let body = existing.body.clone();
    let body_hash_value = body_hash(&body);
    let mut fm = existing.frontmatter.clone();
    fm.insert("archived".to_string(), serde_yaml::Value::Bool(true));
    fm.insert(
        "archived_at".to_string(),
        serde_yaml::Value::String(current_timestamp()),
    );
    fm.insert(
        "state".to_string(),
        serde_yaml::Value::String("archived".to_string()),
    );
    fm.insert(
        "source_of_truth".to_string(),
        serde_yaml::Value::Bool(false),
    );
    fm.insert(
        BODY_HASH_KEY.to_string(),
        serde_yaml::Value::String(body_hash_value),
    );

    let content = format_note(&fm, &body)?;
    if existing.raw_content == content {
        return Ok(Some(VaultWriteResult {
            path,
            status: WriteStatus::Unchanged,
            body_user_edited: false,
        }));
    }
    fs::write(&path, &content)
        .with_context(|| format!("failed to archive memory note {}", path.display()))?;
    Ok(Some(VaultWriteResult {
        path,
        status: WriteStatus::UpdatedAll,
        body_user_edited: false,
    }))
}

// ---------- 内部渲染与读取 ----------

struct ExistingNote {
    frontmatter: BTreeMap<String, serde_yaml::Value>,
    body: String,
    stored_body_hash: Option<String>,
    raw_content: String,
}

fn read_existing_note(path: &Path) -> Result<Option<ExistingNote>> {
    if !path.exists() {
        return Ok(None);
    }
    let raw = fs::read_to_string(path)
        .with_context(|| format!("failed to read memory note {}", path.display()))?;
    let (fm_text, body) = split_frontmatter_raw(&raw);
    let frontmatter: BTreeMap<String, serde_yaml::Value> = match fm_text {
        Some(text) if !text.trim().is_empty() => serde_yaml::from_str(text)
            .with_context(|| format!("failed to parse frontmatter in {}", path.display()))?,
        _ => BTreeMap::new(),
    };
    let stored_body_hash = frontmatter
        .get(BODY_HASH_KEY)
        .and_then(|v| v.as_str())
        .map(ToString::to_string);
    Ok(Some(ExistingNote {
        frontmatter,
        body,
        stored_body_hash,
        raw_content: raw,
    }))
}

fn split_frontmatter_raw(raw: &str) -> (Option<&str>, String) {
    let rest = if let Some(r) = raw.strip_prefix("---\n") {
        r
    } else if let Some(r) = raw.strip_prefix("---\r\n") {
        r
    } else {
        return (None, raw.to_string());
    };
    if let Some(end) = rest.find("\n---\n") {
        let fm = &rest[..end];
        let body_start = end + "\n---\n".len();
        let body = strip_one_leading_newline(&rest[body_start..]);
        (Some(fm), body)
    } else if let Some(end) = rest.find("\n---\r\n") {
        let fm = &rest[..end];
        let body_start = end + "\n---\r\n".len();
        let body = strip_one_leading_newline(&rest[body_start..]);
        (Some(fm), body)
    } else if let Some(stripped) = rest.strip_suffix("\n---") {
        (Some(stripped), String::new())
    } else {
        (None, raw.to_string())
    }
}

fn strip_one_leading_newline(s: &str) -> String {
    if let Some(stripped) = s.strip_prefix("\r\n") {
        stripped.to_string()
    } else if let Some(stripped) = s.strip_prefix('\n') {
        stripped.to_string()
    } else {
        s.to_string()
    }
}

fn render_body(record: &MemoryRecord) -> String {
    let summary = record.summary.trim();

    if record.memory_type == "knowledge" {
        // Knowledge pages already have structured sections in summary
        format!(
            "# {title}\n\n{summary}\n",
            title = record.title.trim(),
            summary = if summary.is_empty() {
                "_no summary_"
            } else {
                summary
            },
        )
    } else {
        format!(
            "# {title}\n\n{summary}\n\n## Provenance\n\n- source_kind: {source_kind}\n- source_ref: {source_ref}\n",
            title = record.title.trim(),
            summary = if summary.is_empty() {
                "_no summary_"
            } else {
                summary
            },
            source_kind = format_source_kind(record.origin.source_kind),
            source_ref = record.origin.source_ref,
        )
    }
}

fn body_hash(body: &str) -> String {
    let mut hasher = std::collections::hash_map::DefaultHasher::new();
    body.hash(&mut hasher);
    format!("{:016x}", hasher.finish())
}

fn render_frontmatter(
    record_id: &str,
    record: &MemoryRecord,
    body: &str,
) -> BTreeMap<String, serde_yaml::Value> {
    use serde_yaml::Value;
    let mut fm = BTreeMap::new();
    fm.insert(
        VERSION_KEY.to_string(),
        Value::String(NOTE_VERSION.to_string()),
    );
    fm.insert(
        "record_id".to_string(),
        Value::String(record_id.to_string()),
    );
    fm.insert(
        "memory_type".to_string(),
        Value::String(record.memory_type.clone()),
    );
    fm.insert(
        "scope".to_string(),
        Value::String(map_scope(record.scope).to_string()),
    );
    fm.insert(
        "state".to_string(),
        Value::String(map_state(record.state).to_string()),
    );
    fm.insert(
        "source_of_truth".to_string(),
        Value::Bool(matches!(record.state, MemoryLifecycleState::Canonical)),
    );
    if let Some(pid) = &record.project_id {
        fm.insert("project_id".to_string(), Value::String(pid.clone()));
    }
    if let Some(uid) = &record.user_id {
        fm.insert("user_id".to_string(), Value::String(uid.clone()));
    }
    if let Some(sens) = &record.sensitivity {
        fm.insert("sensitivity".to_string(), Value::String(sens.clone()));
    }
    fm.insert(
        "source_kind".to_string(),
        Value::String(format_source_kind(record.origin.source_kind).to_string()),
    );
    fm.insert(
        "source_ref".to_string(),
        Value::String(record.origin.source_ref.clone()),
    );
    // Structured retrieval signals
    if !record.entities.is_empty() {
        fm.insert(
            "entities".to_string(),
            Value::Sequence(
                record
                    .entities
                    .iter()
                    .map(|s| Value::String(s.clone()))
                    .collect(),
            ),
        );
    }
    if !record.tags.is_empty() {
        fm.insert(
            "tags".to_string(),
            Value::Sequence(
                record
                    .tags
                    .iter()
                    .map(|s| Value::String(s.clone()))
                    .collect(),
            ),
        );
    }
    if !record.triggers.is_empty() {
        fm.insert(
            "triggers".to_string(),
            Value::Sequence(
                record
                    .triggers
                    .iter()
                    .map(|s| Value::String(s.clone()))
                    .collect(),
            ),
        );
    }
    if !record.related_files.is_empty() {
        fm.insert(
            "related_files".to_string(),
            Value::Sequence(
                record
                    .related_files
                    .iter()
                    .map(|s| Value::String(s.clone()))
                    .collect(),
            ),
        );
    }
    if !record.related_records.is_empty() {
        fm.insert(
            "related_memory".to_string(),
            Value::Sequence(
                record
                    .related_records
                    .iter()
                    .map(|s| Value::String(format!("[[{s}]]")))
                    .collect(),
            ),
        );
    }
    if let Some(supersedes) = &record.supersedes {
        fm.insert("supersedes".to_string(), Value::String(supersedes.clone()));
    }
    fm.insert(BODY_HASH_KEY.to_string(), Value::String(body_hash(body)));
    fm
}

fn format_note(fm: &BTreeMap<String, serde_yaml::Value>, body: &str) -> Result<String> {
    let yaml = serde_yaml::to_string(fm).context("failed to serialize frontmatter as yaml")?;
    let body_trimmed = body.trim_end_matches('\n');
    Ok(format!("---\n{yaml}---\n\n{body_trimmed}\n"))
}

fn map_scope(scope: MemoryScope) -> &'static str {
    match scope {
        MemoryScope::User => "personal",
        MemoryScope::Project => "project",
        MemoryScope::Workspace => "team",
        MemoryScope::Team => "team",
        MemoryScope::Agent => "personal",
    }
}

fn map_state(state: MemoryLifecycleState) -> &'static str {
    match state {
        MemoryLifecycleState::Draft => "draft",
        MemoryLifecycleState::Candidate => "candidate",
        MemoryLifecycleState::Accepted => "accepted",
        MemoryLifecycleState::Canonical => "canonical",
        MemoryLifecycleState::Archived => "archived",
    }
}

fn format_source_kind(kind: MemorySourceKind) -> &'static str {
    match kind {
        MemorySourceKind::Manual => "manual",
        MemorySourceKind::AiProposal => "ai_proposal",
        MemorySourceKind::SessionCapture => "session_capture",
        MemorySourceKind::Distilled => "distilled",
        MemorySourceKind::Imported => "imported",
    }
}

fn current_timestamp() -> String {
    let seconds = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs();
    format!("unix:{seconds}")
}

/// 根据 `LedgerEntry` 的最新 state 做 vault 回写,错误降级为 stderr warn。
///
/// - Accepted / Canonical → 写/更新 note
/// - Archived → 打 archived 标记
/// - Draft / Candidate → 不回写 (schema: 审核后才 promote)
pub fn apply_writeback_for_entry(
    vault_root: &Path,
    entry: &LedgerEntry,
) -> Option<VaultWriteResult> {
    match entry.record.state {
        MemoryLifecycleState::Archived => match archive_memory_note(vault_root, &entry.record_id) {
            Ok(result) => result,
            Err(error) => {
                log_writeback_error(&entry.record_id, &error);
                None
            }
        },
        MemoryLifecycleState::Accepted | MemoryLifecycleState::Canonical => {
            match write_memory_note(vault_root, &entry.record_id, &entry.record) {
                Ok(result) => Some(result),
                Err(error) => {
                    log_writeback_error(&entry.record_id, &error);
                    None
                }
            }
        }
        MemoryLifecycleState::Draft | MemoryLifecycleState::Candidate => None,
    }
}

/// 从 config_path 加载 AppConfig 并 resolve vault root,再做 writeback。
/// 任何 config/vault 解析错误都 swallow 为 None (不阻断业务)。
///
/// Side effects (都 swallow 为 stderr warn,不影响返回值):
/// - 刷 `<vault_root>/INDEX.md` (知识导航索引,Karpathy wiki 的 Query 层入口)
/// - 自动 compile:检测可合并集群,对新集群 auto-propose 为 candidate
///   (Karpathy wiki 的 Compile 层,只在 accepted/canonical ≥ 3 时才跑)
pub fn writeback_from_config(config_path: &Path, entry: &LedgerEntry) -> Option<VaultWriteResult> {
    let vault_root = match resolve_vault_root(config_path) {
        Ok(root) => root,
        Err(error) => {
            log_writeback_error(&entry.record_id, &error);
            return None;
        }
    };
    let result = apply_writeback_for_entry(&vault_root, entry);
    let _ = crate::wiki_index::refresh_index_from_config(config_path);
    let _ = crate::knowledge::auto_compile_from_config(config_path);
    result
}

/// Same as [`writeback_from_config`] but skips auto-compile. Used by
/// MCP handlers that will run LLM-assisted compile separately.
pub fn writeback_from_config_no_compile(
    config_path: &Path,
    entry: &LedgerEntry,
) -> Option<VaultWriteResult> {
    let vault_root = match resolve_vault_root(config_path) {
        Ok(root) => root,
        Err(error) => {
            log_writeback_error(&entry.record_id, &error);
            return None;
        }
    };
    let result = apply_writeback_for_entry(&vault_root, entry);
    let _ = crate::wiki_index::refresh_index_from_config(config_path);
    result
}

fn resolve_vault_root(config_path: &Path) -> Result<PathBuf> {
    let config = crate::app::load(config_path)
        .with_context(|| format!("failed to load config {}", config_path.display()))?;
    crate::app::resolve_override_path(&config.vault.root, config_path)
}

fn log_writeback_error(record_id: &str, error: &anyhow::Error) {
    eprintln!("[spool] vault writeback failed for record {record_id}: {error}");
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::domain::{
        MemoryLifecycleState, MemoryOrigin, MemoryRecord, MemoryScope, MemorySourceKind,
    };
    use tempfile::tempdir;

    fn sample_record(state: MemoryLifecycleState) -> MemoryRecord {
        MemoryRecord {
            title: "简洁输出".to_string(),
            summary: "偏好简短直接的回复,不要 trailing 总结".to_string(),
            memory_type: "preference".to_string(),
            scope: MemoryScope::User,
            state,
            origin: MemoryOrigin {
                source_kind: MemorySourceKind::Manual,
                source_ref: "manual:cli".to_string(),
            },
            project_id: None,
            user_id: Some("long".to_string()),
            sensitivity: Some("internal".to_string()),
            entities: Vec::new(),
            tags: Vec::new(),
            triggers: Vec::new(),
            related_files: Vec::new(),
            related_records: Vec::new(),
            supersedes: None,
            applies_to: Vec::new(),
            valid_until: None,
        }
    }

    #[test]
    fn write_memory_note_should_create_new_file_with_frontmatter_and_body() {
        let temp = tempdir().unwrap();
        let result = write_memory_note(
            temp.path(),
            "rec-001",
            &sample_record(MemoryLifecycleState::Accepted),
        )
        .unwrap();

        assert_eq!(result.status, WriteStatus::Created);
        assert!(!result.body_user_edited);
        let content = fs::read_to_string(&result.path).unwrap();
        assert!(content.starts_with("---\n"));
        assert!(content.contains("record_id: rec-001"));
        assert!(content.contains("memory_type: preference"));
        assert!(content.contains("scope: personal")); // user → personal
        assert!(content.contains("state: accepted"));
        assert!(content.contains("source_of_truth: false"));
        assert!(content.contains("spool_body_hash:"));
        assert!(content.contains("# 简洁输出"));
        assert!(content.contains("## Provenance"));
        assert!(content.contains("source_kind: manual"));
    }

    #[test]
    fn write_memory_note_should_mark_canonical_as_source_of_truth() {
        let temp = tempdir().unwrap();
        let result = write_memory_note(
            temp.path(),
            "rec-002",
            &sample_record(MemoryLifecycleState::Canonical),
        )
        .unwrap();
        let content = fs::read_to_string(&result.path).unwrap();
        assert!(content.contains("state: canonical"));
        assert!(content.contains("source_of_truth: true"));
    }

    #[test]
    fn write_memory_note_should_be_idempotent_on_identical_record() {
        let temp = tempdir().unwrap();
        let record = sample_record(MemoryLifecycleState::Accepted);
        let first = write_memory_note(temp.path(), "rec-003", &record).unwrap();
        assert_eq!(first.status, WriteStatus::Created);
        let second = write_memory_note(temp.path(), "rec-003", &record).unwrap();
        assert_eq!(second.status, WriteStatus::Unchanged);
    }

    #[test]
    fn write_memory_note_should_update_body_when_summary_changes() {
        let temp = tempdir().unwrap();
        let mut record = sample_record(MemoryLifecycleState::Accepted);
        write_memory_note(temp.path(), "rec-004", &record).unwrap();
        record.summary = "新的更短摘要".to_string();
        let result = write_memory_note(temp.path(), "rec-004", &record).unwrap();
        assert_eq!(result.status, WriteStatus::UpdatedAll);
        assert!(!result.body_user_edited);
        let content = fs::read_to_string(&result.path).unwrap();
        assert!(content.contains("新的更短摘要"));
    }

    #[test]
    fn write_memory_note_should_preserve_body_when_user_hand_edited() {
        let temp = tempdir().unwrap();
        let record = sample_record(MemoryLifecycleState::Accepted);
        let first = write_memory_note(temp.path(), "rec-005", &record).unwrap();

        // 模拟用户手改 body
        let original = fs::read_to_string(&first.path).unwrap();
        let user_edited =
            original.replace("# 简洁输出", "# 简洁输出\n\n> NOTE: 用户手动补充的上下文");
        fs::write(&first.path, user_edited).unwrap();

        // 再次回写,record 不变
        let result = write_memory_note(temp.path(), "rec-005", &record).unwrap();
        assert_eq!(result.status, WriteStatus::UpdatedPreserveBody);
        assert!(result.body_user_edited);
        let content = fs::read_to_string(&result.path).unwrap();
        assert!(content.contains("NOTE: 用户手动补充的上下文"));
    }

    #[test]
    fn archive_memory_note_should_mark_archived_and_keep_body() {
        let temp = tempdir().unwrap();
        let record = sample_record(MemoryLifecycleState::Accepted);
        write_memory_note(temp.path(), "rec-006", &record).unwrap();
        let result = archive_memory_note(temp.path(), "rec-006")
            .unwrap()
            .expect("archive should return result for existing file");
        assert_eq!(result.status, WriteStatus::UpdatedAll);
        let content = fs::read_to_string(&result.path).unwrap();
        assert!(content.contains("archived: true"));
        assert!(content.contains("archived_at: unix:"));
        assert!(content.contains("state: archived"));
        assert!(content.contains("# 简洁输出"));
    }

    #[test]
    fn archive_memory_note_should_return_none_for_missing_file() {
        let temp = tempdir().unwrap();
        assert!(
            archive_memory_note(temp.path(), "missing")
                .unwrap()
                .is_none()
        );
    }

    #[test]
    fn write_memory_note_should_reject_empty_record_id() {
        let temp = tempdir().unwrap();
        let err = write_memory_note(
            temp.path(),
            "",
            &sample_record(MemoryLifecycleState::Accepted),
        )
        .unwrap_err();
        assert!(err.to_string().contains("record_id"));
    }

    #[test]
    fn memory_note_path_should_use_extracted_dir_and_record_id() {
        let path = memory_note_path(Path::new("/vault"), "abc");
        assert_eq!(
            path,
            PathBuf::from("/vault/50-Memory-Ledger/Extracted/abc.md")
        );
    }

    #[test]
    fn memory_note_path_for_should_route_knowledge_to_compiled_dir() {
        let compiled = memory_note_path_for(Path::new("/vault"), "wiki-1", "knowledge");
        assert_eq!(
            compiled,
            PathBuf::from("/vault/50-Memory-Ledger/Compiled/wiki-1.md")
        );
        let fragment = memory_note_path_for(Path::new("/vault"), "frag-1", "preference");
        assert_eq!(
            fragment,
            PathBuf::from("/vault/50-Memory-Ledger/Extracted/frag-1.md")
        );
    }

    #[test]
    fn write_memory_note_should_place_knowledge_in_compiled_dir() {
        let temp = tempdir().unwrap();
        let mut record = sample_record(MemoryLifecycleState::Accepted);
        record.memory_type = "knowledge".to_string();
        let result = write_memory_note(temp.path(), "wiki-x", &record).unwrap();
        assert_eq!(result.status, WriteStatus::Created);
        assert!(
            temp.path()
                .join("50-Memory-Ledger/Compiled/wiki-x.md")
                .exists()
        );
        assert!(
            !temp
                .path()
                .join("50-Memory-Ledger/Extracted/wiki-x.md")
                .exists()
        );
    }

    fn sample_entry(record_id: &str, state: MemoryLifecycleState) -> LedgerEntry {
        LedgerEntry {
            schema_version: "memory-ledger.v1".to_string(),
            recorded_at: "unix:0".to_string(),
            record_id: record_id.to_string(),
            scope_key: "user".to_string(),
            action: crate::domain::MemoryLedgerAction::RecordManual,
            source_kind: MemorySourceKind::Manual,
            metadata: Default::default(),
            record: MemoryRecord {
                state,
                ..sample_record(state)
            },
        }
    }

    #[test]
    fn apply_writeback_should_write_for_accepted_and_canonical() {
        let temp = tempdir().unwrap();
        let entry_a = sample_entry("wb-accept", MemoryLifecycleState::Accepted);
        let entry_c = sample_entry("wb-canon", MemoryLifecycleState::Canonical);

        assert!(apply_writeback_for_entry(temp.path(), &entry_a).is_some());
        assert!(apply_writeback_for_entry(temp.path(), &entry_c).is_some());
        assert!(memory_note_path(temp.path(), "wb-accept").exists());
        assert!(memory_note_path(temp.path(), "wb-canon").exists());
    }

    #[test]
    fn apply_writeback_should_skip_draft_and_candidate() {
        let temp = tempdir().unwrap();
        let entry_d = sample_entry("wb-draft", MemoryLifecycleState::Draft);
        let entry_p = sample_entry("wb-cand", MemoryLifecycleState::Candidate);
        assert!(apply_writeback_for_entry(temp.path(), &entry_d).is_none());
        assert!(apply_writeback_for_entry(temp.path(), &entry_p).is_none());
        assert!(!memory_note_path(temp.path(), "wb-draft").exists());
        assert!(!memory_note_path(temp.path(), "wb-cand").exists());
    }

    #[test]
    fn apply_writeback_should_archive_when_state_archived() {
        let temp = tempdir().unwrap();
        let entry_a = sample_entry("wb-life", MemoryLifecycleState::Accepted);
        apply_writeback_for_entry(temp.path(), &entry_a);
        let archived_entry = sample_entry("wb-life", MemoryLifecycleState::Archived);
        let result = apply_writeback_for_entry(temp.path(), &archived_entry).unwrap();
        let content = fs::read_to_string(&result.path).unwrap();
        assert!(content.contains("archived: true"));
    }
}
