use std::sync::Arc;

use serde_json::json;
use tokio::sync::Notify;
use vtcode_core::acp::PermissionGrant;
use vtcode_core::acp::permission_cache::ToolPermissionCache;
use vtcode_core::config::constants::tools;
use vtcode_core::config::loader::VTCodeConfig;
use vtcode_core::core::decision_tracker::DecisionTracker;
use vtcode_core::core::trajectory::TrajectoryLogger;
use vtcode_core::tools::result_cache::ToolResultCache;

use super::*;

#[tokio::test]
async fn test_run_tool_call_unknown_tool_failure() {
    let mut test_ctx = TestContext::new().await;
    let mut registry = test_ctx.registry;

    let permission_cache_arc = Arc::new(tokio::sync::RwLock::new(ToolPermissionCache::new()));
    {
        let mut cache = permission_cache_arc.write().await;
        cache.cache_grant("test_tool".to_string(), PermissionGrant::Permanent);
    }

    let result_cache = Arc::new(tokio::sync::RwLock::new(ToolResultCache::new(10)));
    let decision_ledger = Arc::new(tokio::sync::RwLock::new(DecisionTracker::new()));
    let mut session_stats = crate::agent::runloop::unified::state::SessionStats::default();
    let mut mcp_panel = crate::agent::runloop::mcp_events::McpPanelState::new(10, true);
    let approval_recorder = test_ctx.approval_recorder;
    let traj = TrajectoryLogger::new(&test_ctx.workspace);
    let tools = Arc::new(tokio::sync::RwLock::new(Vec::new()));

    let mut harness_state = build_harness_state();
    let mut ctx = crate::agent::runloop::unified::run_loop_context::RunLoopContext::new(
        &mut test_ctx.renderer,
        &test_ctx.handle,
        &mut registry,
        &tools,
        &result_cache,
        &permission_cache_arc,
        &test_ctx.permissions_state,
        &decision_ledger,
        &mut session_stats,
        &mut mcp_panel,
        &approval_recorder,
        &mut test_ctx.session,
        None,
        &traj,
        &mut harness_state,
        None,
    );

    let call = vtcode_core::llm::provider::ToolCall::function(
        "call_1".to_string(),
        "test_tool".to_string(),
        "{}".to_string(),
    );
    let ctrl_c_state = Arc::new(CtrlCState::new());
    let ctrl_c_notify = Arc::new(Notify::new());

    let outcome = run_tool_call(
        &mut ctx,
        &call,
        &ctrl_c_state,
        &ctrl_c_notify,
        None,
        None,
        true,
        None,
        0,
        false,
    )
    .await
    .expect("run_tool_call must run");

    assert!(matches!(
        outcome.status,
        ToolExecutionStatus::Failure { .. }
    ));
}

#[tokio::test]
async fn test_run_tool_call_respects_max_tool_calls_budget() {
    let mut test_ctx = TestContext::new().await;
    test_ctx.session.set_skip_confirmations(false);
    let mut registry = test_ctx.registry;

    let permission_cache_arc = Arc::new(tokio::sync::RwLock::new(ToolPermissionCache::new()));
    let result_cache = Arc::new(tokio::sync::RwLock::new(ToolResultCache::new(10)));
    let decision_ledger = Arc::new(tokio::sync::RwLock::new(DecisionTracker::new()));
    let mut session_stats = crate::agent::runloop::unified::state::SessionStats::default();
    let mut mcp_panel = crate::agent::runloop::mcp_events::McpPanelState::new(10, true);
    let approval_recorder = test_ctx.approval_recorder;
    let traj = TrajectoryLogger::new(&test_ctx.workspace);
    let tools = Arc::new(tokio::sync::RwLock::new(Vec::new()));

    let mut harness_state = build_harness_state_with(1);
    harness_state.record_tool_call(); // Exhaust the budget (1/1)
    let mut ctx = crate::agent::runloop::unified::run_loop_context::RunLoopContext::new(
        &mut test_ctx.renderer,
        &test_ctx.handle,
        &mut registry,
        &tools,
        &result_cache,
        &permission_cache_arc,
        &test_ctx.permissions_state,
        &decision_ledger,
        &mut session_stats,
        &mut mcp_panel,
        &approval_recorder,
        &mut test_ctx.session,
        None,
        &traj,
        &mut harness_state,
        None,
    );

    let call = vtcode_core::llm::provider::ToolCall::function(
        "call_budget".to_string(),
        "read_file".to_string(),
        "{}".to_string(),
    );
    let ctrl_c_state = Arc::new(CtrlCState::new());
    let ctrl_c_notify = Arc::new(Notify::new());

    let outcome = run_tool_call(
        &mut ctx,
        &call,
        &ctrl_c_state,
        &ctrl_c_notify,
        None,
        None,
        false,
        None,
        0,
        false,
    )
    .await
    .expect("run_tool_call must run");

    println!("Outcome status: {:?}", outcome.status);

    match outcome.status {
        ToolExecutionStatus::Failure { error } => {
            assert!(error.to_string().contains("Policy violation"));
            assert!(
                error
                    .to_string()
                    .contains("exceeded max tool calls per turn")
            );
        }
        other => panic!("Expected permission denial, got: {:?}", other),
    }
}

