use crate::agent::runloop::git::normalize_workspace_path;
use crate::agent::runloop::mcp_events::McpPanelState;
use crate::agent::runloop::unified::state::SessionStats;
use anyhow::Result;
use std::collections::BTreeSet;
use std::path::Path;
use std::path::PathBuf;
use vtcode_core::config::constants::tools;
use vtcode_core::config::loader::VTCodeConfig;
use vtcode_core::tools::tool_intent;
use vtcode_core::utils::ansi::AnsiRenderer;
use vtcode_core::utils::ansi::MessageStyle;
use vtcode_core::utils::transcript;
use vtcode_tui::app::{InlineHandle, InlineMessageKind, InlineSegment, InlineTextStyle};

use crate::agent::runloop::unified::run_loop_context::RunLoopContext;
use crate::agent::runloop::unified::tool_pipeline::{ToolExecutionStatus, ToolPipelineOutcome};

fn record_mcp_success_event(
    mcp_panel_state: &mut McpPanelState,
    tool_name: &str,
    args_val: &serde_json::Value,
) {
    let mut mcp_event = crate::agent::runloop::mcp_events::McpEvent::new(
        "mcp".to_string(),
        tool_name.to_string(),
        Some(args_val.to_string()),
    );
    mcp_event.success(None);
    mcp_panel_state.add_event(mcp_event);
}

fn collect_modified_files(modified_files: &[String]) -> Vec<PathBuf> {
    modified_files.iter().map(PathBuf::from).collect()
}

fn collect_instruction_activity_paths(
    workspace_root: &Path,
    args_val: &serde_json::Value,
    output: &serde_json::Value,
    modified_files: &[String],
) -> Vec<PathBuf> {
    let mut paths = BTreeSet::new();
    for modified in modified_files {
        push_activity_path(workspace_root, modified, &mut paths);
    }
    collect_paths_from_value(workspace_root, Some("args"), args_val, &mut paths);
    collect_paths_from_value(workspace_root, Some("output"), output, &mut paths);
    paths.into_iter().collect()
}

fn collect_paths_from_value(
    workspace_root: &Path,
    key: Option<&str>,
    value: &serde_json::Value,
    paths: &mut BTreeSet<PathBuf>,
) {
    match value {
        serde_json::Value::String(text) => {
            if key.is_some_and(path_like_key) {
                push_activity_path(workspace_root, text, paths);
            }
        }
        serde_json::Value::Array(values) => {
            for value in values {
                collect_paths_from_value(workspace_root, key, value, paths);
            }
        }
        serde_json::Value::Object(map) => {
            for (child_key, child_value) in map {
                collect_paths_from_value(
                    workspace_root,
                    Some(child_key.as_str()),
                    child_value,
                    paths,
                );
            }
        }
        serde_json::Value::Null | serde_json::Value::Bool(_) | serde_json::Value::Number(_) => {}
    }
}

fn path_like_key(key: &str) -> bool {
    matches!(
        key,
        "path"
            | "paths"
            | "file"
            | "files"
            | "file_path"
            | "file_paths"
            | "cwd"
            | "workdir"
            | "directory"
            | "directories"
            | "root"
            | "workspace"
    )
}

fn push_activity_path(workspace_root: &Path, raw: &str, paths: &mut BTreeSet<PathBuf>) {
    let trimmed = raw.trim();
    if trimmed.is_empty() || trimmed.contains("://") || trimmed.starts_with("untitled:") {
        return;
    }

    let normalized = normalize_workspace_path(workspace_root, Path::new(trimmed));
    let canonical_workspace =
        std::fs::canonicalize(workspace_root).unwrap_or_else(|_| workspace_root.to_path_buf());
    if normalized.starts_with(&canonical_workspace) || normalized.starts_with(workspace_root) {
        paths.insert(normalized);
    }
}

fn is_run_pty_tool(name: &str, args_val: &serde_json::Value) -> bool {
    tool_intent::is_command_run_tool_call(name, args_val)
}

fn compact_run_completion_line(
    output: &serde_json::Value,
    command_success: bool,
) -> Option<String> {
    if let Some(exit_code) = output.get("exit_code").and_then(serde_json::Value::as_i64) {
        if exit_code == 0 {
            return Some("✓ run completed (exit code: 0)".to_string());
        }
        return Some(format!("✗ run error, exit code: {}", exit_code));
    }

    if output.get("is_exited").and_then(serde_json::Value::as_bool) == Some(true) {
        if command_success {
            return Some("✓ done".to_string());
        }
        return Some("✗ failed".to_string());
    }

    None
}

