use super::{
    CompactionContext, CompactionState, GroundedFactRecord, SESSION_MEMORY_ENVELOPE_SCHEMA_VERSION,
    SessionMemoryEnvelope, build_server_compaction_context_management,
    build_summarized_fork_history, compact_history_for_recovery_in_place,
    compact_history_from_index_in_place, compact_history_in_place,
    compact_history_in_place_with_events, inject_latest_memory_envelope,
    latest_memory_envelope_path_for_session, manual_openai_compact_history_in_place,
    maybe_auto_compact_history, resolve_compaction_threshold,
};
use crate::agent::runloop::unified::context_manager::ContextManager;
use crate::agent::runloop::unified::inline_events::harness::HarnessEventEmitter;
use crate::agent::runloop::unified::state::SessionStats;
use async_trait::async_trait;
use hashbrown::HashMap;
use serde_json::json;
use std::fs;
use std::sync::Arc;
use tempfile::tempdir;
use tokio::sync::RwLock;
use vtcode_commons::llm::Usage;
use vtcode_core::config::constants::tools as tool_names;
use vtcode_core::config::loader::VTCodeConfig;
use vtcode_core::llm::provider::{
    LLMError, LLMProvider, LLMRequest, LLMResponse, Message, MessageRole,
    ResponsesCompactionOptions, ToolCall,
};

struct LocalCompactionProvider;

struct ProviderCompactionProvider;

struct NoOpProviderCompactionProvider;

struct FailingProviderCompactionProvider;

struct RecordingProviderCompactionProvider {
    seen_history: Arc<RwLock<Vec<Message>>>,
}

#[async_trait]
impl LLMProvider for LocalCompactionProvider {
    fn name(&self) -> &str {
        "stub"
    }

    async fn generate(&self, _request: LLMRequest) -> Result<LLMResponse, LLMError> {
        Ok(LLMResponse::new("stub-model", "summary"))
    }

    fn supported_models(&self) -> Vec<String> {
        vec!["stub-model".to_string()]
    }

    fn validate_request(&self, _request: &LLMRequest) -> Result<(), LLMError> {
        Ok(())
    }

    fn effective_context_size(&self, _model: &str) -> usize {
        1_000
    }
}

#[async_trait]
impl LLMProvider for ProviderCompactionProvider {
    fn name(&self) -> &str {
        "provider-stub"
    }

    async fn generate(&self, _request: LLMRequest) -> Result<LLMResponse, LLMError> {
        Ok(LLMResponse::new("stub-model", "summary"))
    }

    async fn compact_history(
        &self,
        _model: &str,
        history: &[Message],
    ) -> Result<Vec<Message>, LLMError> {
        let mut compacted = Vec::new();
        compacted.push(Message::system(
            "Previous conversation summary:\nProvider compacted history".to_string(),
        ));
        compacted.extend(history.iter().rev().take(2).cloned().collect::<Vec<_>>());
        compacted.reverse();
        Ok(compacted)
    }

    async fn compact_history_with_options(
        &self,
        model: &str,
        history: &[Message],
        _options: &ResponsesCompactionOptions,
    ) -> Result<Vec<Message>, LLMError> {
        self.compact_history(model, history).await
    }

    fn supports_responses_compaction(&self, _model: &str) -> bool {
        true
    }

    fn supports_manual_openai_compaction(&self, _model: &str) -> bool {
        true
    }

    fn supported_models(&self) -> Vec<String> {
        vec!["stub-model".to_string()]
    }

    fn validate_request(&self, _request: &LLMRequest) -> Result<(), LLMError> {
        Ok(())
    }

    fn effective_context_size(&self, _model: &str) -> usize {
        1_000
    }
}

#[async_trait]
impl LLMProvider for NoOpProviderCompactionProvider {
    fn name(&self) -> &str {
        "noop-provider-stub"
    }

    async fn generate(&self, _request: LLMRequest) -> Result<LLMResponse, LLMError> {
        Ok(LLMResponse::new("stub-model", "summary"))
    }

    async fn compact_history(
        &self,
        _model: &str,
        history: &[Message],
    ) -> Result<Vec<Message>, LLMError> {
        Ok(history.to_vec())
    }

    async fn compact_history_with_options(
        &self,
        _model: &str,
        history: &[Message],
        _options: &ResponsesCompactionOptions,
    ) -> Result<Vec<Message>, LLMError> {
        Ok(history.to_vec())
    }

    fn supports_responses_compaction(&self, _model: &str) -> bool {
        true
    }

    fn supports_manual_openai_compaction(&self, _model: &str) -> bool {
        true
    }

    fn supported_models(&self) -> Vec<String> {
        vec!["stub-model".to_string()]
    }

    fn validate_request(&self, _request: &LLMRequest) -> Result<(), LLMError> {
        Ok(())
    }

    fn effective_context_size(&self, _model: &str) -> usize {
        1_000
    }
}

#[async_trait]
impl LLMProvider for FailingProviderCompactionProvider {
    fn name(&self) -> &str {
        "failing-provider-stub"
    }

    async fn generate(&self, _request: LLMRequest) -> Result<LLMResponse, LLMError> {
        Ok(LLMResponse::new("stub-model", "summary"))
    }

    async fn compact_history(
        &self,
        _model: &str,
        _history: &[Message],
    ) -> Result<Vec<Message>, LLMError> {
        Err(LLMError::Provider {
            message: "provider compaction failed".to_string(),
            metadata: None,
        })
    }

    fn supports_responses_compaction(&self, _model: &str) -> bool {
        true
    }

    fn supported_models(&self) -> Vec<String> {
        vec!["stub-model".to_string()]
    }

    fn validate_request(&self, _request: &LLMRequest) -> Result<(), LLMError> {
        Ok(())
    }

    fn effective_context_size(&self, _model: &str) -> usize {
        1_000
    }
}

#[async_trait]
impl LLMProvider for RecordingProviderCompactionProvider {
    fn name(&self) -> &str {
        "recording-provider-stub"
    }

    async fn generate(&self, _request: LLMRequest) -> Result<LLMResponse, LLMError> {
        Ok(LLMResponse::new("stub-model", "summary"))
    }

    async fn compact_history(
        &self,
        _model: &str,
        history: &[Message],
    ) -> Result<Vec<Message>, LLMError> {
        *self.seen_history.write().await = history.to_vec();
        Ok(history.to_vec())
    }

    async fn compact_history_with_options(
        &self,
        _model: &str,
        history: &[Message],
        _options: &ResponsesCompactionOptions,
    ) -> Result<Vec<Message>, LLMError> {
        self.compact_history("stub-model", history).await
    }

    fn supports_responses_compaction(&self, _model: &str) -> bool {
        true
    }

    fn supported_models(&self) -> Vec<String> {
        vec!["stub-model".to_string()]
    }

    fn validate_request(&self, _request: &LLMRequest) -> Result<(), LLMError> {
        Ok(())
    }

    fn effective_context_size(&self, _model: &str) -> usize {
        1_000
    }
}

fn test_history() -> Vec<Message> {
    vec![
        Message::user("message-0".to_string()),
        Message::assistant("assistant-0".to_string()),
        Message::tool_response("call-0".to_string(), "tool-0".to_string()),
        Message::user("message-1".to_string()),
        Message::assistant("assistant-1".to_string()),
        Message::tool_response("call-1".to_string(), "tool-1".to_string()),
        Message::user("message-2".to_string()),
        Message::assistant("assistant-2".to_string()),
        Message::tool_response("call-2".to_string(), "tool-2".to_string()),
        Message::user("message-3".to_string()),
        Message::assistant("assistant-3".to_string()),
        Message::tool_response("call-3".to_string(), "tool-3".to_string()),
    ]
}