#[tokio::test]
async fn test_run_tool_call_allows_unlimited_budget_when_disabled() {
    let mut test_ctx = TestContext::new().await;
    let mut registry = test_ctx.registry;

    let permission_cache_arc = Arc::new(tokio::sync::RwLock::new(ToolPermissionCache::new()));
    let result_cache = Arc::new(tokio::sync::RwLock::new(ToolResultCache::new(10)));
    let decision_ledger = Arc::new(tokio::sync::RwLock::new(DecisionTracker::new()));
    let mut session_stats = crate::agent::runloop::unified::state::SessionStats::default();
    let mut mcp_panel = crate::agent::runloop::mcp_events::McpPanelState::new(10, true);
    let approval_recorder = test_ctx.approval_recorder;
    let traj = TrajectoryLogger::new(&test_ctx.workspace);
    let tool_defs = Arc::new(tokio::sync::RwLock::new(Vec::new()));

    let mut harness_state = build_harness_state_with(0);
    for _ in 0..4 {
        harness_state.record_tool_call();
    }
    let mut ctx = crate::agent::runloop::unified::run_loop_context::RunLoopContext::new(
        &mut test_ctx.renderer,
        &test_ctx.handle,
        &mut registry,
        &tool_defs,
        &result_cache,
        &permission_cache_arc,
        &test_ctx.permissions_state,
        &decision_ledger,
        &mut session_stats,
        &mut mcp_panel,
        &approval_recorder,
        &mut test_ctx.session,
        None,
        &traj,
        &mut harness_state,
        None,
    );

    let call = vtcode_core::llm::provider::ToolCall::function(
        "call_unlimited".to_string(),
        "read_file".to_string(),
        "{}".to_string(),
    );
    let ctrl_c_state = Arc::new(CtrlCState::new());
    let ctrl_c_notify = Arc::new(Notify::new());

    let outcome = run_tool_call(
        &mut ctx,
        &call,
        &ctrl_c_state,
        &ctrl_c_notify,
        None,
        None,
        false,
        None,
        0,
        false,
    )
    .await
    .expect("run_tool_call must run");

    assert!(!matches!(
        outcome.status,
        ToolExecutionStatus::Failure { ref error }
            if error
                .to_string()
                .contains("exceeded max tool calls per turn")
    ));
}

#[tokio::test]
async fn test_run_tool_call_prevalidated_blocks_mutation_in_plan_mode() {
    let mut test_ctx = TestContext::new().await;
    let mut registry = test_ctx.registry;

    let permission_cache_arc = Arc::new(tokio::sync::RwLock::new(ToolPermissionCache::new()));
    {
        let mut cache = permission_cache_arc.write().await;
        cache.cache_grant(tools::WRITE_FILE.to_string(), PermissionGrant::Permanent);
    }

    let result_cache = Arc::new(tokio::sync::RwLock::new(ToolResultCache::new(10)));
    let decision_ledger = Arc::new(tokio::sync::RwLock::new(DecisionTracker::new()));
    let mut session_stats = crate::agent::runloop::unified::state::SessionStats::default();
    let mut mcp_panel = crate::agent::runloop::mcp_events::McpPanelState::new(10, true);
    let approval_recorder = test_ctx.approval_recorder;
    let traj = TrajectoryLogger::new(&test_ctx.workspace);
    let tool_defs = Arc::new(tokio::sync::RwLock::new(Vec::new()));

    registry.enable_plan_mode();
    registry.plan_mode_state().enable();
    session_stats.set_plan_mode(true);

    let mut harness_state = build_harness_state();
    let mut ctx = crate::agent::runloop::unified::run_loop_context::RunLoopContext::new(
        &mut test_ctx.renderer,
        &test_ctx.handle,
        &mut registry,
        &tool_defs,
        &result_cache,
        &permission_cache_arc,
        &test_ctx.permissions_state,
        &decision_ledger,
        &mut session_stats,
        &mut mcp_panel,
        &approval_recorder,
        &mut test_ctx.session,
        None,
        &traj,
        &mut harness_state,
        None,
    );

    let payload = serde_json::to_string(&json!({
        "path": "notes.txt",
        "content": "hello plan mode"
    }))
    .expect("serialize tool args");
    let call = vtcode_core::llm::provider::ToolCall::function(
        "call_plan_write".to_string(),
        tools::WRITE_FILE.to_string(),
        payload,
    );
    let ctrl_c_state = Arc::new(CtrlCState::new());
    let ctrl_c_notify = Arc::new(Notify::new());

    let outcome = run_tool_call(
        &mut ctx,
        &call,
        &ctrl_c_state,
        &ctrl_c_notify,
        None,
        None,
        true,
        None,
        0,
        true,
    )
    .await
    .expect("run_tool_call must run");

    println!("Plan-mode guard test outcome status: {:?}", outcome.status);

    match outcome.status {
        ToolExecutionStatus::Failure { error } => {
            assert!(error.to_string().contains("plan mode"));
        }
        other => panic!("Expected plan mode failure, got: {:?}", other),
    }
    assert!(session_stats.is_plan_mode());
    assert!(registry.is_plan_mode());
    assert!(registry.plan_mode_state().is_active());
}