fn is_git_diff_payload(output: &serde_json::Value) -> bool {
    output
        .get("content_type")
        .and_then(serde_json::Value::as_str)
        .is_some_and(|content_type| content_type == "git_diff")
}

fn has_renderable_stream_content(output: &serde_json::Value) -> bool {
    ["output", "stdout", "stderr"].iter().any(|key| {
        output
            .get(*key)
            .and_then(serde_json::Value::as_str)
            .is_some_and(|s| !s.trim().is_empty())
    })
}

fn is_task_tracker_tool(name: &str) -> bool {
    matches!(name, tools::TASK_TRACKER | tools::PLAN_TASK_TRACKER)
}

fn task_tracker_item_preview(item: &serde_json::Value) -> Option<String> {
    item.as_str().map(str::to_string).or_else(|| {
        item.get("description")
            .and_then(serde_json::Value::as_str)
            .map(str::to_string)
    })
}

fn task_tracker_call_lines(args_val: &serde_json::Value) -> Vec<String> {
    let mut lines = vec!["• Task tracker".to_string()];

    if let Some(action) = args_val.get("action").and_then(serde_json::Value::as_str) {
        lines.push(format!("  └ Action: {action}"));
    }
    if let Some(title) = args_val.get("title").and_then(serde_json::Value::as_str) {
        lines.push(format!("  └ Title: {title}"));
    }
    if let Some(index) = args_val.get("index").and_then(serde_json::Value::as_u64) {
        lines.push(format!("  └ Index: {index}"));
    } else if let Some(index_path) = args_val
        .get("index_path")
        .and_then(serde_json::Value::as_str)
    {
        lines.push(format!("  └ Index: {index_path}"));
    }
    if let Some(status) = args_val.get("status").and_then(serde_json::Value::as_str) {
        lines.push(format!("  └ Status: {status}"));
    }
    if let Some(files) = args_val.get("files").and_then(serde_json::Value::as_array) {
        let display = files
            .iter()
            .filter_map(serde_json::Value::as_str)
            .collect::<Vec<_>>();
        if !display.is_empty() {
            lines.push(format!("  └ Files: {}", display.join(", ")));
        }
    }
    if let Some(outcome) = args_val.get("outcome").and_then(serde_json::Value::as_str) {
        lines.push(format!("  └ Outcome: {outcome}"));
    }
    if let Some(verify) = args_val.get("verify") {
        let commands = match verify {
            serde_json::Value::String(command) => vec![command.as_str()],
            serde_json::Value::Array(values) => values
                .iter()
                .filter_map(serde_json::Value::as_str)
                .collect::<Vec<_>>(),
            _ => Vec::new(),
        };
        if !commands.is_empty() {
            lines.push(format!("  └ Verify: {}", commands.join(" | ")));
        }
    }
    if let Some(items) = args_val.get("items").and_then(serde_json::Value::as_array)
        && !items.is_empty()
    {
        let preview = items
            .iter()
            .filter_map(task_tracker_item_preview)
            .take(2)
            .collect::<Vec<_>>();
        let suffix = match items.len().saturating_sub(preview.len()) {
            0 => String::new(),
            remaining => format!(" +{remaining} more"),
        };
        if !preview.is_empty() {
            lines.push(format!("  └ Items: {}{}", preview.join(", "), suffix));
        }
    }

    lines
}

fn task_tracker_block_lines(
    args_val: &serde_json::Value,
    output: &serde_json::Value,
) -> Vec<String> {
    let mut lines = task_tracker_call_lines(args_val);
    lines.extend(crate::agent::runloop::tool_output::tracker_view_lines(
        output,
    ));
    lines
}

fn task_tracker_block_segments(lines: &[String]) -> Vec<Vec<InlineSegment>> {
    let style = std::sync::Arc::new(InlineTextStyle::default());
    lines
        .iter()
        .map(|line| {
            vec![InlineSegment {
                text: line.clone(),
                style: style.clone(),
            }]
        })
        .collect()
}