fn test_history_with_memory_envelope() -> Vec<Message> {
    let mut history = vec![Message::system(
        "[Session Memory Envelope]\nSummary:\nExisting summary".to_string(),
    )];
    history.extend(test_history());
    history
}

fn assert_local_compaction_history(history: &[Message], envelope_index: usize) {
    assert_local_compaction_history_with_user_count(history, envelope_index, 4);
}

fn assert_local_compaction_history_with_user_count(
    history: &[Message],
    envelope_index: usize,
    retained_user_messages: usize,
) {
    assert_eq!(history.len(), retained_user_messages + 2);
    assert!(
        history[envelope_index]
            .content
            .as_text()
            .contains("[Session Memory Envelope]")
    );
    assert_eq!(
        history.len(),
        history
            .iter()
            .filter(|message| {
                message.role == MessageRole::System || message.role == MessageRole::User
            })
            .count()
    );
    assert!(history.iter().any(|message| {
        message.role == MessageRole::System
            && message
                .content
                .as_text()
                .contains("Previous conversation summary")
    }));
    assert_eq!(
        history
            .iter()
            .filter(|message| message.role == MessageRole::User)
            .count(),
        retained_user_messages
    );
}

fn read_file_tool_call(id: &str, path: &str) -> ToolCall {
    ToolCall::function(
        id.to_string(),
        tool_names::READ_FILE.to_string(),
        json!({ "path": path }).to_string(),
    )
}

fn unified_file_read_tool_call(id: &str, path: &str) -> ToolCall {
    ToolCall::function(
        id.to_string(),
        tool_names::UNIFIED_FILE.to_string(),
        json!({ "action": "read", "path": path }).to_string(),
    )
}

fn assistant_with_tool_call(tool_call: ToolCall) -> Message {
    let mut message = Message::assistant(String::new());
    message.tool_calls = Some(vec![tool_call]);
    message
}

fn test_context_manager() -> ContextManager {
    ContextManager::new(
        "You are VT Code.".to_string(),
        (),
        Arc::new(RwLock::new(HashMap::new())),
        None,
    )
}

#[tokio::test]
async fn manual_compaction_succeeds_without_server_side_support() {
    let temp = tempdir().expect("tempdir");
    let provider = LocalCompactionProvider;
    let mut history = test_history();
    let mut session_stats = SessionStats::default();
    session_stats.set_previous_response_chain("stub", "stub-model", Some("resp_123"), &[]);
    let mut context_manager = test_context_manager();
    context_manager.update_token_usage(&Some(Usage {
        prompt_tokens: 900,
        completion_tokens: 10,
        total_tokens: 910,
        ..Usage::default()
    }));

    let outcome = compact_history_in_place(
        &provider,
        "stub-model",
        "session-alpha",
        temp.path(),
        Some(&VTCodeConfig::default()),
        &mut history,
        &mut session_stats,
        &mut context_manager,
    )
    .await
    .expect("manual compaction succeeds")
    .expect("history should compact");

    assert_eq!(outcome.original_len, 12);
    assert_eq!(outcome.compacted_len, 5);
    assert_local_compaction_history(&history, 0);
    assert_eq!(
        session_stats.previous_response_id_for("stub", "stub-model"),
        None
    );
    assert!(context_manager.current_token_usage() < 900);
    assert!(latest_memory_envelope_path_for_session(temp.path(), "session-alpha").is_some());
}

#[tokio::test]
async fn manual_compaction_emits_local_compaction_boundary_event() {
    let temp = tempdir().expect("tempdir");
    let provider = LocalCompactionProvider;
    let harness_path = temp.path().join("harness.jsonl");
    let harness_emitter = HarnessEventEmitter::new(harness_path.clone()).expect("emitter");
    let mut history = test_history();
    let mut session_stats = SessionStats::default();
    let mut context_manager = test_context_manager();

    let outcome = compact_history_in_place_with_events(
        CompactionContext::new(
            &provider,
            "stub-model",
            "session-alpha",
            "thread-alpha",
            temp.path(),
            Some(&VTCodeConfig::default()),
            None,
            Some(&harness_emitter),
        ),
        CompactionState::new(&mut history, &mut session_stats, &mut context_manager),
        vtcode_core::exec::events::CompactionTrigger::Manual,
    )
    .await
    .expect("compaction succeeds")
    .expect("history should compact");

    assert_eq!(
        outcome.mode,
        vtcode_core::exec::events::CompactionMode::Local
    );
    let content = fs::read_to_string(harness_path).expect("read harness log");
    assert!(content.contains("\"type\":\"thread.compact_boundary\""));
    assert!(content.contains("\"mode\":\"local\""));
}

#[tokio::test]
async fn provider_compaction_emits_provider_boundary_event() {
    let temp = tempdir().expect("tempdir");
    let provider = ProviderCompactionProvider;
    let harness_path = temp.path().join("provider-harness.jsonl");
    let harness_emitter = HarnessEventEmitter::new(harness_path.clone()).expect("emitter");
    let mut history = test_history();
    let mut session_stats = SessionStats::default();
    let mut context_manager = test_context_manager();

    let outcome = compact_history_in_place_with_events(
        CompactionContext::new(
            &provider,
            "stub-model",
            "session-alpha",
            "thread-alpha",
            temp.path(),
            Some(&VTCodeConfig::default()),
            None,
            Some(&harness_emitter),
        ),
        CompactionState::new(&mut history, &mut session_stats, &mut context_manager),
        vtcode_core::exec::events::CompactionTrigger::Manual,
    )
    .await
    .expect("compaction succeeds")
    .expect("history should compact");

    assert_eq!(
        outcome.mode,
        vtcode_core::exec::events::CompactionMode::Provider
    );
    let content = fs::read_to_string(harness_path).expect("read harness log");
    assert!(content.contains("\"type\":\"thread.compact_boundary\""));
    assert!(content.contains("\"mode\":\"provider\""));
}

#[tokio::test]
async fn manual_openai_compaction_clears_previous_response_chain() {
    let temp = tempdir().expect("tempdir");
    let provider = ProviderCompactionProvider;
    let mut history = test_history();
    let mut session_stats = SessionStats::default();
    session_stats.set_previous_response_chain("provider-stub", "stub-model", Some("resp_123"), &[]);
    let mut context_manager = test_context_manager();

    let outcome = manual_openai_compact_history_in_place(
        CompactionContext::new(
            &provider,
            "stub-model",
            "session-alpha",
            "thread-alpha",
            temp.path(),
            Some(&VTCodeConfig::default()),
            None,
            None,
        ),
        CompactionState::new(&mut history, &mut session_stats, &mut context_manager),
        &ResponsesCompactionOptions::default(),
    )
    .await
    .expect("manual OpenAI compaction succeeds")
    .expect("history should compact");

    assert_eq!(
        outcome.mode,
        vtcode_core::exec::events::CompactionMode::Provider
    );
    assert_eq!(
        session_stats.previous_response_id_for("provider-stub", "stub-model"),
        None
    );
}

#[tokio::test]
async fn manual_openai_compaction_rejects_unsupported_provider_without_local_fallback() {
    let temp = tempdir().expect("tempdir");
    let provider = LocalCompactionProvider;
    let mut history = test_history();
    let original_history = history.clone();
    let mut session_stats = SessionStats::default();
    let mut context_manager = test_context_manager();

    let err = manual_openai_compact_history_in_place(
        CompactionContext::new(
            &provider,
            "stub-model",
            "session-alpha",
            "thread-alpha",
            temp.path(),
            Some(&VTCodeConfig::default()),
            None,
            None,
        ),
        CompactionState::new(&mut history, &mut session_stats, &mut context_manager),
        &ResponsesCompactionOptions::default(),
    )
    .await
    .expect_err("unsupported provider should fail");

    assert!(err.to_string().contains(
        "Manual `/compact` is available only for the native OpenAI provider on api.openai.com"
    ));
    assert_eq!(history, original_history);
}