#[tokio::test]
async fn test_run_tool_call_prevalidated_allows_task_tracker_in_plan_mode() {
    let mut test_ctx = TestContext::new().await;
    let mut registry = test_ctx.registry;

    let permission_cache_arc = Arc::new(tokio::sync::RwLock::new(ToolPermissionCache::new()));
    let result_cache = Arc::new(tokio::sync::RwLock::new(ToolResultCache::new(10)));
    let decision_ledger = Arc::new(tokio::sync::RwLock::new(DecisionTracker::new()));
    let mut session_stats = crate::agent::runloop::unified::state::SessionStats::default();
    let mut mcp_panel = crate::agent::runloop::mcp_events::McpPanelState::new(10, true);
    let approval_recorder = test_ctx.approval_recorder;
    let traj = TrajectoryLogger::new(&test_ctx.workspace);
    let tool_defs = Arc::new(tokio::sync::RwLock::new(Vec::new()));

    registry.enable_plan_mode();
    registry.plan_mode_state().enable();
    session_stats.set_plan_mode(true);

    let plans_dir = test_ctx.workspace.join(".vtcode").join("plans");
    std::fs::create_dir_all(&plans_dir).expect("create plans dir");
    let plan_file = plans_dir.join("tracker-test-task-tracker.md");
    std::fs::write(&plan_file, "# Tracker Test\n").expect("write plan file");
    registry
        .plan_mode_state()
        .set_plan_file(Some(plan_file))
        .await;

    let mut harness_state = build_harness_state();
    let mut ctx = crate::agent::runloop::unified::run_loop_context::RunLoopContext::new(
        &mut test_ctx.renderer,
        &test_ctx.handle,
        &mut registry,
        &tool_defs,
        &result_cache,
        &permission_cache_arc,
        &test_ctx.permissions_state,
        &decision_ledger,
        &mut session_stats,
        &mut mcp_panel,
        &approval_recorder,
        &mut test_ctx.session,
        None,
        &traj,
        &mut harness_state,
        None,
    );

    let call = vtcode_core::llm::provider::ToolCall::function(
        "call_plan_task_tracker".to_string(),
        tools::TASK_TRACKER.to_string(),
        r#"{"action":"list"}"#.to_string(),
    );
    let ctrl_c_state = Arc::new(CtrlCState::new());
    let ctrl_c_notify = Arc::new(Notify::new());

    let outcome = run_tool_call(
        &mut ctx,
        &call,
        &ctrl_c_state,
        &ctrl_c_notify,
        None,
        None,
        true,
        None,
        0,
        true,
    )
    .await
    .expect("run_tool_call must run");

    match outcome.status {
        ToolExecutionStatus::Success { output, .. } => {
            assert!(
                output["status"] == "ok" || output["status"] == "empty",
                "unexpected status: {}",
                output["status"]
            );
        }
        other => panic!(
            "Expected task_tracker success in plan mode, got: {:?}",
            other
        ),
    }
}