fn apply_task_tracker_block(
    handle: &InlineHandle,
    harness_state: &mut crate::agent::runloop::unified::run_loop_context::HarnessTurnState,
    lines: Vec<String>,
) {
    let replace_count = harness_state.replaceable_task_tracker_count();
    let segments = task_tracker_block_segments(&lines);

    if let Some(count) = replace_count {
        handle.replace_last(count, InlineMessageKind::Tool, segments);
        transcript::replace_last(count, &lines);
    } else {
        for (segments, plain_line) in segments.into_iter().zip(lines.iter()) {
            handle.append_line(InlineMessageKind::Tool, segments);
            transcript::append(plain_line);
        }
    }

    harness_state.remember_task_tracker_block(lines);
}

/// Extract the command string from tool call arguments.
fn extract_command_line(args: &serde_json::Value) -> Option<String> {
    args.get("command")
        .and_then(serde_json::Value::as_str)
        .map(str::to_string)
        .or_else(|| {
            args.get("raw_command")
                .and_then(serde_json::Value::as_str)
                .map(str::to_string)
        })
        .or_else(|| {
            args.get("cmd")
                .and_then(serde_json::Value::as_str)
                .map(str::to_string)
        })
}

/// Record the tool-call summary line ("• Ran ...") to the transcript only.
fn record_summary_line(
    name: &str,
    args: &serde_json::Value,
    _output: &serde_json::Value,
    _command_success: bool,
) {
    let action_label = if tool_intent::is_command_run_tool_call(name, args) {
        "Run command"
    } else {
        name
    };
    let headline = if action_label == "Run command" {
        if let Some(cmd) = extract_command_line(args) {
            format!("Ran {cmd}")
        } else {
            "Ran command".to_string()
        }
    } else {
        format!("• {action_label}")
    };
    transcript::append(&headline);
}

/// Record the tool output content (stdout/stderr) to the transcript only.
fn record_output_lines(output: &serde_json::Value) {
    for key in ["output", "stdout", "stderr"] {
        if let Some(text) = output.get(key).and_then(serde_json::Value::as_str)
            && !text.trim().is_empty()
        {
            for line in text.lines() {
                transcript::append(line);
            }
        }
    }
}

async fn render_tool_output_common(
    renderer: &mut AnsiRenderer,
    name: &str,
    args_val: &serde_json::Value,
    output: &serde_json::Value,
    command_success: bool,
    vt_config: Option<&VTCodeConfig>,
) -> Result<()> {
    let inline_run_tool = renderer.supports_inline_ui() && is_run_pty_tool(name, args_val);
    let git_diff_payload = is_git_diff_payload(output);

    // For streamed inline PTY tools: the pre-execution Pty block already
    // shows "• Ran ..." and output. Skip duplicating the output in the TUI,
    // but still record the summary and output content to the transcript.
    if inline_run_tool && !git_diff_payload {
        // Record the command summary to transcript (TUI already showed it via PTY block)
        record_summary_line(name, args_val, output, command_success);

        // Record streamed output content to transcript
        record_output_lines(output);

        if !has_renderable_stream_content(output) && command_success {
            renderer.line(MessageStyle::Info, "(no output)")?;
            return Ok(());
        }

        // Send completion via pty_continuation_line so it joins the Pty block
        // as a continuation (2-space block prefix + 4-space indent = 6 spaces,
        // matching PTY output continuation lines).
        if let Some(completion) = compact_run_completion_line(output, command_success) {
            renderer.pty_continuation_line(&completion)?;
            // Pty kind skips transcript recording in TUI mode; append manually.
            transcript::append(&completion);
        }
        return Ok(());
    }

    // Render the summary header for non-streamed tools.
    // (streamed PTY tools with git_diff_payload also skip summary here,
    //  falling through to the full render_tool_output path.)
    if !(inline_run_tool && git_diff_payload) {
        let stream_label = crate::agent::runloop::unified::tool_summary::stream_label_from_output(
            output,
            command_success,
        );
        crate::agent::runloop::unified::tool_summary::render_tool_call_summary(
            renderer,
            name,
            args_val,
            stream_label,
        )?;
    }

    crate::agent::runloop::tool_output::render_tool_output(renderer, Some(name), output, vt_config)
        .await
}