#[tokio::test]
async fn manual_openai_compaction_noop_preserves_existing_history() {
    let temp = tempdir().expect("tempdir");
    let provider = NoOpProviderCompactionProvider;
    let mut history = test_history_with_memory_envelope();
    let original_history = history.clone();
    let mut session_stats = SessionStats::default();
    let mut context_manager = test_context_manager();

    let outcome = manual_openai_compact_history_in_place(
        CompactionContext::new(
            &provider,
            "stub-model",
            "session-alpha",
            "thread-alpha",
            temp.path(),
            Some(&VTCodeConfig::default()),
            None,
            None,
        ),
        CompactionState::new(&mut history, &mut session_stats, &mut context_manager),
        &ResponsesCompactionOptions::default(),
    )
    .await
    .expect("noop compaction succeeds");

    assert!(outcome.is_none());
    assert_eq!(history, original_history);
}

#[tokio::test]
async fn provider_compaction_noop_preserves_existing_history() {
    let temp = tempdir().expect("tempdir");
    let provider = NoOpProviderCompactionProvider;
    let mut history = test_history_with_memory_envelope();
    let original_history = history.clone();
    let mut session_stats = SessionStats::default();
    let mut context_manager = test_context_manager();

    let outcome = compact_history_in_place_with_events(
        CompactionContext::new(
            &provider,
            "stub-model",
            "session-alpha",
            "thread-alpha",
            temp.path(),
            Some(&VTCodeConfig::default()),
            None,
            None,
        ),
        CompactionState::new(&mut history, &mut session_stats, &mut context_manager),
        vtcode_core::exec::events::CompactionTrigger::Manual,
    )
    .await
    .expect("noop compaction succeeds");

    assert!(outcome.is_none());
    assert_eq!(history, original_history);
}

#[tokio::test]
async fn provider_compaction_preserves_original_repeated_file_reads() {
    let temp = tempdir().expect("tempdir");
    let seen_history = Arc::new(RwLock::new(Vec::new()));
    let provider = RecordingProviderCompactionProvider {
        seen_history: Arc::clone(&seen_history),
    };
    let mut history = vec![
        assistant_with_tool_call(read_file_tool_call("call-1", "src/lib.rs")),
        Message::tool_response_with_origin(
            "call-1".to_string(),
            json!({
                "file_path": "src/lib.rs",
                "start_line": 1,
                "end_line": 40,
                "result": "older contents"
            })
            .to_string(),
            tool_names::READ_FILE.to_string(),
        ),
        assistant_with_tool_call(read_file_tool_call("call-2", "src/lib.rs")),
        Message::tool_response_with_origin(
            "call-2".to_string(),
            json!({
                "file_path": "src/lib.rs",
                "start_line": 1,
                "end_line": 40,
                "result": "newer contents"
            })
            .to_string(),
            tool_names::READ_FILE.to_string(),
        ),
    ];
    history.extend(test_history());
    let original_history = history.clone();
    let mut session_stats = SessionStats::default();
    let mut context_manager = test_context_manager();

    let outcome = compact_history_in_place_with_events(
        CompactionContext::new(
            &provider,
            "stub-model",
            "session-alpha",
            "thread-alpha",
            temp.path(),
            Some(&VTCodeConfig::default()),
            None,
            None,
        ),
        CompactionState::new(&mut history, &mut session_stats, &mut context_manager),
        vtcode_core::exec::events::CompactionTrigger::Manual,
    )
    .await
    .expect("provider compaction succeeds");

    assert!(outcome.is_none());
    assert_eq!(history, original_history);

    let seen = seen_history.read().await.clone();
    assert_eq!(seen.len(), original_history.len());
    assert!(seen[1].content.as_text().contains("older contents"));
    assert!(!seen[1].content.as_text().contains("deduped_read"));
}

#[test]
fn dedup_repeated_file_reads_rewrites_only_older_exact_matches() {
    let history = vec![
        assistant_with_tool_call(read_file_tool_call("call-1", "src/lib.rs")),
        Message::tool_response_with_origin(
            "call-1".to_string(),
            json!({
                "file_path": "src/lib.rs",
                "start_line": 1,
                "end_line": 40,
                "result": "older contents"
            })
            .to_string(),
            tool_names::READ_FILE.to_string(),
        ),
        assistant_with_tool_call(unified_file_read_tool_call("call-2", "src/lib.rs")),
        Message::tool_response(
            "call-2".to_string(),
            json!({
                "path": "src/lib.rs",
                "start_line": 1,
                "end_line": 40,
                "result": "newer contents"
            })
            .to_string(),
        ),
    ];

    let deduped = super::dedup_repeated_file_reads_for_local_compaction(&history);

    let older_payload: serde_json::Value =
        serde_json::from_str(deduped[1].content.as_text().as_ref()).expect("json payload");
    assert_eq!(
        older_payload
            .get("deduped_read")
            .and_then(serde_json::Value::as_bool),
        Some(true)
    );
    assert_eq!(
        older_payload
            .get("note")
            .and_then(serde_json::Value::as_str),
        Some(super::DEDUPED_FILE_READ_NOTE)
    );
    assert_eq!(
        older_payload
            .get("file_path")
            .and_then(serde_json::Value::as_str),
        Some("src/lib.rs")
    );
    assert!(deduped[3].content.as_text().contains("newer contents"));
    assert!(!deduped[3].content.as_text().contains("deduped_read"));
}

#[test]
fn dedup_repeated_file_reads_keeps_different_slices_and_chunked_reads() {
    let different_slice_history = vec![
        assistant_with_tool_call(read_file_tool_call("call-1", "src/lib.rs")),
        Message::tool_response_with_origin(
            "call-1".to_string(),
            json!({
                "file_path": "src/lib.rs",
                "start_line": 1,
                "end_line": 20,
                "result": "slice one"
            })
            .to_string(),
            tool_names::READ_FILE.to_string(),
        ),
        assistant_with_tool_call(read_file_tool_call("call-2", "src/lib.rs")),
        Message::tool_response_with_origin(
            "call-2".to_string(),
            json!({
                "file_path": "src/lib.rs",
                "start_line": 21,
                "end_line": 40,
                "result": "slice two"
            })
            .to_string(),
            tool_names::READ_FILE.to_string(),
        ),
    ];
    let chunked_history = vec![
        assistant_with_tool_call(read_file_tool_call("call-3", "src/lib.rs")),
        Message::tool_response_with_origin(
            "call-3".to_string(),
            json!({
                "file_path": "src/lib.rs",
                "start_line": 1,
                "end_line": 40,
                "result": "first chunk",
                "spool_chunked": true,
                "has_more": true
            })
            .to_string(),
            tool_names::READ_FILE.to_string(),
        ),
        assistant_with_tool_call(read_file_tool_call("call-4", "src/lib.rs")),
        Message::tool_response_with_origin(
            "call-4".to_string(),
            json!({
                "file_path": "src/lib.rs",
                "start_line": 1,
                "end_line": 40,
                "result": "second chunk",
                "spool_chunked": true,
                "has_more": false
            })
            .to_string(),
            tool_names::READ_FILE.to_string(),
        ),
    ];

    assert_eq!(
        super::dedup_repeated_file_reads_for_local_compaction(&different_slice_history),
        different_slice_history
    );
    assert_eq!(
        super::dedup_repeated_file_reads_for_local_compaction(&chunked_history),
        chunked_history
    );
}