#[tokio::test]
async fn test_run_tool_call_non_prevalidated_allows_task_tracker_in_plan_mode_and_tracks_budget() {
    let mut test_ctx = TestContext::new().await;
    let mut registry = test_ctx.registry;

    let permission_cache_arc = Arc::new(tokio::sync::RwLock::new(ToolPermissionCache::new()));
    let result_cache = Arc::new(tokio::sync::RwLock::new(ToolResultCache::new(10)));
    let decision_ledger = Arc::new(tokio::sync::RwLock::new(DecisionTracker::new()));
    let mut session_stats = crate::agent::runloop::unified::state::SessionStats::default();
    let mut mcp_panel = crate::agent::runloop::mcp_events::McpPanelState::new(10, true);
    let approval_recorder = test_ctx.approval_recorder;
    let traj = TrajectoryLogger::new(&test_ctx.workspace);
    let tool_defs = Arc::new(tokio::sync::RwLock::new(Vec::new()));

    registry.enable_plan_mode();
    registry.plan_mode_state().enable();
    session_stats.set_plan_mode(true);

    let plans_dir = test_ctx.workspace.join(".vtcode").join("plans");
    std::fs::create_dir_all(&plans_dir).expect("create plans dir");
    let plan_file = plans_dir.join("tracker-test-task-tracker-non-prevalidated.md");
    std::fs::write(&plan_file, "# Tracker Test\n").expect("write plan file");
    registry
        .plan_mode_state()
        .set_plan_file(Some(plan_file))
        .await;

    let mut harness_state = build_harness_state_with(2);
    let mut ctx = crate::agent::runloop::unified::run_loop_context::RunLoopContext::new(
        &mut test_ctx.renderer,
        &test_ctx.handle,
        &mut registry,
        &tool_defs,
        &result_cache,
        &permission_cache_arc,
        &test_ctx.permissions_state,
        &decision_ledger,
        &mut session_stats,
        &mut mcp_panel,
        &approval_recorder,
        &mut test_ctx.session,
        None,
        &traj,
        &mut harness_state,
        None,
    );

    let call = vtcode_core::llm::provider::ToolCall::function(
        "call_plan_task_tracker_non_prevalidated".to_string(),
        tools::TASK_TRACKER.to_string(),
        r#"{"action":"list"}"#.to_string(),
    );
    let ctrl_c_state = Arc::new(CtrlCState::new());
    let ctrl_c_notify = Arc::new(Notify::new());

    let outcome = run_tool_call(
        &mut ctx,
        &call,
        &ctrl_c_state,
        &ctrl_c_notify,
        None,
        None,
        true,
        None,
        0,
        false,
    )
    .await
    .expect("run_tool_call must run");

    match outcome.status {
        ToolExecutionStatus::Success { output, .. } => {
            assert!(
                output["status"] == "ok" || output["status"] == "empty",
                "unexpected status: {}",
                output["status"]
            );
        }
        other => panic!(
            "Expected task_tracker success in plan mode, got: {:?}",
            other
        ),
    }

    assert_eq!(ctx.harness_state.tool_calls, 1);
}

#[tokio::test]
async fn test_run_tool_call_prevalidated_allows_plan_task_tracker_in_plan_mode() {
    let mut test_ctx = TestContext::new().await;
    let mut registry = test_ctx.registry;

    let permission_cache_arc = Arc::new(tokio::sync::RwLock::new(ToolPermissionCache::new()));
    let result_cache = Arc::new(tokio::sync::RwLock::new(ToolResultCache::new(10)));
    let decision_ledger = Arc::new(tokio::sync::RwLock::new(DecisionTracker::new()));
    let mut session_stats = crate::agent::runloop::unified::state::SessionStats::default();
    let mut mcp_panel = crate::agent::runloop::mcp_events::McpPanelState::new(10, true);
    let approval_recorder = test_ctx.approval_recorder;
    let traj = TrajectoryLogger::new(&test_ctx.workspace);
    let tool_defs = Arc::new(tokio::sync::RwLock::new(Vec::new()));

    registry.enable_plan_mode();
    registry.plan_mode_state().enable();
    session_stats.set_plan_mode(true);

    let plans_dir = test_ctx.workspace.join(".vtcode").join("plans");
    std::fs::create_dir_all(&plans_dir).expect("create plans dir");
    let plan_file = plans_dir.join("tracker-test.md");
    std::fs::write(&plan_file, "# Tracker Test\n").expect("write plan file");
    registry
        .plan_mode_state()
        .set_plan_file(Some(plan_file))
        .await;

    let mut harness_state = build_harness_state();
    let mut ctx = crate::agent::runloop::unified::run_loop_context::RunLoopContext::new(
        &mut test_ctx.renderer,
        &test_ctx.handle,
        &mut registry,
        &tool_defs,
        &result_cache,
        &permission_cache_arc,
        &test_ctx.permissions_state,
        &decision_ledger,
        &mut session_stats,
        &mut mcp_panel,
        &approval_recorder,
        &mut test_ctx.session,
        None,
        &traj,
        &mut harness_state,
        None,
    );

    let call = vtcode_core::llm::provider::ToolCall::function(
        "call_plan_task_tracker_allowed".to_string(),
        tools::PLAN_TASK_TRACKER.to_string(),
        r#"{"action":"create","items":["Define guard","  Verify guard"]}"#.to_string(),
    );
    let ctrl_c_state = Arc::new(CtrlCState::new());
    let ctrl_c_notify = Arc::new(Notify::new());

    let outcome = run_tool_call(
        &mut ctx,
        &call,
        &ctrl_c_state,
        &ctrl_c_notify,
        None,
        None,
        true,
        None,
        0,
        true,
    )
    .await
    .expect("run_tool_call must run");

    match outcome.status {
        ToolExecutionStatus::Success { output, .. } => {
            assert_eq!(output["status"], "created");
            assert_eq!(output["checklist"]["total"], 2);
        }
        other => panic!("Expected success, got: {:?}", other),
    }
}