fn render_error_common(
    renderer: &mut AnsiRenderer,
    name: &str,
    error: &str,
    error_type: &str,
) -> Result<()> {
    let err_msg = format!("Tool '{}' {}: {}", name, error_type, error);
    renderer.line(MessageStyle::Error, &err_msg)?;
    Ok(())
}

#[derive(Default)]
struct OutcomeState {
    turn_modified_files: Vec<PathBuf>,
    last_tool_stdout: Option<String>,
}

impl OutcomeState {
    fn into_tuple(self) -> (Vec<PathBuf>, Option<String>) {
        (self.turn_modified_files, self.last_tool_stdout)
    }
}

struct OutcomeContext<'a> {
    session_stats: &'a mut SessionStats,
    renderer: &'a mut AnsiRenderer,
    handle: &'a InlineHandle,
    harness_state: &'a mut crate::agent::runloop::unified::run_loop_context::HarnessTurnState,
    mcp_panel_state: &'a mut McpPanelState,
    vt_config: Option<&'a VTCodeConfig>,
}

struct SuccessPayload<'a> {
    output: &'a serde_json::Value,
    stdout: &'a Option<String>,
    modified_files: &'a [String],
    command_success: bool,
}

async fn handle_success_common(
    ctx: &mut OutcomeContext<'_>,
    name: &str,
    args_val: &serde_json::Value,
    payload: SuccessPayload<'_>,
    state: &mut OutcomeState,
) -> Result<()> {
    ctx.session_stats.record_tool(name);

    if let Some(tool_name) = name.strip_prefix("mcp_") {
        let tool_name = tool_name.trim_start_matches('_');
        let tool_name = tool_name.split("__").last().unwrap_or(tool_name);
        record_mcp_success_event(ctx.mcp_panel_state, tool_name, args_val);
    } else if is_task_tracker_tool(name) && ctx.renderer.supports_inline_ui() {
        let block_lines = task_tracker_block_lines(args_val, payload.output);
        ctx.handle.update_task_panel(block_lines.clone());
        apply_task_tracker_block(ctx.handle, ctx.harness_state, block_lines);
    } else {
        render_tool_output_common(
            ctx.renderer,
            name,
            args_val,
            payload.output,
            payload.command_success,
            ctx.vt_config,
        )
        .await?;
    }

    state.last_tool_stdout = if payload.command_success {
        payload.stdout.clone()
    } else {
        None
    };

    if !payload.modified_files.is_empty() {
        state
            .turn_modified_files
            .extend(collect_modified_files(payload.modified_files));
    }

    Ok(())
}

fn handle_non_success_common(
    ctx: &mut OutcomeContext<'_>,
    name: &str,
    status: &ToolExecutionStatus,
) -> Result<()> {
    match status {
        ToolExecutionStatus::Failure { error } => {
            render_error_common(ctx.renderer, name, &error.user_message(), "failure")?;
        }
        ToolExecutionStatus::Timeout { error } => {
            render_error_common(ctx.renderer, name, &error.user_message(), "timed out")?;
        }
        ToolExecutionStatus::Cancelled => {
            ctx.renderer
                .line(MessageStyle::Info, "Tool execution cancelled")?;
        }
        ToolExecutionStatus::Success { .. } => {}
    }

    Ok(())
}

async fn process_outcome_common(
    ctx: &mut OutcomeContext<'_>,
    name: &str,
    args_val: &serde_json::Value,
    outcome: &ToolPipelineOutcome,
) -> Result<OutcomeState> {
    let mut state = OutcomeState::default();

    match &outcome.status {
        ToolExecutionStatus::Success {
            output,
            stdout,
            modified_files,
            command_success,
            ..
        } => {
            handle_success_common(
                ctx,
                name,
                args_val,
                SuccessPayload {
                    output,
                    stdout,
                    modified_files,
                    command_success: *command_success,
                },
                &mut state,
            )
            .await?;
        }
        _ => handle_non_success_common(ctx, name, &outcome.status)?,
    }

    Ok(state)
}