#[test]
fn recovery_context_previews_include_latest_user_request_and_recent_distinct_tool_outputs() {
    let history = vec![
        Message::user("first request".to_string()),
        Message::tool_response("call-1".to_string(), "duplicate output".to_string()),
        Message::tool_response("call-2".to_string(), "distinct output".to_string()),
        Message::tool_response("call-3".to_string(), "duplicate output".to_string()),
        Message::user("latest request".to_string()),
    ];

    let previews = super::build_recovery_context_previews_with_workspace(&history, None);

    assert_eq!(previews[0], "Latest user request: latest request");
    assert_eq!(previews[1], "Tool output 1: duplicate output");
    assert_eq!(previews[2], "Tool output 2: distinct output");
    assert_eq!(previews.len(), 3);
}

#[test]
fn recovery_context_previews_fall_back_to_latest_assistant_text_when_needed() {
    let history = vec![Message::assistant("assistant summary".to_string())];

    let previews = super::build_recovery_context_previews_with_workspace(&history, None);

    assert_eq!(previews, vec!["Latest assistant text: assistant summary"]);
}

#[test]
fn recovery_context_previews_extract_structured_tool_guidance() {
    let history = vec![
        Message::user("use structured search".to_string()),
        Message::tool_response(
            "call-1".to_string(),
            json!({
                "backend": "ast-grep",
                "matches": [],
                "path": "src/agent",
                "is_recoverable": true,
                "hint": "Pattern looks like a code fragment.",
                "next_action": "Retry with a larger parseable pattern.",
                "fallback_tool": "unified_search",
                "fallback_tool_args": {"action": "structural", "path": "src/agent"}
            })
            .to_string(),
        ),
    ];

    let previews = super::build_recovery_context_previews_with_workspace(&history, None);

    assert_eq!(previews[0], "Latest user request: use structured search");
    assert!(previews[1].contains("No matches found in src/agent"));
    assert!(previews[1].contains("Pattern looks like a code fragment."));
    assert!(previews[1].contains("Next action: Retry with a larger parseable pattern."));
    assert!(previews[1].contains("Fallback tool: unified_search"));
}

#[test]
fn recovery_context_previews_extract_nested_error_guidance_and_spool_excerpt() {
    let temp = tempdir().expect("tempdir");
    let spool_dir = temp.path().join(".vtcode/context/tool_outputs");
    fs::create_dir_all(&spool_dir).expect("spool dir");
    let spool_path = spool_dir.join("read_1.txt");
    fs::write(
        &spool_path,
        (1..=40)
            .map(|idx| format!("spooled-line-{idx}"))
            .collect::<Vec<_>>()
            .join("\n"),
    )
    .expect("spool file");

    let history = vec![
        Message::user("review the read failure".to_string()),
        Message::tool_response(
            "call-1".to_string(),
            json!({
                "path": "src/main.rs",
                "spool_path": ".vtcode/context/tool_outputs/read_1.txt",
                "error": {
                    "message": "Read failed",
                    "hint": "Inspect the spooled content.",
                    "next_action": "Retry with a smaller slice."
                }
            })
            .to_string(),
        ),
    ];

    let previews =
        super::build_recovery_context_previews_with_workspace(&history, Some(temp.path()));

    assert_eq!(previews[0], "Latest user request: review the read failure");
    assert!(previews[1].contains("Read failed"));
    assert!(previews[1].contains("Inspect the spooled content."));
    assert!(previews[1].contains("Next action: Retry with a smaller slice."));
    assert!(previews[1].contains("source_path: src/main.rs"));
    assert!(previews[1].contains("Spool excerpt:"));
    assert!(previews[1].contains("spooled-line-1"));
}

#[test]
fn recovery_context_previews_prefer_substantive_reads_over_recent_low_signal_outputs() {
    let history = vec![
        Message::user("tell me more".to_string()),
        Message::tool_response(
            "call-1".to_string(),
            json!({
                "path": "README.md",
                "content": "VT Code is an open-source coding agent with LLM-native code understanding."
            })
            .to_string(),
        ),
        Message::tool_response(
            "call-2".to_string(),
            json!({
                "path": "docs/ARCHITECTURE.md",
                "content": "VT Code follows a modular architecture designed for maintainability and extensibility."
            })
            .to_string(),
        ),
        Message::tool_response(
            "call-3".to_string(),
            json!({
                "count": 20,
                "items": [{"path": "docs/ide"}]
            })
            .to_string(),
        ),
        Message::tool_response(
            "call-4".to_string(),
            json!({
                "error": "Repeated reads of 'docs/ARCHITECTURE.md' with limited progress detected.",
                "next_action": "Try an alternative tool or narrower scope."
            })
            .to_string(),
        ),
    ];

    let previews = super::build_recovery_context_previews_with_workspace(&history, None);

    assert_eq!(previews[0], "Latest user request: tell me more");
    assert!(previews[1].contains("VT Code follows a modular architecture"));
    assert!(previews[2].contains("VT Code is an open-source coding agent"));
    assert!(previews[3].contains("Repeated reads of 'docs/ARCHITECTURE.md'"));
    assert!(
        previews
            .iter()
            .all(|preview| !preview.contains("Listed 20 items")),
        "low-signal listing should be dropped when richer previews exist: {previews:?}"
    );
}

#[test]
fn legacy_memory_envelope_deserializes_with_new_fields_defaulted() {
    let envelope: SessionMemoryEnvelope = serde_json::from_value(json!({
        "session_id": "session-alpha",
        "summary": "Persisted summary",
        "task_summary": "Task tracker",
        "spec_summary": null,
        "evaluation_summary": null,
        "grounded_facts": [{
            "fact": "fact",
            "source": "tool:read_file"
        }],
        "touched_files": ["src/lib.rs"],
        "history_artifact_path": ".vtcode/history/session-alpha.jsonl",
        "generated_at": "2026-03-14T00:00:00Z"
    }))
    .expect("legacy envelope should deserialize");

    assert_eq!(envelope.schema_version, None);
    assert_eq!(envelope.objective, None);
    assert_eq!(envelope.verification_summary, None);
    assert!(envelope.constraints.is_empty());
    assert!(envelope.open_questions.is_empty());
    assert!(envelope.verification_todo.is_empty());
    assert!(envelope.delegation_notes.is_empty());
}