#[tokio::test]
async fn test_run_tool_call_non_prevalidated_blocks_plan_task_tracker_outside_plan_mode_without_budget_use()
 {
    let mut test_ctx = TestContext::new().await;
    let mut registry = test_ctx.registry;

    let permission_cache_arc = Arc::new(tokio::sync::RwLock::new(ToolPermissionCache::new()));
    let result_cache = Arc::new(tokio::sync::RwLock::new(ToolResultCache::new(10)));
    let decision_ledger = Arc::new(tokio::sync::RwLock::new(DecisionTracker::new()));
    let mut session_stats = crate::agent::runloop::unified::state::SessionStats::default();
    let mut mcp_panel = crate::agent::runloop::mcp_events::McpPanelState::new(10, true);
    let approval_recorder = test_ctx.approval_recorder;
    let traj = TrajectoryLogger::new(&test_ctx.workspace);
    let tools = Arc::new(tokio::sync::RwLock::new(Vec::new()));

    let mut harness_state = build_harness_state_with(2);
    let mut ctx = crate::agent::runloop::unified::run_loop_context::RunLoopContext::new(
        &mut test_ctx.renderer,
        &test_ctx.handle,
        &mut registry,
        &tools,
        &result_cache,
        &permission_cache_arc,
        &test_ctx.permissions_state,
        &decision_ledger,
        &mut session_stats,
        &mut mcp_panel,
        &approval_recorder,
        &mut test_ctx.session,
        None,
        &traj,
        &mut harness_state,
        None,
    );

    let call = vtcode_core::llm::provider::ToolCall::function(
        "call_plan_task_tracker_blocked".to_string(),
        tools::PLAN_TASK_TRACKER.to_string(),
        r#"{"action":"list"}"#.to_string(),
    );
    let ctrl_c_state = Arc::new(CtrlCState::new());
    let ctrl_c_notify = Arc::new(Notify::new());

    let outcome = run_tool_call(
        &mut ctx,
        &call,
        &ctrl_c_state,
        &ctrl_c_notify,
        None,
        None,
        true,
        None,
        0,
        false,
    )
    .await
    .expect("run_tool_call must run");

    match outcome.status {
        ToolExecutionStatus::Failure { error } => {
            assert!(error.to_string().contains("plan_task_tracker"));
            assert!(error.to_string().contains("compatibility alias"));
        }
        other => panic!("Expected plan mode failure, got: {:?}", other),
    }

    assert_eq!(ctx.harness_state.tool_calls, 0);
}

#[tokio::test]
async fn test_run_tool_call_invalid_preflight_does_not_consume_budget() {
    let mut test_ctx = TestContext::new().await;
    let mut registry = test_ctx.registry;

    let permission_cache_arc = Arc::new(tokio::sync::RwLock::new(ToolPermissionCache::new()));
    let result_cache = Arc::new(tokio::sync::RwLock::new(ToolResultCache::new(10)));
    let decision_ledger = Arc::new(tokio::sync::RwLock::new(DecisionTracker::new()));
    let mut session_stats = crate::agent::runloop::unified::state::SessionStats::default();
    let mut mcp_panel = crate::agent::runloop::mcp_events::McpPanelState::new(10, true);
    let approval_recorder = test_ctx.approval_recorder;
    let traj = TrajectoryLogger::new(&test_ctx.workspace);
    let tools = Arc::new(tokio::sync::RwLock::new(Vec::new()));

    let mut harness_state = build_harness_state_with(1);
    let mut ctx = crate::agent::runloop::unified::run_loop_context::RunLoopContext::new(
        &mut test_ctx.renderer,
        &test_ctx.handle,
        &mut registry,
        &tools,
        &result_cache,
        &permission_cache_arc,
        &test_ctx.permissions_state,
        &decision_ledger,
        &mut session_stats,
        &mut mcp_panel,
        &approval_recorder,
        &mut test_ctx.session,
        None,
        &traj,
        &mut harness_state,
        None,
    );

    let call = vtcode_core::llm::provider::ToolCall::function(
        "call_invalid_preflight".to_string(),
        tools::READ_FILE.to_string(),
        r#"{"path":"/var/db/shadow"}"#.to_string(),
    );
    let ctrl_c_state = Arc::new(CtrlCState::new());
    let ctrl_c_notify = Arc::new(Notify::new());

    let first_outcome = run_tool_call(
        &mut ctx,
        &call,
        &ctrl_c_state,
        &ctrl_c_notify,
        None,
        None,
        false,
        None,
        0,
        false,
    )
    .await
    .expect("first run_tool_call must run");

    assert!(matches!(
        first_outcome.status,
        ToolExecutionStatus::Failure { .. }
    ));
    assert_eq!(ctx.harness_state.tool_calls, 0);

    let second_outcome = run_tool_call(
        &mut ctx,
        &call,
        &ctrl_c_state,
        &ctrl_c_notify,
        None,
        None,
        false,
        None,
        0,
        false,
    )
    .await
    .expect("second run_tool_call must run");

    assert!(matches!(
        second_outcome.status,
        ToolExecutionStatus::Failure { .. }
    ));
    assert_eq!(ctx.harness_state.tool_calls, 0);
}