pub(crate) async fn handle_pipeline_output(
    ctx: &mut RunLoopContext<'_>,
    name: &str,
    args_val: &serde_json::Value,
    outcome: &ToolPipelineOutcome,
    vt_config: Option<&VTCodeConfig>,
) -> Result<(Vec<PathBuf>, Option<String>)> {
    let mut output_ctx = OutcomeContext {
        session_stats: ctx.session_stats,
        renderer: ctx.renderer,
        handle: ctx.handle,
        harness_state: ctx.harness_state,
        mcp_panel_state: ctx.mcp_panel_state,
        vt_config,
    };
    let state = process_outcome_common(&mut output_ctx, name, args_val, outcome).await?;
    Ok(state.into_tuple())
}

// Adapter for TurnLoopContext (to avoid duplication when handling tool output in the turn loop)
pub(crate) async fn handle_pipeline_output_from_turn_ctx(
    ctx: &mut crate::agent::runloop::unified::turn::TurnLoopContext<'_>,
    name: &str,
    args_val: &serde_json::Value,
    outcome: &ToolPipelineOutcome,
    vt_config: Option<&VTCodeConfig>,
) -> Result<(Vec<PathBuf>, Option<String>)> {
    let mut run_ctx = ctx.as_run_loop_context();
    let (modified_files, last_stdout) =
        handle_pipeline_output(&mut run_ctx, name, args_val, outcome, vt_config).await?;

    if let ToolExecutionStatus::Success {
        output,
        modified_files,
        ..
    } = &outcome.status
    {
        let activity_paths = collect_instruction_activity_paths(
            ctx.config.workspace.as_path(),
            args_val,
            output,
            modified_files,
        );
        if !activity_paths.is_empty() {
            ctx.context_manager
                .record_instruction_activity_paths(activity_paths);
        }
    }

    Ok((modified_files, last_stdout))
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::{IsTerminal, stdin};
    use std::sync::Arc;
    use tempfile::TempDir;
    use tokio::sync::{RwLock, mpsc::unbounded_channel};
    use vtcode_core::acp::ToolPermissionCache;
    use vtcode_core::config::loader::VTCodeConfig;
    use vtcode_core::core::decision_tracker::DecisionTracker;
    use vtcode_core::core::trajectory::TrajectoryLogger;
    use vtcode_core::tools::ApprovalRecorder;
    use vtcode_core::tools::registry::ToolRegistry;
    use vtcode_core::tools::result_cache::{ToolCacheKey, ToolResultCache};
    use vtcode_core::ui::inline_theme_from_core_styles;
    use vtcode_core::ui::theme;
    use vtcode_tui::app::{
        InlineCommand, InlineHandle, SessionOptions, spawn_session_with_options,
    };

    fn build_harness_state() -> crate::agent::runloop::unified::run_loop_context::HarnessTurnState {
        crate::agent::runloop::unified::run_loop_context::HarnessTurnState::new(
            crate::agent::runloop::unified::run_loop_context::TurnRunId("test-run".to_string()),
            crate::agent::runloop::unified::run_loop_context::TurnId("test-turn".to_string()),
            4,
            60,
            0,
        )
    }

    fn dummy_handle() -> InlineHandle {
        InlineHandle::new_for_tests(unbounded_channel().0)
    }

    // Use Tokio runtime for async test blocks
    #[tokio::test]
    async fn test_renderer_records_tool_and_collects_modified_files() {
        // Setup a stdout renderer
        let mut renderer = AnsiRenderer::stdout();

        // Prepare session stats and mcp state
        let mut stats = SessionStats::default();
        let mut mcp = McpPanelState::default();

        // Create an outcome that indicates write to /tmp/foo.txt
        let output_json = serde_json::json!({"result":"ok"});
        let outcome = ToolPipelineOutcome::from_status(ToolExecutionStatus::Success {
            output: output_json.clone(),
            stdout: None,
            modified_files: vec!["/tmp/foo.txt".to_string()],
            command_success: true,
        });

        // Invoke the shared outcome processor via a minimal output context.
        let handle = dummy_handle();
        let mut harness_state = build_harness_state();
        let mut output_ctx = OutcomeContext {
            session_stats: &mut stats,
            renderer: &mut renderer,
            handle: &handle,
            harness_state: &mut harness_state,
            mcp_panel_state: &mut mcp,
            vt_config: None::<&VTCodeConfig>,
        };
        let (mod_files, _last_stdout) = process_outcome_common(
            &mut output_ctx,
            "write_file",
            &serde_json::json!({}),
            &outcome,
        )
        .await
        .expect("render should succeed")
        .into_tuple();

        // Confirm the function recorded the tool call
        let recorded = stats.sorted_tools();
        assert!(recorded.contains(&"write_file".to_string()));

        // Confirm the modified files list contains our path
        assert_eq!(mod_files, vec![PathBuf::from("/tmp/foo.txt")]);
    }

    #[tokio::test]
    async fn test_renderer_records_mcp_event_for_mcp_tool() {
        let mut renderer = AnsiRenderer::stdout();

        // Note: tests involving `apply_turn_outcome` live in `turn/turn_loop.rs` and can be added there
        let mut stats = SessionStats::default();
        let mut mcp = McpPanelState::new(32, true); // enabled

        let output_json = serde_json::json!({"exit_code":0});
        let outcome = ToolPipelineOutcome::from_status(ToolExecutionStatus::Success {
            output: output_json.clone(),
            stdout: Some("ok".to_string()),
            modified_files: vec![],
            command_success: true,
        });

        let handle = dummy_handle();
        let mut harness_state = build_harness_state();
        let mut output_ctx = OutcomeContext {
            session_stats: &mut stats,
            renderer: &mut renderer,
            handle: &handle,
            harness_state: &mut harness_state,
            mcp_panel_state: &mut mcp,
            vt_config: None::<&VTCodeConfig>,
        };
        let (_mod_files, _last_stdout) = process_outcome_common(
            &mut output_ctx,
            "mcp_example",
            &serde_json::json!({}),
            &outcome,
        )
        .await
        .expect("render should succeed")
        .into_tuple();

        // Ensure mcp panel recorded an event
        assert!(mcp.event_count() > 0);
    }

    #[tokio::test]
    async fn spooled_exec_output_keeps_transcript_at_reference_only() {
        let mut renderer = AnsiRenderer::stdout();
        let mut stats = SessionStats::default();
        let mut mcp = McpPanelState::default();
        let handle = dummy_handle();
        let mut harness_state = build_harness_state();

        transcript::clear();

        let outcome = ToolPipelineOutcome::from_status(ToolExecutionStatus::Success {
            output: serde_json::json!({
                "output": "preview text that should stay out of transcript persistence",
                "spool_path": ".vtcode/context/tool_outputs/unified_exec_1.txt",
                "exit_code": 0,
                "is_exited": true
            }),
            stdout: Some("preview text that should stay out of transcript persistence".to_string()),
            modified_files: vec![],
            command_success: true,
        });

        let mut output_ctx = OutcomeContext {
            session_stats: &mut stats,
            renderer: &mut renderer,
            handle: &handle,
            harness_state: &mut harness_state,
            mcp_panel_state: &mut mcp,
            vt_config: None::<&VTCodeConfig>,
        };

        process_outcome_common(
            &mut output_ctx,
            tools::UNIFIED_EXEC,
            &serde_json::json!({
                "action": "run",
                "command": "cargo check -p vtcode-core"
            }),
            &outcome,
        )
        .await
        .expect("render should succeed");

        let transcript_lines = transcript::snapshot();
        let transcript_text = transcript_lines.join("\n");
        let stripped_text = vtcode_core::utils::ansi_parser::strip_ansi(&transcript_text);
        assert!(
            stripped_text.contains("Large output was spooled to"),
            "Transcript: {:?}",
            stripped_text
        );
        assert!(
            !stripped_text.contains("preview text that should stay out of transcript persistence")
        );

        transcript::clear();
    }

    #[tokio::test]
    async fn test_handle_pipeline_output_collects_modified_files_and_records_stats() {
        if !stdin().is_terminal() {
            eprintln!("Skipping TUI-dependent test in non-interactive environment");
            return;
        }

        let tmp = TempDir::new().unwrap();
        let workspace = tmp.path().to_path_buf();

        let mut registry = ToolRegistry::new(workspace.clone()).await;
        let permission_cache_arc = Arc::new(RwLock::new(ToolPermissionCache::new()));
        let permissions_state = Arc::new(RwLock::new(
            vtcode_core::config::PermissionsConfig::default(),
        ));

        let mut session = spawn_session_with_options(
            inline_theme_from_core_styles(&theme::active_styles()),
            SessionOptions {
                inline_rows: 10,
                workspace_root: Some(workspace.clone()),
                ..SessionOptions::default()
            },
        )
        .unwrap();
        let handle = session.clone_inline_handle();
        let mut renderer = AnsiRenderer::with_inline_ui(handle.clone(), Default::default());

        let cache = Arc::new(RwLock::new(ToolResultCache::new(8)));
        let key = ToolCacheKey::new("read_file", "{}", "/tmp/foo.txt");
        {
            let mut c = cache.write().await;
            c.insert_arc(key.clone(), Arc::new("{}".to_string()));
            assert!(c.get(&key).is_some());
        }

        let decision_ledger = Arc::new(RwLock::new(DecisionTracker::new()));
        let mut session_stats = SessionStats::default();
        let mut mcp_panel = McpPanelState::new(10, true);
        let approval_recorder = ApprovalRecorder::new(workspace.clone());
        let traj = TrajectoryLogger::new(&workspace);
        let tools = Arc::new(RwLock::new(Vec::new()));

        let mut harness_state = build_harness_state();
        let mut ctx = RunLoopContext::new(
            &mut renderer,
            &handle,
            &mut registry,
            &tools,
            &cache,
            &permission_cache_arc,
            &permissions_state,
            &decision_ledger,
            &mut session_stats,
            &mut mcp_panel,
            &approval_recorder,
            &mut session,
            None,
            &traj,
            &mut harness_state,
            None,
        );

        let outcome = ToolPipelineOutcome::from_status(ToolExecutionStatus::Success {
            output: serde_json::json!({"ok": true}),
            stdout: None,
            modified_files: vec!["/tmp/foo.txt".to_string()],
            command_success: true,
        });

        let (mod_files, _last_stdout) = handle_pipeline_output(
            &mut ctx,
            "read_file",
            &serde_json::json!({}),
            &outcome,
            None::<&VTCodeConfig>,
        )
        .await
        .expect("handle should succeed");

        assert_eq!(mod_files, vec![PathBuf::from("/tmp/foo.txt")]);

        // Cache invalidation is handled in execution side-effects, not output rendering.
        {
            let c = cache.write().await;
            assert!(c.get(&key).is_some());
        }

        // Ensure session stats were updated
        let rec = session_stats.sorted_tools();
        assert!(rec.contains(&"read_file".to_string()));
    }

    #[tokio::test]
    async fn task_tracker_updates_replace_previous_inline_block() {
        transcript::clear();

        let (sender, mut receiver) = unbounded_channel();
        let handle = InlineHandle::new_for_tests(sender);
        let mut renderer = AnsiRenderer::with_inline_ui(handle.clone(), Default::default());
        let mut stats = SessionStats::default();
        let mut mcp = McpPanelState::default();
        let mut harness_state = build_harness_state();

        let first = ToolPipelineOutcome::from_status(ToolExecutionStatus::Success {
            output: serde_json::json!({
                "status": "updated",
                "view": {
                    "title": "Respond to user greeting and assess next steps",
                    "lines": [
                        {"display": "├ ✔ Greet user and summarize current workspace state"},
                        {"display": "├ > Ask what task they'd like to tackle"},
                        {"display": "└ • Offer to provide workspace tour if needed"}
                    ]
                },
                "checklist": {
                    "title": "Respond to user greeting and assess next steps",
                    "total": 3,
                    "completed": 1,
                    "in_progress": 1,
                    "pending": 1,
                    "blocked": 0,
                    "progress_percent": 33,
                    "items": [
                        {"index": 1, "description": "Greet user and summarize current workspace state", "status": "completed"},
                        {"index": 2, "description": "Ask what task they'd like to tackle", "status": "in_progress"},
                        {"index": 3, "description": "Offer to provide workspace tour if needed", "status": "pending"}
                    ]
                },
                "message": "Item 2 status changed: pending → in_progress"
            }),
            stdout: None,
            modified_files: vec![],
            command_success: true,
        });
        let second = ToolPipelineOutcome::from_status(ToolExecutionStatus::Success {
            output: serde_json::json!({
                "status": "updated",
                "view": {
                    "title": "Respond to user greeting and assess next steps",
                    "lines": [
                        {"display": "├ ✔ Greet user and summarize current workspace state"},
                        {"display": "├ ✔ Ask what task they'd like to tackle"},
                        {"display": "└ • Offer to provide workspace tour if needed"}
                    ]
                },
                "checklist": {
                    "title": "Respond to user greeting and assess next steps",
                    "total": 3,
                    "completed": 2,
                    "in_progress": 0,
                    "pending": 1,
                    "blocked": 0,
                    "progress_percent": 67,
                    "items": [
                        {"index": 1, "description": "Greet user and summarize current workspace state", "status": "completed"},
                        {"index": 2, "description": "Ask what task they'd like to tackle", "status": "completed"},
                        {"index": 3, "description": "Offer to provide workspace tour if needed", "status": "pending"}
                    ]
                },
                "message": "Item 2 status changed: in_progress → completed"
            }),
            stdout: None,
            modified_files: vec![],
            command_success: true,
        });

        let args = serde_json::json!({"action": "update", "index": 2, "status": "in_progress"});
        let mut output_ctx = OutcomeContext {
            session_stats: &mut stats,
            renderer: &mut renderer,
            handle: &handle,
            harness_state: &mut harness_state,
            mcp_panel_state: &mut mcp,
            vt_config: None::<&VTCodeConfig>,
        };

        process_outcome_common(&mut output_ctx, tools::TASK_TRACKER, &args, &first)
            .await
            .expect("first tracker render should succeed");

        let args = serde_json::json!({"action": "update", "index": 2, "status": "completed"});
        process_outcome_common(&mut output_ctx, tools::TASK_TRACKER, &args, &second)
            .await
            .expect("second tracker render should succeed");

        let mut saw_task_panel_update = false;
        while let Ok(command) = receiver.try_recv() {
            if matches!(command, InlineCommand::ShowTransient { .. }) {
                saw_task_panel_update = true;
            }
        }

        assert!(
            saw_task_panel_update,
            "expected tracker updates to refresh the dedicated task panel"
        );
    }

    #[tokio::test]
    async fn test_handle_pipeline_output_mcp_events() {
        if !stdin().is_terminal() {
            eprintln!("Skipping TUI-dependent test in non-interactive environment");
            return;
        }

        let tmp = TempDir::new().unwrap();
        let workspace = tmp.path().to_path_buf();

        let mut registry = ToolRegistry::new(workspace.clone()).await;
        let permission_cache_arc = Arc::new(RwLock::new(ToolPermissionCache::new()));
        let permissions_state = Arc::new(RwLock::new(
            vtcode_core::config::PermissionsConfig::default(),
        ));

        let mut session = spawn_session_with_options(
            inline_theme_from_core_styles(&theme::active_styles()),
            SessionOptions {
                inline_rows: 10,
                workspace_root: Some(workspace.clone()),
                ..SessionOptions::default()
            },
        )
        .unwrap();
        let handle = session.clone_inline_handle();
        let mut renderer = AnsiRenderer::with_inline_ui(handle.clone(), Default::default());

        let cache = Arc::new(RwLock::new(ToolResultCache::new(8)));
        let decision_ledger = Arc::new(RwLock::new(DecisionTracker::new()));
        let mut session_stats = SessionStats::default();
        let mut mcp_panel = McpPanelState::new(10, true);
        let approval_recorder = ApprovalRecorder::new(workspace.clone());
        let traj = TrajectoryLogger::new(&workspace);
        let tools = Arc::new(RwLock::new(Vec::new()));

        let mut harness_state = build_harness_state();
        let mut ctx = RunLoopContext::new(
            &mut renderer,
            &handle,
            &mut registry,
            &tools,
            &cache,
            &permission_cache_arc,
            &permissions_state,
            &decision_ledger,
            &mut session_stats,
            &mut mcp_panel,
            &approval_recorder,
            &mut session,
            None,
            &traj,
            &mut harness_state,
            None,
        );

        let outcome = ToolPipelineOutcome::from_status(ToolExecutionStatus::Success {
            output: serde_json::json!({"exit_code": 0}),
            stdout: Some("ok".to_string()),
            modified_files: vec![],
            command_success: true,
        });

        let (_mod_files, _last_stdout) = handle_pipeline_output(
            &mut ctx,
            "mcp_example",
            &serde_json::json!({}),
            &outcome,
            None::<&VTCodeConfig>,
        )
        .await
        .expect("handle should succeed");

        assert!(ctx.mcp_panel_state.event_count() > 0);
    }
}