#[test]
fn refresh_session_memory_envelope_merges_existing_continuity_fields() {
    let temp = tempdir().expect("tempdir");
    let history_dir = temp.path().join(".vtcode").join("history");
    fs::create_dir_all(&history_dir).expect("history dir");
    fs::create_dir_all(temp.path().join(".vtcode").join("tasks")).expect("tasks dir");
    fs::write(
        temp.path()
            .join(".vtcode")
            .join("tasks")
            .join("current_task.md"),
        "# Ship compaction cleanup\n- [ ] Run cargo nextest\n- [x] Wire in config\n",
    )
    .expect("write task");
    fs::write(
        temp.path()
            .join(".vtcode")
            .join("tasks")
            .join("current_spec.md"),
        "# Spec\nKeep local compaction aligned with summarized forks.\n",
    )
    .expect("write spec");
    fs::write(
        temp.path()
            .join(".vtcode")
            .join("tasks")
            .join("current_evaluation.md"),
        "# Eval\nNeed a regression test for repeated reads.\n",
    )
    .expect("write eval");

    let prior_envelope = SessionMemoryEnvelope {
        session_id: "session-alpha".to_string(),
        schema_version: Some(SESSION_MEMORY_ENVELOPE_SCHEMA_VERSION),
        summary: "Prior summary".to_string(),
        objective: Some("Keep continuity".to_string()),
        task_summary: Some("Older task summary".to_string()),
        spec_summary: None,
        evaluation_summary: None,
        verification_summary: Some("- [x] Prior verification passed".to_string()),
        constraints: vec!["Do not redesign the harness".to_string()],
        grounded_facts: vec![GroundedFactRecord {
            fact: "Existing grounded fact".to_string(),
            source: "tool:read_file".to_string(),
        }],
        touched_files: vec!["src/old.rs".to_string()],
        open_questions: vec!["What should summarized forks retain?".to_string()],
        verification_todo: vec!["Confirm refresh runs at turn boundaries.".to_string()],
        delegation_notes: vec!["explorer: looked at compaction flow".to_string()],
        history_artifact_path: Some(".vtcode/history/session-alpha_0001.jsonl".to_string()),
        generated_at: "2026-03-14T00:00:00Z".to_string(),
    };
    fs::write(
        history_dir.join("session-alpha.memory.json"),
        serde_json::to_string_pretty(&prior_envelope).expect("serialize envelope"),
    )
    .expect("write envelope");

    let mut history = vec![
        Message::user("Continue the compaction work.".to_string()),
        Message::assistant("I will update the local compaction path.".to_string()),
    ];
    let mut session_stats = SessionStats::default();
    session_stats.record_touched_files(["src/new.rs".to_string()]);

    let update = super::SessionMemoryEnvelopeUpdate {
        grounded_facts: vec![GroundedFactRecord {
            fact: "Child agent confirmed the parser contract.".to_string(),
            source: "subagent:reviewer".to_string(),
        }],
        touched_files: vec!["src/child.rs".to_string()],
        open_questions: vec!["Should dedup cover batch reads?".to_string()],
        verification_todo: vec!["Run cargo check".to_string()],
        delegation_notes: vec!["reviewer: parser contract validated".to_string()],
        ..Default::default()
    };

    let envelope = super::refresh_session_memory_envelope(
        temp.path(),
        "session-alpha",
        Some(&VTCodeConfig::default()),
        &mut history,
        &session_stats,
        Some(&update),
    )
    .expect("refresh succeeds")
    .expect("envelope should be refreshed");

    assert_eq!(envelope.objective.as_deref(), Some("Keep continuity"));
    assert!(
        envelope
            .constraints
            .contains(&"Do not redesign the harness".to_string())
    );
    assert!(
        envelope
            .spec_summary
            .as_deref()
            .is_some_and(|summary| summary.contains("Keep local compaction aligned"))
    );
    assert!(
        envelope
            .evaluation_summary
            .as_deref()
            .is_some_and(|summary| summary.contains("Need a regression test"))
    );
    assert!(
        envelope
            .verification_summary
            .as_deref()
            .is_some_and(|summary| summary.contains("Run cargo nextest"))
    );
    assert!(
        envelope
            .open_questions
            .contains(&"Should dedup cover batch reads?".to_string())
    );
    assert!(
        envelope
            .verification_todo
            .iter()
            .any(|item| item.contains("Run cargo nextest"))
    );
    assert!(
        envelope
            .verification_todo
            .contains(&"Run cargo check".to_string())
    );
    assert!(
        envelope
            .delegation_notes
            .contains(&"reviewer: parser contract validated".to_string())
    );
    assert!(envelope.touched_files.contains(&"src/new.rs".to_string()));
    assert!(envelope.touched_files.contains(&"src/child.rs".to_string()));
    assert!(
        history[0]
            .content
            .as_text()
            .contains("[Session Memory Envelope]")
    );
    assert!(history[0].content.as_text().contains("Verification Status"));
}

#[test]
fn refresh_session_memory_envelope_prefers_structured_verify_metadata() {
    let temp = tempdir().expect("tempdir");
    let history_dir = temp.path().join(".vtcode").join("history");
    fs::create_dir_all(&history_dir).expect("history dir");
    fs::create_dir_all(temp.path().join(".vtcode").join("tasks")).expect("tasks dir");
    fs::write(
        temp.path()
            .join(".vtcode")
            .join("tasks")
            .join("current_task.md"),
        "# Ship compaction cleanup\n- [x] Analyze current continuity path\n  outcome: Existing envelope flow reviewed.\n- [ ] Update verification preservation\n  verify: cargo check -p vtcode\n- [ ] Run focused regression\n  verify:\n    - cargo test -p vtcode --bin vtcode agent::runloop::unified::turn::compaction::tests::refresh_session_memory_envelope_prefers_structured_verify_metadata -- --exact\n",
    )
    .expect("write task");

    let mut history = vec![Message::user("Continue the compaction work.".to_string())];
    let session_stats = SessionStats::default();

    let envelope = super::refresh_session_memory_envelope(
        temp.path(),
        "session-alpha",
        Some(&VTCodeConfig::default()),
        &mut history,
        &session_stats,
        None,
    )
    .expect("refresh succeeds")
    .expect("envelope should be refreshed");

    assert_eq!(
        envelope.verification_summary.as_deref(),
        Some(
            "- cargo check -p vtcode\n- cargo test -p vtcode --bin vtcode agent::runloop::unified::turn::compaction::tests::refresh_session_memory_envelope_prefers_structured_verify_metadata -- --exact"
        )
    );
    assert!(history[0].content.as_text().contains("Verification Status"));
    assert!(
        history[0]
            .content
            .as_text()
            .contains("- cargo check -p vtcode")
    );
    assert!(history[0]
        .content
        .as_text()
        .contains("- cargo test -p vtcode --bin vtcode agent::runloop::unified::turn::compaction::tests::refresh_session_memory_envelope_prefers_structured_verify_metadata -- --exact"));
}

#[tokio::test]
async fn provider_compaction_error_preserves_existing_history() {
    let temp = tempdir().expect("tempdir");
    let provider = FailingProviderCompactionProvider;
    let mut history = test_history_with_memory_envelope();
    let original_history = history.clone();
    let mut session_stats = SessionStats::default();
    let mut context_manager = test_context_manager();

    let err = compact_history_in_place_with_events(
        CompactionContext::new(
            &provider,
            "stub-model",
            "session-alpha",
            "thread-alpha",
            temp.path(),
            Some(&VTCodeConfig::default()),
            None,
            None,
        ),
        CompactionState::new(&mut history, &mut session_stats, &mut context_manager),
        vtcode_core::exec::events::CompactionTrigger::Manual,
    )
    .await
    .expect_err("failing provider should fail");

    assert!(!err.to_string().is_empty());
    assert_eq!(history, original_history);
}

#[tokio::test]
async fn auto_compaction_replaces_history_and_clears_response_chain() {
    let temp = tempdir().expect("tempdir");
    let provider = LocalCompactionProvider;
    let mut vt_cfg = VTCodeConfig::default();
    vt_cfg.agent.harness.auto_compaction_enabled = true;
    vt_cfg.agent.harness.auto_compaction_threshold_tokens = Some(700);

    let mut history = test_history();
    let mut session_stats = SessionStats::default();
    session_stats.set_previous_response_chain("stub", "stub-model", Some("resp_123"), &[]);
    let mut context_manager = test_context_manager();
    context_manager.update_token_usage(&Some(Usage {
        prompt_tokens: 900,
        completion_tokens: 10,
        total_tokens: 910,
        ..Usage::default()
    }));

    let outcome = maybe_auto_compact_history(
        CompactionContext::new(
            &provider,
            "stub-model",
            "session-alpha",
            "thread-alpha",
            temp.path(),
            Some(&vt_cfg),
            None,
            None,
        ),
        CompactionState::new(&mut history, &mut session_stats, &mut context_manager),
    )
    .await
    .expect("auto compaction succeeds")
    .expect("history should compact");

    assert_eq!(outcome.original_len, 12);
    assert_eq!(outcome.compacted_len, 5);
    assert_local_compaction_history(&history, 4);
    assert!(
        history[0]
            .content
            .as_text()
            .contains("Previous conversation summary")
    );
    assert_eq!(history[5].role, MessageRole::User);
    assert_eq!(
        session_stats.previous_response_id_for("stub", "stub-model"),
        None
    );
    assert!(context_manager.current_token_usage() < 700);
    assert!(latest_memory_envelope_path_for_session(temp.path(), "session-alpha").is_some());
}