#[tokio::test]
async fn test_run_tool_call_unified_exec_git_diff_uses_cache_on_repeat() {
    let mut test_ctx = TestContext::new().await;
    std::fs::create_dir_all(&test_ctx.workspace).expect("create workspace directory");
    std::fs::write(test_ctx.workspace.join("a.txt"), "same-content\n").expect("write a.txt");
    std::fs::write(test_ctx.workspace.join("b.txt"), "same-content\n").expect("write b.txt");

    let mut registry = test_ctx.registry;
    let permission_cache_arc = Arc::new(tokio::sync::RwLock::new(ToolPermissionCache::new()));
    {
        let mut cache = permission_cache_arc.write().await;
        cache.cache_grant(tools::UNIFIED_EXEC.to_string(), PermissionGrant::Permanent);
    }

    let result_cache = Arc::new(tokio::sync::RwLock::new(ToolResultCache::new(32)));
    let decision_ledger = Arc::new(tokio::sync::RwLock::new(DecisionTracker::new()));
    let mut session_stats = crate::agent::runloop::unified::state::SessionStats::default();
    let mut mcp_panel = crate::agent::runloop::mcp_events::McpPanelState::new(10, true);
    let approval_recorder = test_ctx.approval_recorder;
    let traj = TrajectoryLogger::new(&test_ctx.workspace);
    let tools = Arc::new(tokio::sync::RwLock::new(Vec::new()));

    let mut harness_state = build_harness_state();
    let mut ctx = crate::agent::runloop::unified::run_loop_context::RunLoopContext::new(
        &mut test_ctx.renderer,
        &test_ctx.handle,
        &mut registry,
        &tools,
        &result_cache,
        &permission_cache_arc,
        &test_ctx.permissions_state,
        &decision_ledger,
        &mut session_stats,
        &mut mcp_panel,
        &approval_recorder,
        &mut test_ctx.session,
        None,
        &traj,
        &mut harness_state,
        None,
    );

    let args = serde_json::to_string(&json!({
        "action": "run",
        "command": "git diff --no-index ./a.txt ./b.txt"
    }))
    .expect("serialize unified_exec args");

    let first_call = vtcode_core::llm::provider::ToolCall::function(
        "call_unified_exec_1".to_string(),
        tools::UNIFIED_EXEC.to_string(),
        args.clone(),
    );
    let second_call = vtcode_core::llm::provider::ToolCall::function(
        "call_unified_exec_2".to_string(),
        tools::UNIFIED_EXEC.to_string(),
        args,
    );

    let ctrl_c_state = Arc::new(CtrlCState::new());
    let ctrl_c_notify = Arc::new(Notify::new());

    let first_outcome = run_tool_call(
        &mut ctx,
        &first_call,
        &ctrl_c_state,
        &ctrl_c_notify,
        None,
        None,
        true,
        None,
        0,
        false,
    )
    .await
    .expect("first unified_exec call must run");

    let second_outcome = run_tool_call(
        &mut ctx,
        &second_call,
        &ctrl_c_state,
        &ctrl_c_notify,
        None,
        None,
        true,
        None,
        0,
        false,
    )
    .await
    .expect("second unified_exec call must run");

    let extract_session_id = |status: &ToolExecutionStatus| -> String {
        match status {
            ToolExecutionStatus::Success {
                output,
                command_success,
                ..
            } => {
                assert!(*command_success);
                output
                    .get("session_id")
                    .or_else(|| output.get("id"))
                    .and_then(|value| value.as_str())
                    .expect("command output should include session id")
                    .to_string()
            }
            other => panic!("Expected success status, got: {:?}", other),
        }
    };

    let first_session_id = extract_session_id(&first_outcome.status);
    let second_session_id = extract_session_id(&second_outcome.status);
    assert_eq!(first_session_id, second_session_id);

    let first_output = match &first_outcome.status {
        ToolExecutionStatus::Success { output, .. } => output,
        _ => unreachable!(),
    };
    let second_output = match &second_outcome.status {
        ToolExecutionStatus::Success { output, .. } => output,
        _ => unreachable!(),
    };

    let mut first_stable = first_output.clone();
    let mut second_stable = second_output.clone();
    let first_wall_time = first_stable
        .get("wall_time")
        .and_then(|value| value.as_f64())
        .expect("first output should include wall_time");
    let second_wall_time = second_stable
        .get("wall_time")
        .and_then(|value| value.as_f64())
        .expect("second output should include wall_time");
    assert!(first_wall_time >= 0.0);
    assert!(second_wall_time >= 0.0);
    first_stable
        .as_object_mut()
        .map(|object| object.remove("wall_time"));
    second_stable
        .as_object_mut()
        .map(|object| object.remove("wall_time"));
    assert_eq!(first_stable, second_stable);
}

