//! Integration coverage for on-disk memory artifacts.
//!
//! Uses a real Slack ingest to create raw/source artifacts, then stages a
//! mocked summary record to verify the Obsidian-compatible vault layout and
//! frontmatter contract without requiring a live summarizer model.

use tempfile::tempdir;

use chrono::{TimeZone, Utc};

use openhuman_core::openhuman::config::Config;
use openhuman_core::openhuman::memory::tree_source::registry::get_or_create_source_tree;
use openhuman_core::openhuman::memory_queue::drain_until_idle;
use openhuman_core::openhuman::memory_store::content::atomic::stage_summary;
use openhuman_core::openhuman::memory_store::content::obsidian::ensure_obsidian_defaults;
use openhuman_core::openhuman::memory_store::content::wiki_git::{
    get_read_pointer_tag, set_read_pointer_tag,
};
use openhuman_core::openhuman::memory_store::content::{SummaryComposeInput, SummaryTreeKind};
use openhuman_core::openhuman::memory_sync::composio::providers::slack::ingest::ingest_page_into_memory_tree;
use openhuman_core::openhuman::memory_sync::composio::providers::slack::SlackMessage;
use openhuman_core::openhuman::memory_tree::ingest::{ingest_summary, SummaryIngestInput};

fn make_config(workspace_dir: &std::path::Path) -> Config {
    let mut config = Config::default();
    config.workspace_dir = workspace_dir.to_path_buf();
    config
}

#[tokio::test]
async fn sync_raw_artifacts_and_mocked_summary_match_obsidian_contract() {
    let tmp = tempdir().expect("tempdir");
    let workspace_dir = tmp.path().join("workspace");
    std::fs::create_dir_all(&workspace_dir).expect("workspace dir");
    let config = make_config(&workspace_dir);

    let ts = Utc.timestamp_opt(1_700_000_000, 0).single().unwrap();
    let msg = SlackMessage {
        channel_id: "C123".into(),
        channel_name: "engineering".into(),
        is_private: false,
        author: "alice".into(),
        author_id: "U123".into(),
        text: "Phoenix migration launch window is Friday at 22:00 UTC.".into(),
        timestamp: ts,
        ts_raw: "1700000000.000100".into(),
        thread_ts: None,
        permalink: Some("https://slack.example.test/archives/C123/p1700000000000100".into()),
    };
    ingest_page_into_memory_tree(&config, "alice", "conn-slack-1", &[msg])
        .await
        .expect("seed slack sync");
    drain_until_idle(&config).await.expect("drain sync jobs");

    let content_root = config.memory_tree_content_root();
    let raw_file = content_root
        .join("raw")
        .join("slack-conn-slack-1")
        .join("chats")
        .join("1700000000000_1700000000.000100.md");
    let source_file = content_root
        .join("raw")
        .join("slack-conn-slack-1")
        .join("_source.md");
    assert!(raw_file.exists(), "expected raw markdown artifact");
    assert!(source_file.exists(), "expected source registry mirror");

    let raw_body = std::fs::read_to_string(&raw_file).expect("read raw artifact");
    assert!(raw_body.contains("**Channel:** #engineering"));
    assert!(raw_body.contains("**Author:** alice"));
    assert!(raw_body.contains("Phoenix migration launch window"));

    ensure_obsidian_defaults(&content_root).expect("stage obsidian defaults");

    let sealed_at = Utc.with_ymd_and_hms(2026, 5, 24, 22, 0, 0).unwrap();
    let child_ids = vec!["chunk-1".to_string()];
    let child_basenames = vec![Some("1700000000000_1700000000.000100".to_string())];
    let staged = stage_summary(
        &content_root,
        &SummaryComposeInput {
            summary_id: "summary:1760000000000:L1-phoenix-window",
            tree_kind: SummaryTreeKind::Source,
            tree_id: "source:slack",
            tree_scope: "slack:conn-slack-1",
            level: 1,
            child_ids: &child_ids,
            child_basenames: Some(&child_basenames),
            child_count: 1,
            time_range_start: ts,
            time_range_end: ts,
            sealed_at,
            body: "Phoenix migration launch window confirmed for Friday 22:00 UTC.",
        },
        "slack-conn-slack-1",
    )
    .expect("stage mocked summary");

    let summary_path = content_root.join(&staged.content_path);
    assert!(summary_path.exists(), "summary markdown should be written");
    let summary_body = std::fs::read_to_string(&summary_path).expect("read summary markdown");
    assert!(summary_body.contains("tree_kind: source"));
    assert!(summary_body.contains("tree_scope: \"slack:conn-slack-1\""));
    assert!(summary_body.contains("time_range_start: 2023-11-14T22:13:20+00:00"));
    assert!(summary_body.contains("time_range_end: 2023-11-14T22:13:20+00:00"));
    assert!(summary_body.contains("sealed_at: 2026-05-24T22:00:00+00:00"));
    assert!(summary_body.contains("[[1700000000000_1700000000.000100]]"));

    let graph_json = content_root.join(".obsidian").join("graph.json");
    let types_json = content_root.join(".obsidian").join("types.json");
    assert!(graph_json.exists(), "graph defaults should exist");
    assert!(types_json.exists(), "type hints should exist");
    let types_body = std::fs::read_to_string(types_json).expect("read types.json");
    assert!(types_body.contains("\"time_range_start\": \"date\""));
    assert!(types_body.contains("\"sealed_at\": \"datetime\""));
}