#[tokio::test]
async fn targeted_compaction_preserves_prefix_and_replaces_suffix() {
    let temp = tempdir().expect("tempdir");
    let provider = LocalCompactionProvider;
    let mut history = test_history();
    let preserved_prefix = history[..1].to_vec();
    let mut session_stats = SessionStats::default();
    let mut context_manager = test_context_manager();
    context_manager.update_token_usage(&Some(Usage {
        prompt_tokens: 900,
        completion_tokens: 10,
        total_tokens: 910,
        ..Usage::default()
    }));

    let outcome = compact_history_from_index_in_place(
        &provider,
        "stub-model",
        "session-alpha",
        temp.path(),
        Some(&VTCodeConfig::default()),
        &mut history,
        1,
        &mut session_stats,
        &mut context_manager,
    )
    .await
    .expect("targeted compaction succeeds")
    .expect("history should compact");

    assert_eq!(&history[..1], preserved_prefix.as_slice());
    assert_eq!(outcome.original_len, 12);
    assert_eq!(outcome.compacted_len, 5);
    assert_eq!(history.len(), 6);
    assert!(
        history[1]
            .content
            .as_text()
            .contains("[Session Memory Envelope]")
    );
    assert!(
        history[2]
            .content
            .as_text()
            .contains("Previous conversation summary")
    );
    assert!(latest_memory_envelope_path_for_session(temp.path(), "session-alpha").is_none());
}

#[tokio::test]
async fn recovery_compaction_preserves_current_turn_suffix_and_emits_event() {
    let temp = tempdir().expect("tempdir");
    let provider = LocalCompactionProvider;
    let harness_path = temp.path().join("recovery-harness.jsonl");
    let harness_emitter = HarnessEventEmitter::new(harness_path.clone()).expect("emitter");
    let mut history = test_history();
    history.push(Message::system("Previous turn already completed tool execution. Reuse the latest tool outputs in history instead of rerunning the same exploration. If those tool outputs include `critical_note`, `hint`, `next_action`, `fallback_tool`, `fallback_tool_args`, or `rerun_hint`, follow that guidance first.".to_string()));
    history.push(Message::system("Model follow-up failed after tool activity. Tools are disabled on the next pass; provide a direct textual response from the current context and reuse the latest tool outputs already in history.".to_string()));
    history.push(Message::user("current-turn".to_string()));
    history.push(Message::assistant("".to_string()));
    history.push(Message::tool_response(
        "call-current".to_string(),
        "{\"ok\":true}".to_string(),
    ));
    let preserved_suffix = history[12..].to_vec();
    let mut session_stats = SessionStats::default();
    let mut context_manager = test_context_manager();

    let outcome = compact_history_for_recovery_in_place(
        CompactionContext::new(
            &provider,
            "stub-model",
            "session-alpha",
            "thread-alpha",
            temp.path(),
            Some(&VTCodeConfig::default()),
            None,
            Some(&harness_emitter),
        ),
        CompactionState::new(&mut history, &mut session_stats, &mut context_manager),
        12,
    )
    .await
    .expect("recovery compaction succeeds")
    .expect("history should compact");

    assert_eq!(
        history[history.len() - preserved_suffix.len()..],
        preserved_suffix
    );
    assert!(outcome.compacted_len < outcome.original_len);

    let content = fs::read_to_string(harness_path).expect("read harness log");
    assert!(content.contains("\"type\":\"thread.compact_boundary\""));
    assert!(content.contains("\"trigger\":\"recovery\""));
    assert!(content.contains("\"mode\":\"local\""));
}

#[tokio::test]
async fn recovery_compaction_uses_provider_mode_when_supported() {
    let temp = tempdir().expect("tempdir");
    let provider = ProviderCompactionProvider;
    let harness_path = temp.path().join("provider-recovery-harness.jsonl");
    let harness_emitter = HarnessEventEmitter::new(harness_path.clone()).expect("emitter");
    let mut history = test_history();
    history.push(Message::user("current-turn".to_string()));
    history.push(Message::assistant("".to_string()));
    history.push(Message::tool_response(
        "call-current".to_string(),
        "{\"ok\":true}".to_string(),
    ));
    let preserved_suffix = history[12..].to_vec();
    let mut session_stats = SessionStats::default();
    let mut context_manager = test_context_manager();

    let outcome = compact_history_for_recovery_in_place(
        CompactionContext::new(
            &provider,
            "stub-model",
            "session-alpha",
            "thread-alpha",
            temp.path(),
            Some(&VTCodeConfig::default()),
            None,
            Some(&harness_emitter),
        ),
        CompactionState::new(&mut history, &mut session_stats, &mut context_manager),
        12,
    )
    .await
    .expect("provider recovery compaction succeeds")
    .expect("history should compact");

    assert_eq!(
        outcome.mode,
        vtcode_core::exec::events::CompactionMode::Provider
    );
    assert_eq!(
        history[history.len() - preserved_suffix.len()..],
        preserved_suffix
    );

    let content = fs::read_to_string(harness_path).expect("read harness log");
    assert!(content.contains("\"trigger\":\"recovery\""));
    assert!(content.contains("\"mode\":\"provider\""));
}

#[test]
fn inject_latest_memory_envelope_rehydrates_resume_history() {
    let temp = tempdir().expect("tempdir");
    let history_dir = temp.path().join(".vtcode").join("history");
    fs::create_dir_all(&history_dir).expect("history dir");
    let envelope_path = history_dir.join("resume-session_001.memory.json");
    let envelope = SessionMemoryEnvelope {
        session_id: "resume-session".to_string(),
        schema_version: Some(SESSION_MEMORY_ENVELOPE_SCHEMA_VERSION),
        summary: "Persisted summary".to_string(),
        objective: None,
        task_summary: Some("Tracker: - [ ] Follow up".to_string()),
        spec_summary: None,
        evaluation_summary: None,
        verification_summary: Some("- [ ] Run cargo nextest".to_string()),
        constraints: Vec::new(),
        grounded_facts: vec![GroundedFactRecord {
            fact: "Cargo.toml declares vtcode-core".to_string(),
            source: "tool:read_file".to_string(),
        }],
        touched_files: vec!["Cargo.toml".to_string()],
        open_questions: Vec::new(),
        verification_todo: Vec::new(),
        delegation_notes: Vec::new(),
        history_artifact_path: Some(".vtcode/history/resume-session_001.jsonl".to_string()),
        generated_at: "2026-03-14T00:00:00Z".to_string(),
    };
    fs::write(
        &envelope_path,
        serde_json::to_string_pretty(&envelope).expect("serialize envelope"),
    )
    .expect("write envelope");

    let mut history = vec![Message::user("resume".to_string())];
    assert!(inject_latest_memory_envelope(
        temp.path(),
        "resume-session",
        &mut history
    ));
    assert!(history[0].content.as_text().contains("Persisted summary"));
    assert!(history[0].content.as_text().contains("Cargo.toml"));
    assert!(history[0].content.as_text().contains("Verification Status"));
}