#[tokio::test]
async fn test_run_tool_call_rejects_escalated_shell_when_hitl_disabled() {
    let mut test_ctx = TestContext::new().await;
    let mut registry = test_ctx.registry;

    let permission_cache_arc = Arc::new(tokio::sync::RwLock::new(ToolPermissionCache::new()));
    let result_cache = Arc::new(tokio::sync::RwLock::new(ToolResultCache::new(10)));
    let decision_ledger = Arc::new(tokio::sync::RwLock::new(DecisionTracker::new()));
    let mut session_stats = crate::agent::runloop::unified::state::SessionStats::default();
    let mut mcp_panel = crate::agent::runloop::mcp_events::McpPanelState::new(10, true);
    let approval_recorder = test_ctx.approval_recorder;
    let traj = TrajectoryLogger::new(&test_ctx.workspace);
    let tools = Arc::new(tokio::sync::RwLock::new(Vec::new()));

    let mut harness_state = build_harness_state();
    let mut ctx = crate::agent::runloop::unified::run_loop_context::RunLoopContext::new(
        &mut test_ctx.renderer,
        &test_ctx.handle,
        &mut registry,
        &tools,
        &result_cache,
        &permission_cache_arc,
        &test_ctx.permissions_state,
        &decision_ledger,
        &mut session_stats,
        &mut mcp_panel,
        &approval_recorder,
        &mut test_ctx.session,
        None,
        &traj,
        &mut harness_state,
        None,
    );

    let mut vt_cfg = VTCodeConfig::default();
    vt_cfg.security.human_in_the_loop = false;

    let args = serde_json::to_string(&json!({
        "action": "run",
        "command": "echo hi",
        "sandbox_permissions": "require_escalated",
        "justification": "Do you want to run this command without sandbox restrictions?"
    }))
    .expect("serialize unified_exec args");

    let call = vtcode_core::llm::provider::ToolCall::function(
        "call_unified_exec_escalated".to_string(),
        tools::UNIFIED_EXEC.to_string(),
        args,
    );
    let ctrl_c_state = Arc::new(CtrlCState::new());
    let ctrl_c_notify = Arc::new(Notify::new());

    let outcome = run_tool_call(
        &mut ctx,
        &call,
        &ctrl_c_state,
        &ctrl_c_notify,
        None,
        None,
        false,
        Some(&vt_cfg),
        0,
        false,
    )
    .await
    .expect("run_tool_call must run");

    match outcome.status {
        ToolExecutionStatus::Failure { error } => {
            assert!(error.to_string().contains("Tool permission denied"));
        }
        other => panic!("Expected permission denial, got: {:?}", other),
    }
    assert_eq!(ctx.harness_state.tool_calls, 0);
}

#[tokio::test]
async fn test_run_tool_call_allows_escalated_shell_with_saved_prefix_rule() {
    let mut test_ctx = TestContext::new().await;
    let mut registry = test_ctx.registry;

    let permission_cache_arc = Arc::new(tokio::sync::RwLock::new(ToolPermissionCache::new()));
    let result_cache = Arc::new(tokio::sync::RwLock::new(ToolResultCache::new(10)));
    let decision_ledger = Arc::new(tokio::sync::RwLock::new(DecisionTracker::new()));
    let mut session_stats = crate::agent::runloop::unified::state::SessionStats::default();
    let mut mcp_panel = crate::agent::runloop::mcp_events::McpPanelState::new(10, true);
    let approval_recorder = test_ctx.approval_recorder;
    let traj = TrajectoryLogger::new(&test_ctx.workspace);
    let tools = Arc::new(tokio::sync::RwLock::new(Vec::new()));

    let mut harness_state = build_harness_state();
    let mut ctx = crate::agent::runloop::unified::run_loop_context::RunLoopContext::new(
        &mut test_ctx.renderer,
        &test_ctx.handle,
        &mut registry,
        &tools,
        &result_cache,
        &permission_cache_arc,
        &test_ctx.permissions_state,
        &decision_ledger,
        &mut session_stats,
        &mut mcp_panel,
        &approval_recorder,
        &mut test_ctx.session,
        None,
        &traj,
        &mut harness_state,
        None,
    );

    let mut vt_cfg = VTCodeConfig::default();
    vt_cfg.security.human_in_the_loop = false;
    vt_cfg.commands.approval_prefixes.push(
        "echo hi|sandbox_permissions=\"require_escalated\"|additional_permissions=null".to_string(),
    );
    ctx.tool_registry.apply_commands_config(&vt_cfg.commands);

    let args = serde_json::to_string(&json!({
        "action": "run",
        "command": "echo hi",
        "sandbox_permissions": "require_escalated",
        "justification": "Do you want to run this command without sandbox restrictions?"
    }))
    .expect("serialize unified_exec args");

    let call = vtcode_core::llm::provider::ToolCall::function(
        "call_unified_exec_escalated_saved_prefix".to_string(),
        tools::UNIFIED_EXEC.to_string(),
        args,
    );
    let ctrl_c_state = Arc::new(CtrlCState::new());
    let ctrl_c_notify = Arc::new(Notify::new());

    let outcome = run_tool_call(
        &mut ctx,
        &call,
        &ctrl_c_state,
        &ctrl_c_notify,
        None,
        None,
        false,
        Some(&vt_cfg),
        0,
        false,
    )
    .await
    .expect("run_tool_call must run");

    match outcome.status {
        ToolExecutionStatus::Success { .. } => {}
        other => panic!(
            "Expected saved prefix approval to allow execution, got: {:?}",
            other
        ),
    }
}