#[tokio::test]
async fn summary_ingest_records_summary_only_git_history_and_timestamped_read_tags() {
    let tmp = tempdir().expect("tempdir");
    let workspace_dir = tmp.path().join("workspace");
    std::fs::create_dir_all(&workspace_dir).expect("workspace dir");
    let config = make_config(&workspace_dir);
    let content_root = config.memory_tree_content_root();
    let raw_path = content_root.join("wiki/raw/not-tracked.md");
    std::fs::create_dir_all(raw_path.parent().unwrap()).expect("raw dir");
    std::fs::write(&raw_path, "should stay out of git history").expect("seed raw file");

    let tree = get_or_create_source_tree(&config, "github:tinyhumansai/openhuman")
        .expect("create source tree");
    let start = Utc.with_ymd_and_hms(2026, 6, 26, 9, 0, 0).unwrap();
    let end = Utc.with_ymd_and_hms(2026, 6, 26, 9, 30, 0).unwrap();
    let outcome = ingest_summary(
        &config,
        &tree,
        SummaryIngestInput {
            content: "Memory wiki git history now records summary-node seals.".to_string(),
            token_count: 64,
            entities: Vec::new(),
            topics: vec!["memory".to_string()],
            time_range_start: start,
            time_range_end: end,
            score: 0.8,
            child_labels: vec!["commit:abc123".to_string(), "issue:4142".to_string()],
            child_basenames: Vec::new(),
        },
    )
    .await
    .expect("ingest summary");

    let wiki_root = content_root.join("wiki");
    let repo = git2::Repository::open(&wiki_root).expect("wiki git repo should be initialized");
    let head = repo.head().expect("wiki head").peel_to_commit().unwrap();
    let tree_obj = head.tree().expect("wiki commit tree");

    let repo_summary_path = outcome
        .content_path
        .strip_prefix("wiki/")
        .expect("summary path should live under wiki/");
    tree_obj
        .get_path(std::path::Path::new(repo_summary_path))
        .expect("summary markdown should be tracked");
    assert!(
        tree_obj
            .get_path(std::path::Path::new("raw/not-tracked.md"))
            .is_err(),
        "git history should remain scoped to summaries"
    );

    let message = head.message().expect("commit message");
    assert!(message.contains("Seal memory tree github:tinyhumansai/openhuman L1 summaries"));
    assert!(message.contains("Reason: summary_ingest"));
    assert!(message.contains("Summary-Count: 1"));
    assert!(message.contains("Child-Count: 2"));
    assert!(message.contains("Token-Count: 64"));
    assert!(message.contains(&outcome.summary_id));
    assert!(message.contains(repo_summary_path));

    let head_id = head.id().to_string();
    let tagged = set_read_pointer_tag(&content_root, "agent:e2e", None)
        .expect("set timestamped read pointer tag");
    assert_eq!(tagged, head_id);
    assert_eq!(
        get_read_pointer_tag(&content_root, "agent:e2e")
            .expect("read latest pointer")
            .as_deref(),
        Some(head_id.as_str())
    );

    let tag_prefix = format!("refs/tags/read/{}/", hex::encode("agent:e2e".as_bytes()));
    let tags = repo.references().unwrap().fold(Vec::new(), |mut acc, r| {
        let r = r.unwrap();
        let name = r.name().unwrap();
        if name.starts_with(&tag_prefix) {
            acc.push(name.to_string());
        }
        acc
    });
    assert!(tags.iter().any(|name| name.ends_with("/latest")));
    assert!(tags.iter().any(|name| {
        let suffix = name.strip_prefix(&tag_prefix).unwrap_or_default();
        suffix.len() == "20260626T090000.000000000Z".len()
            && suffix.ends_with('Z')
            && suffix.contains('T')
    }));
}