#[test]
fn inject_latest_memory_envelope_is_session_scoped() {
    let temp = tempdir().expect("tempdir");
    let history_dir = temp.path().join(".vtcode").join("history");
    fs::create_dir_all(&history_dir).expect("history dir");

    for (session_id, summary) in [
        ("session-alpha", "Alpha summary"),
        ("session-beta", "Beta summary"),
    ] {
        let envelope_path = history_dir.join(format!("{session_id}_0001.memory.json"));
        let envelope = SessionMemoryEnvelope {
            session_id: session_id.to_string(),
            schema_version: Some(SESSION_MEMORY_ENVELOPE_SCHEMA_VERSION),
            summary: summary.to_string(),
            objective: None,
            task_summary: None,
            spec_summary: None,
            evaluation_summary: None,
            verification_summary: None,
            constraints: Vec::new(),
            grounded_facts: Vec::new(),
            touched_files: Vec::new(),
            open_questions: Vec::new(),
            verification_todo: Vec::new(),
            delegation_notes: Vec::new(),
            history_artifact_path: None,
            generated_at: "2026-03-14T00:00:00Z".to_string(),
        };
        fs::write(
            envelope_path,
            serde_json::to_string_pretty(&envelope).expect("serialize envelope"),
        )
        .expect("write envelope");
    }

    let mut history = vec![Message::user("resume".to_string())];
    assert!(inject_latest_memory_envelope(
        temp.path(),
        "session-beta",
        &mut history
    ));
    assert!(history[0].content.as_text().contains("Beta summary"));
    assert!(!history[0].content.as_text().contains("Alpha summary"));
}

#[test]
fn inject_latest_memory_envelope_requires_exact_session_prefix_match() {
    let temp = tempdir().expect("tempdir");
    let history_dir = temp.path().join(".vtcode").join("history");
    fs::create_dir_all(&history_dir).expect("history dir");

    for (file_name, summary) in [
        ("session-a_0001.memory.json", "Exact summary"),
        ("session-alpha_0002.memory.json", "Wrong summary"),
    ] {
        let envelope = SessionMemoryEnvelope {
            session_id: "session-a".to_string(),
            schema_version: Some(SESSION_MEMORY_ENVELOPE_SCHEMA_VERSION),
            summary: summary.to_string(),
            objective: None,
            task_summary: None,
            spec_summary: None,
            evaluation_summary: None,
            verification_summary: None,
            constraints: Vec::new(),
            grounded_facts: Vec::new(),
            touched_files: Vec::new(),
            open_questions: Vec::new(),
            verification_todo: Vec::new(),
            delegation_notes: Vec::new(),
            history_artifact_path: None,
            generated_at: "2026-03-14T00:00:00Z".to_string(),
        };
        fs::write(
            history_dir.join(file_name),
            serde_json::to_string_pretty(&envelope).expect("serialize envelope"),
        )
        .expect("write envelope");
    }

    let mut history = vec![Message::user("resume".to_string())];
    assert!(inject_latest_memory_envelope(
        temp.path(),
        "session-a",
        &mut history
    ));
    assert!(history[0].content.as_text().contains("Exact summary"));
    assert!(!history[0].content.as_text().contains("Wrong summary"));
}

#[tokio::test]
async fn no_envelope_written_when_dynamic_history_is_disabled() {
    let temp = tempdir().expect("tempdir");
    let provider = LocalCompactionProvider;
    let mut vt_cfg = VTCodeConfig::default();
    vt_cfg.context.dynamic.enabled = false;

    let mut history = test_history();
    let mut session_stats = SessionStats::default();
    let mut context_manager = test_context_manager();

    compact_history_in_place(
        &provider,
        "stub-model",
        "session-alpha",
        temp.path(),
        Some(&vt_cfg),
        &mut history,
        &mut session_stats,
        &mut context_manager,
    )
    .await
    .expect("compaction succeeds");

    assert!(latest_memory_envelope_path_for_session(temp.path(), "session-alpha").is_none());
    assert!(
        history[0]
            .content
            .as_text()
            .contains("Previous conversation summary")
    );
}

#[tokio::test]
async fn persisted_envelope_uses_recorded_touched_files_only() {
    let temp = tempdir().expect("tempdir");
    let provider = LocalCompactionProvider;
    let mut history = test_history();
    history.push(Message::user(
        "Mentioning docs/example.md in prose should not populate touched files.".to_string(),
    ));
    let mut session_stats = SessionStats::default();
    session_stats.record_touched_files(["src/main.rs".to_string(), "Cargo.toml".to_string()]);
    let mut context_manager = test_context_manager();

    compact_history_in_place(
        &provider,
        "stub-model",
        "session-alpha",
        temp.path(),
        Some(&VTCodeConfig::default()),
        &mut history,
        &mut session_stats,
        &mut context_manager,
    )
    .await
    .expect("compaction succeeds");

    let envelope_path = latest_memory_envelope_path_for_session(temp.path(), "session-alpha")
        .expect("envelope path");
    let envelope: SessionMemoryEnvelope =
        serde_json::from_str(&fs::read_to_string(envelope_path).expect("read envelope"))
            .expect("parse envelope");

    assert_eq!(
        envelope.touched_files,
        vec!["src/main.rs".to_string(), "Cargo.toml".to_string()]
    );
    assert_eq!(envelope.session_id, "session-alpha");
}

#[test]
fn inject_latest_memory_envelope_uses_exact_session_id_when_prefixes_collide() {
    let temp = tempdir().expect("tempdir");
    let history_dir = temp.path().join(".vtcode").join("history");
    fs::create_dir_all(&history_dir).expect("history dir");

    let session_alpha = "01234567890123456789012345678901-alpha";
    let session_beta = "01234567890123456789012345678901-beta";

    for (session_id, summary, suffix) in [
        (session_alpha, "Alpha summary", "0001"),
        (session_beta, "Beta summary", "0002"),
    ] {
        let envelope = SessionMemoryEnvelope {
            session_id: session_id.to_string(),
            schema_version: Some(SESSION_MEMORY_ENVELOPE_SCHEMA_VERSION),
            summary: summary.to_string(),
            objective: None,
            task_summary: None,
            spec_summary: None,
            evaluation_summary: None,
            verification_summary: None,
            constraints: Vec::new(),
            grounded_facts: Vec::new(),
            touched_files: Vec::new(),
            open_questions: Vec::new(),
            verification_todo: Vec::new(),
            delegation_notes: Vec::new(),
            history_artifact_path: None,
            generated_at: "2026-03-14T00:00:00Z".to_string(),
        };
        let file_name = format!("{}_{suffix}.memory.json", &session_id[..32]);
        fs::write(
            history_dir.join(file_name),
            serde_json::to_string_pretty(&envelope).expect("serialize envelope"),
        )
        .expect("write envelope");
    }

    let mut history = vec![Message::user("resume".to_string())];
    assert!(inject_latest_memory_envelope(
        temp.path(),
        session_alpha,
        &mut history
    ));
    assert!(history[0].content.as_text().contains("Alpha summary"));
    assert!(!history[0].content.as_text().contains("Beta summary"));
}