#[tokio::test]
async fn test_run_tool_call_reuses_streamed_invocation_item_without_duplicate_start() {
    let mut test_ctx = TestContext::new().await;
    std::fs::create_dir_all(&test_ctx.workspace).expect("create workspace");
    std::fs::write(test_ctx.workspace.join("note.txt"), "hello\n").expect("write note.txt");
    let mut registry = test_ctx.registry;

    let permission_cache_arc = Arc::new(tokio::sync::RwLock::new(ToolPermissionCache::new()));
    {
        let mut cache = permission_cache_arc.write().await;
        cache.cache_grant(tools::READ_FILE.to_string(), PermissionGrant::Permanent);
    }

    let result_cache = Arc::new(tokio::sync::RwLock::new(ToolResultCache::new(10)));
    let decision_ledger = Arc::new(tokio::sync::RwLock::new(DecisionTracker::new()));
    let mut session_stats = crate::agent::runloop::unified::state::SessionStats::default();
    let mut mcp_panel = crate::agent::runloop::mcp_events::McpPanelState::new(10, true);
    let approval_recorder = test_ctx.approval_recorder;
    let traj = TrajectoryLogger::new(&test_ctx.workspace);
    let tools = Arc::new(tokio::sync::RwLock::new(Vec::new()));

    let log_dir = tempfile::TempDir::new().expect("log dir");
    let emitter = crate::agent::runloop::unified::inline_events::harness::HarnessEventEmitter::new(
        log_dir.path().join("harness.jsonl"),
    )
    .expect("harness emitter");

    let mut harness_state = build_harness_state();
    let tool_call_id = "call_streamed".to_string();
    let streamed_item_id = "streamed-tool-item".to_string();
    harness_state
        .remember_streamed_tool_call_items([(tool_call_id.clone(), streamed_item_id.clone())]);
    emitter
        .emit(
            crate::agent::runloop::unified::inline_events::harness::tool_started_event(
                streamed_item_id.clone(),
                tools::READ_FILE,
                Some(&json!({"path":"note.txt"})),
                Some(tool_call_id.as_str()),
            ),
        )
        .expect("emit tool started");

    let mut ctx = crate::agent::runloop::unified::run_loop_context::RunLoopContext::new(
        &mut test_ctx.renderer,
        &test_ctx.handle,
        &mut registry,
        &tools,
        &result_cache,
        &permission_cache_arc,
        &test_ctx.permissions_state,
        &decision_ledger,
        &mut session_stats,
        &mut mcp_panel,
        &approval_recorder,
        &mut test_ctx.session,
        None,
        &traj,
        &mut harness_state,
        Some(&emitter),
    );

    let call = vtcode_core::llm::provider::ToolCall::function(
        tool_call_id.clone(),
        tools::READ_FILE.to_string(),
        r#"{"path":"note.txt"}"#.to_string(),
    );
    let ctrl_c_state = Arc::new(CtrlCState::new());
    let ctrl_c_notify = Arc::new(Notify::new());

    let outcome = run_tool_call(
        &mut ctx,
        &call,
        &ctrl_c_state,
        &ctrl_c_notify,
        None,
        None,
        true,
        None,
        0,
        false,
    )
    .await
    .expect("run_tool_call must run");

    assert!(matches!(
        outcome.status,
        ToolExecutionStatus::Success { .. }
    ));
    assert!(
        ctx.harness_state
            .take_streamed_tool_call_item_id(&tool_call_id)
            .is_none()
    );

    let payload =
        std::fs::read_to_string(log_dir.path().join("harness.jsonl")).expect("read harness log");
    let mut started_count = 0usize;
    let mut completed_count = 0usize;

    for line in payload.lines() {
        let value: serde_json::Value = serde_json::from_str(line).expect("json line");
        let event = value.get("event").expect("event");
        let event_type = event
            .get("type")
            .and_then(|kind| kind.as_str())
            .unwrap_or_default();
        let item = event.get("item").expect("item");
        let item_id = item
            .get("id")
            .and_then(|id| id.as_str())
            .unwrap_or_default();
        let item_type = item
            .get("type")
            .and_then(|kind| kind.as_str())
            .unwrap_or_default();

        if item_id == streamed_item_id && item_type == "tool_invocation" {
            if event_type == "item.started" {
                started_count += 1;
            }
            if event_type == "item.completed" {
                completed_count += 1;
            }
        }
    }

    assert_eq!(started_count, 1);
    assert_eq!(completed_count, 1);
}