#[tokio::test]
async fn compaction_strips_existing_memory_envelope_before_recompacting() {
    let temp = tempdir().expect("tempdir");
    let provider = LocalCompactionProvider;
    let mut history = test_history();
    history.insert(
        0,
        Message::system("[Session Memory Envelope]\nSummary:\nPersisted summary".to_string()),
    );
    let mut session_stats = SessionStats::default();
    let mut context_manager = test_context_manager();

    let outcome = compact_history_in_place(
        &provider,
        "stub-model",
        "session-alpha",
        temp.path(),
        Some(&VTCodeConfig::default()),
        &mut history,
        &mut session_stats,
        &mut context_manager,
    )
    .await
    .expect("compaction succeeds")
    .expect("history should compact");

    assert_eq!(outcome.original_len, 12);
    assert_eq!(outcome.compacted_len, 5);
    assert_eq!(
        history
            .iter()
            .filter(|message| message
                .content
                .as_text()
                .contains("[Session Memory Envelope]"))
            .count(),
        1
    );
}

#[tokio::test]
async fn summarized_fork_history_reuses_compaction_pipeline_and_prior_envelope() {
    let temp = tempdir().expect("tempdir");
    let history_dir = temp.path().join(".vtcode").join("history");
    fs::create_dir_all(&history_dir).expect("history dir");
    let source_envelope = SessionMemoryEnvelope {
        session_id: "session-source".to_string(),
        schema_version: Some(SESSION_MEMORY_ENVELOPE_SCHEMA_VERSION),
        summary: "Prior source summary".to_string(),
        objective: Some("Keep the source session moving".to_string()),
        task_summary: Some("Tracker: keep going".to_string()),
        spec_summary: None,
        evaluation_summary: None,
        verification_summary: None,
        constraints: Vec::new(),
        grounded_facts: vec![GroundedFactRecord {
            fact: "src/lib.rs was updated".to_string(),
            source: "tool:write_file".to_string(),
        }],
        touched_files: vec!["src/lib.rs".to_string()],
        open_questions: Vec::new(),
        verification_todo: Vec::new(),
        delegation_notes: Vec::new(),
        history_artifact_path: Some(".vtcode/history/session-source_0001.jsonl".to_string()),
        generated_at: "2026-03-14T00:00:00Z".to_string(),
    };
    fs::write(
        history_dir.join("session-source_0001.memory.json"),
        serde_json::to_string_pretty(&source_envelope).expect("serialize envelope"),
    )
    .expect("write envelope");

    let compacted = build_summarized_fork_history(
        &LocalCompactionProvider,
        "stub-model",
        "session-source",
        "session-target",
        temp.path(),
        Some(&VTCodeConfig::default()),
        &test_history(),
        false,
    )
    .await
    .expect("summarized fork history");

    assert_eq!(compacted.len(), 6);
    assert!(
        compacted[0]
            .content
            .as_text()
            .contains("[Session Memory Envelope]")
    );
    assert!(compacted[0].content.as_text().contains("src/lib.rs"));
    assert!(
        compacted[1]
            .content
            .as_text()
            .contains("Previous conversation summary")
    );
    assert_eq!(
        compacted
            .iter()
            .filter(|message| message.role == MessageRole::User)
            .count(),
        4
    );
    assert!(compacted.iter().all(
        |message| message.role == MessageRole::System || message.role == MessageRole::User
    ));
}

#[tokio::test]
async fn budget_resume_summary_reuses_saved_envelope_without_provider_compaction() {
    let temp = tempdir().expect("tempdir");
    let history_dir = temp.path().join(".vtcode").join("history");
    fs::create_dir_all(&history_dir).expect("create history dir");

    let source_envelope = SessionMemoryEnvelope {
        session_id: "session-source".to_string(),
        schema_version: Some(SESSION_MEMORY_ENVELOPE_SCHEMA_VERSION),
        summary: "Budget-limited session summary".to_string(),
        objective: None,
        task_summary: None,
        spec_summary: None,
        evaluation_summary: None,
        verification_summary: None,
        constraints: Vec::new(),
        grounded_facts: Vec::new(),
        touched_files: vec!["src/lib.rs".to_string()],
        open_questions: Vec::new(),
        verification_todo: Vec::new(),
        delegation_notes: Vec::new(),
        history_artifact_path: None,
        generated_at: "2026-03-14T00:00:00Z".to_string(),
    };
    fs::write(
        history_dir.join("session-source_0001.memory.json"),
        serde_json::to_string_pretty(&source_envelope).expect("serialize envelope"),
    )
    .expect("write envelope");

    let compacted = build_summarized_fork_history(
        &FailingProviderCompactionProvider,
        "stub-model",
        "session-source",
        "session-target",
        temp.path(),
        Some(&VTCodeConfig::default()),
        &test_history(),
        true,
    )
    .await
    .expect("saved summary fork history");

    assert!(
        compacted[0]
            .content
            .as_text()
            .contains("[Session Memory Envelope]")
    );
    assert!(
        compacted[1]
            .content
            .as_text()
            .contains("Budget-limited session summary")
    );
}

#[tokio::test]
async fn local_and_fork_compaction_share_retained_user_budget() {
    let temp = tempdir().expect("tempdir");
    let provider = LocalCompactionProvider;
    let mut vt_cfg = VTCodeConfig::default();
    vt_cfg.context.dynamic.retained_user_messages = 2;

    let mut history = test_history();
    let mut session_stats = SessionStats::default();
    let mut context_manager = test_context_manager();

    compact_history_in_place(
        &provider,
        "stub-model",
        "session-alpha",
        temp.path(),
        Some(&vt_cfg),
        &mut history,
        &mut session_stats,
        &mut context_manager,
    )
    .await
    .expect("compaction succeeds")
    .expect("history should compact");

    assert_local_compaction_history_with_user_count(&history, 0, 2);

    let compacted = build_summarized_fork_history(
        &provider,
        "stub-model",
        "session-alpha",
        "session-beta",
        temp.path(),
        Some(&vt_cfg),
        &test_history(),
        false,
    )
    .await
    .expect("summarized fork history");

    assert_eq!(
        compacted
            .iter()
            .filter(|message| message.role == MessageRole::User)
            .count(),
        2
    );
}

#[test]
fn grounded_fact_extraction_dedupes_caps_and_skips_errors() {
    let history = vec![
        Message::tool_response_with_origin(
            "call_1".to_string(),
            "{\"result\":\"Cargo.toml declares vtcode-core\"}".to_string(),
            "read_file".to_string(),
        ),
        Message::tool_response_with_origin(
            "call_2".to_string(),
            "{\"result\":\"cargo.toml declares vtcode-core\"}".to_string(),
            "read_file".to_string(),
        ),
        Message::tool_response_with_origin(
            "call_3".to_string(),
            "{\"error\":\"denied\"}".to_string(),
            "read_file".to_string(),
        ),
        Message::user("I prefer concise answers.".to_string()),
    ];

    let facts = super::dedup_latest_facts(&history, 5);
    assert_eq!(facts.len(), 2);
    assert!(facts.iter().any(|fact| fact.source == "tool:read_file"));
    assert!(facts.iter().any(|fact| fact.source == "user_assertion"));
}

#[test]
fn resolve_compaction_threshold_prefers_configured_value() {
    assert_eq!(resolve_compaction_threshold(Some(42), 200_000), Some(42));
}

#[test]
fn resolve_compaction_threshold_uses_context_ratio_when_unset() {
    assert_eq!(resolve_compaction_threshold(None, 200_000), Some(180_000));
}

#[test]
fn resolve_compaction_threshold_clamps_to_context_size() {
    assert_eq!(
        resolve_compaction_threshold(Some(300_000), 200_000),
        Some(200_000)
    );
}

#[test]
fn resolve_compaction_threshold_requires_context_or_override() {
    assert_eq!(resolve_compaction_threshold(None, 0), None);
}

#[test]
fn build_server_compaction_context_management_creates_openai_payload() {
    assert_eq!(
        build_server_compaction_context_management(Some(512), 2_000),
        Some(json!([{
            "type": "compaction",
            "compact_threshold": 512,
        }]))
    );
}
